• R/O
  • HTTP
  • SSH
  • HTTPS

Jindolf: 提交

Jindolfプロジェクトは、CGIゲーム「人狼BBS」を快適にプレイするための専用クライアントを製作するために発足したオープンソースプロジェクトです。


Commit MetaInfo

修订版a7ab8f9253aa74a853ec13b95d1853b691948234 (tree)
时间2020-10-11 23:48:20
作者Olyutorskii <olyutorskii@user...>
CommiterOlyutorskii

Log Message

Merge branch 'release/v4.101.4'

更改概述

差异

--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -4,6 +4,10 @@
44 Jindolf 変更履歴
55
66
7+4.101.4 (2020-10-11)
8+ ・G国URL変更に伴い JinParser 2.102.4 に対応。
9+ ・ログ出力ウィンドウをQuetexJに変更。
10+
711 4.101.2 (2020-04-20)
812 ・G国亡国に伴い JinParser 2.102.2 に対応。
913 ・ログイン管理画面の廃止。
--- a/config/checkstyle/checkstyle.xml
+++ b/config/checkstyle/checkstyle.xml
@@ -6,7 +6,7 @@
66
77 <!--
88 Checkstyle modules
9- for Checkstyle 8.31 or later
9+ for Checkstyle 8.36 or later
1010
1111 [ https://checkstyle.org/ ]
1212
@@ -197,6 +197,7 @@
197197 <module name="ParameterAssignment" />
198198 <module name="RequireThis">
199199 <property name="checkMethods" value="false" />
200+ <property name="validateOnlyOverlapping" value="false" />
200201 </module>
201202 <module name="ReturnCount">
202203 <property name="max" value="5" />
@@ -234,6 +235,7 @@
234235 <module name="JavadocBlockTagLocation" />
235236 <module name="JavadocContentLocationCheck" />
236237 <module name="JavadocMethod" />
238+ <module name="JavadocMissingWhitespaceAfterAsterisk" />
237239 <module name="JavadocParagraph" />
238240 <module name="JavadocStyle">
239241 <property
@@ -281,6 +283,7 @@
281283 <module name="Indentation">
282284 <property name="caseIndent" value="0" />
283285 </module>
286+ <module name="NoCodeInFile" />
284287 <module name="OuterTypeFilename" />
285288 <module name="TodoComment">
286289 <property name="format" value="TODO" />
@@ -305,6 +308,7 @@
305308 <module name="CatchParameterName" />
306309 <module name="ClassTypeParameterName" />
307310 <module name="ConstantName" />
311+ <module name="IllegalIdentifierName" />
308312 <module name="InterfaceTypeParameterName" />
309313 <module name="LambdaParameterName" />
310314 <module name="LocalFinalVariableName" />
@@ -314,6 +318,8 @@
314318 <module name="MethodTypeParameterName" />
315319 <module name="PackageName" />
316320 <module name="ParameterName" />
321+ <module name="PatternVariableName" />
322+ <module name="RecordTypeParameterName" />
317323 <module name="StaticVariableName" />
318324 <module name="TypeName" />
319325
@@ -344,6 +350,7 @@
344350 <module name="MethodLength" />
345351 <module name="OuterTypeNumber" />
346352 <module name="ParameterNumber" />
353+ <module name="RecordComponentNumber" />
347354
348355
349356 <!-- Whitespace -->
--- a/pom.xml
+++ b/pom.xml
@@ -16,7 +16,7 @@
1616 <groupId>jp.sourceforge.jindolf</groupId>
1717 <artifactId>jindolf</artifactId>
1818
19- <version>4.101.2</version>
19+ <version>4.101.4</version>
2020
2121 <packaging>jar</packaging>
2222 <name>Jindolf</name>
@@ -121,7 +121,7 @@
121121 <dependency>
122122 <groupId>jp.osdn.jindolf</groupId>
123123 <artifactId>jinparser</artifactId>
124- <version>2.102.2</version>
124+ <version>2.102.4</version>
125125 <scope>compile</scope>
126126 </dependency>
127127
@@ -132,6 +132,13 @@
132132 <scope>compile</scope>
133133 </dependency>
134134
135+ <dependency>
136+ <groupId>io.github.olyutorskii</groupId>
137+ <artifactId>quetexj</artifactId>
138+ <version>1.0.4</version>
139+ <scope>compile</scope>
140+ </dependency>
141+
135142 </dependencies>
136143
137144 <repositories/>
@@ -158,7 +165,7 @@
158165 <plugin>
159166 <groupId>org.apache.maven.plugins</groupId>
160167 <artifactId>maven-resources-plugin</artifactId>
161- <version>3.1.0</version>
168+ <version>3.2.0</version>
162169 </plugin>
163170
164171 <plugin>
@@ -170,19 +177,19 @@
170177 <plugin>
171178 <groupId>org.apache.maven.plugins</groupId>
172179 <artifactId>maven-surefire-plugin</artifactId>
173- <version>3.0.0-M4</version>
180+ <version>3.0.0-M5</version>
174181 </plugin>
175182
176183 <plugin>
177184 <groupId>org.apache.maven.plugins</groupId>
178185 <artifactId>maven-surefire-report-plugin</artifactId>
179- <version>3.0.0-M4</version>
186+ <version>3.0.0-M5</version>
180187 </plugin>
181188
182189 <plugin>
183190 <groupId>org.jacoco</groupId>
184191 <artifactId>jacoco-maven-plugin</artifactId>
185- <version>0.8.5</version>
192+ <version>0.8.6</version>
186193 </plugin>
187194
188195 <plugin>
@@ -194,7 +201,7 @@
194201 <plugin>
195202 <groupId>org.apache.maven.plugins</groupId>
196203 <artifactId>maven-shade-plugin</artifactId>
197- <version>3.2.1</version>
204+ <version>3.2.4</version>
198205 </plugin>
199206
200207 <plugin>
@@ -218,19 +225,19 @@
218225 <plugin>
219226 <groupId>org.apache.maven.plugins</groupId>
220227 <artifactId>maven-site-plugin</artifactId>
221- <version>3.9.0</version>
228+ <version>3.9.1</version>
222229 </plugin>
223230
224231 <plugin>
225232 <groupId>org.apache.maven.plugins</groupId>
226233 <artifactId>maven-assembly-plugin</artifactId>
227- <version>3.2.0</version>
234+ <version>3.3.0</version>
228235 </plugin>
229236
230237 <plugin>
231238 <groupId>org.apache.maven.plugins</groupId>
232239 <artifactId>maven-project-info-reports-plugin</artifactId>
233- <version>3.0.0</version>
240+ <version>3.1.1</version>
234241 </plugin>
235242
236243 <plugin>
@@ -253,7 +260,7 @@
253260 <dependency>
254261 <groupId>com.puppycrawl.tools</groupId>
255262 <artifactId>checkstyle</artifactId>
256- <version>8.31</version>
263+ <version>8.36.2</version>
257264 </dependency>
258265 </dependencies>
259266 </plugin>
@@ -267,12 +274,12 @@
267274 <plugin>
268275 <groupId>com.github.spotbugs</groupId>
269276 <artifactId>spotbugs-maven-plugin</artifactId>
270- <version>4.0.0</version>
277+ <version>4.1.3</version>
271278 <dependencies>
272279 <dependency>
273280 <groupId>com.github.spotbugs</groupId>
274281 <artifactId>spotbugs</artifactId>
275- <version>4.0.2</version>
282+ <version>4.1.3</version>
276283 </dependency>
277284 </dependencies>
278285 </plugin>
@@ -440,6 +447,7 @@
440447 <groupId>org.apache.maven.plugins</groupId>
441448 <artifactId>maven-assembly-plugin</artifactId>
442449 <configuration>
450+ <tarLongFileMode>posix</tarLongFileMode>
443451 <descriptors>
444452 <descriptor>src/assembly/src.xml</descriptor>
445453 </descriptors>
--- /dev/null
+++ b/src/main/java/jp/sfjp/jindolf/BusyStatus.java
@@ -0,0 +1,177 @@
1+/*
2+ * busy status manager
3+ *
4+ * License : The MIT License
5+ * Copyright(c) 2020 olyutorskii
6+ */
7+
8+package jp.sfjp.jindolf;
9+
10+import java.awt.EventQueue;
11+import java.lang.reflect.InvocationTargetException;
12+import java.util.concurrent.Executor;
13+import java.util.concurrent.Executors;
14+import java.util.logging.Level;
15+import java.util.logging.Logger;
16+import jp.sfjp.jindolf.view.TopFrame;
17+import jp.sfjp.jindolf.view.TopView;
18+
19+/**
20+ * ビジー状態の見た目を管理する。
21+ *
22+ * <p>ビジー状態を伴うタスクの管理も行う。
23+ *
24+ * <p>EDTで処理しきれない長時間タスクを実行している状況、
25+ * およびその間のGUI操作が抑止される期間をビジーと呼ぶ。
26+ */
27+public class BusyStatus {
28+
29+ private static final Logger LOGGER = Logger.getAnonymousLogger();
30+
31+
32+ private final TopFrame topFrame;
33+ private final Executor executor = Executors.newCachedThreadPool();
34+
35+ private boolean isBusy = false;
36+
37+
38+ /**
39+ * コンストラクタ。
40+ *
41+ * @param topFrame TopFrameインスタンス
42+ */
43+ public BusyStatus(TopFrame topFrame){
44+ super();
45+ this.topFrame = topFrame;
46+ return;
47+ }
48+
49+
50+ /**
51+ * ビジー状態か否か返す。
52+ *
53+ * @return ビジーならtrue
54+ */
55+ public boolean isBusy(){
56+ return this.isBusy;
57+ }
58+
59+ /**
60+ * ビジー状態を設定する。
61+ *
62+ * <p>ヘビーなタスク実行をアピールするために、
63+ * プログレスバーとカーソルの設定を行う。
64+ *
65+ * <p>ビジー中のトップフレームのマウス操作、キーボード入力は
66+ * 全てグラブされるため無視される。
67+ *
68+ * @param flag trueならプログレスバーのアニメ開始&amp;WAITカーソル。
69+ * falseなら停止&amp;通常カーソル。
70+ * @param msg フッタメッセージ。nullなら変更なし。
71+ */
72+ public void setBusy(boolean flag, String msg){
73+ if(EventQueue.isDispatchThread()){
74+ setBusyEdt(flag, msg);
75+ return;
76+ }
77+
78+ try{
79+ EventQueue.invokeAndWait(() -> {
80+ setBusyEdt(flag, msg);
81+ });
82+ }catch(InterruptedException | InvocationTargetException e){
83+ LOGGER.log(Level.SEVERE, "ビジー処理で失敗", e);
84+ }
85+
86+ return;
87+ }
88+
89+ /**
90+ * ビジー状態を設定する。
91+ *
92+ * <p>原則EDTから呼ばねばならない。
93+ *
94+ * @param flag trueならビジー
95+ * @param msg メッセージ。nullなら変更なし。
96+ */
97+ public void setBusyEdt(boolean flag, String msg){
98+ assert EventQueue.isDispatchThread();
99+
100+ this.isBusy = flag;
101+
102+ this.topFrame.setBusy(this.isBusy);
103+
104+ if(msg != null){
105+ TopView topView = this.topFrame.getTopView();
106+ topView.updateSysMessage(msg);
107+ }
108+
109+ return;
110+ }
111+
112+ /**
113+ * 軽量タスクをEDTで実行する。
114+ *
115+ * <p>タスク実行中はビジー状態となる。
116+ *
117+ * <p>軽量タスク実行中はイベントループが停止するので、
118+ * 入出力待ちを伴わなずに早急に終わるタスクでなければならない。
119+ *
120+ * @param task 軽量タスク
121+ * @param beforeMsg ビジー中ステータス文字列
122+ * @param afterMsg ビジー復帰時のステータス文字列
123+ */
124+ public void submitLightBusyTask(Runnable task,
125+ String beforeMsg,
126+ String afterMsg ){
127+ EventQueue.invokeLater(() -> {
128+ setBusy(true, beforeMsg);
129+ });
130+
131+ EventQueue.invokeLater(task);
132+
133+ EventQueue.invokeLater(() -> {
134+ setBusy(false, afterMsg);
135+ });
136+
137+ return;
138+ }
139+
140+ /**
141+ * 重量級タスクをEDTとは別のスレッドで実行する。
142+ *
143+ * <p>タスク実行中はビジー状態となる。
144+ *
145+ * @param heavyTask 重量級タスク
146+ * @param beforeMsg ビジー中ステータス文字列
147+ * @param afterMsg ビジー復帰時のステータス文字列
148+ */
149+ public void submitHeavyBusyTask(final Runnable heavyTask,
150+ final String beforeMsg,
151+ final String afterMsg ){
152+ setBusy(true, beforeMsg);
153+
154+ EventQueue.invokeLater(() -> {
155+ fork(() -> {
156+ try{
157+ heavyTask.run();
158+ }finally{
159+ setBusy(false, afterMsg);
160+ }
161+ });
162+ });
163+
164+ return;
165+ }
166+
167+ /**
168+ * スレッドプールを用いて非EDTなタスクを投入する。
169+ *
170+ * @param task タスク
171+ */
172+ private void fork(Runnable task){
173+ this.executor.execute(task);
174+ return;
175+ }
176+
177+}
--- a/src/main/java/jp/sfjp/jindolf/Controller.java
+++ b/src/main/java/jp/sfjp/jindolf/Controller.java
@@ -16,12 +16,9 @@ import java.awt.event.WindowAdapter;
1616 import java.awt.event.WindowEvent;
1717 import java.io.File;
1818 import java.io.IOException;
19-import java.lang.reflect.InvocationTargetException;
2019 import java.net.URL;
2120 import java.text.MessageFormat;
2221 import java.util.List;
23-import java.util.concurrent.Executor;
24-import java.util.concurrent.Executors;
2522 import java.util.logging.Handler;
2623 import java.util.logging.Level;
2724 import java.util.logging.Logger;
@@ -45,6 +42,7 @@ import javax.swing.filechooser.FileNameExtensionFilter;
4542 import javax.swing.tree.TreePath;
4643 import jp.sfjp.jindolf.config.AppSetting;
4744 import jp.sfjp.jindolf.config.ConfigStore;
45+import jp.sfjp.jindolf.config.JsonIo;
4846 import jp.sfjp.jindolf.config.OptionInfo;
4947 import jp.sfjp.jindolf.data.Anchor;
5048 import jp.sfjp.jindolf.data.DialogPref;
@@ -120,8 +118,7 @@ public class Controller
120118 private final ChangeListener filterWatcher =
121119 new FilterWatcher();
122120
123- private final Executor executor = Executors.newCachedThreadPool();
124- private volatile boolean isBusyNow;
121+ private final BusyStatus busyStatus;
125122
126123
127124 /**
@@ -184,15 +181,18 @@ public class Controller
184181 shutdown();
185182 }
186183 });
184+ this.busyStatus = new BusyStatus(topFrame);
187185
188186 filterPanel.addChangeListener(this.filterWatcher);
189187
190188 Handler newHandler = logFrame.getHandler();
191- LogUtils.switchHandler(newHandler);
189+ EventQueue.invokeLater(() -> {
190+ LogUtils.switchHandler(newHandler);
191+ });
192192
193- ConfigStore config = this.appSetting.getConfigStore();
193+ JsonIo jsonIo = this.appSetting.getJsonIo();
194194
195- JsObject history = config.loadHistoryConfig();
195+ JsObject history = jsonIo.loadHistoryConfig();
196196 findPanel.putJson(history);
197197
198198 FontInfo fontInfo = this.appSetting.getFontInfo();
@@ -323,34 +323,6 @@ public class Controller
323323 }
324324
325325 /**
326- * ビジー状態の設定を行う。
327- *
328- * <p>ヘビーなタスク実行をアピールするために、
329- * プログレスバーとカーソルの設定を行う。
330- *
331- * <p>ビジー中のActionコマンド受信は無視される。
332- *
333- * <p>ビジー中のトップフレームのマウス操作、キーボード入力は
334- * 全てグラブされるため無視される。
335- *
336- * @param isBusy trueならプログレスバーのアニメ開始&amp;WAITカーソル。
337- * falseなら停止&amp;通常カーソル。
338- * @param msg フッタメッセージ。nullなら変更なし。
339- */
340- private void setBusy(boolean isBusy, String msg){
341- this.isBusyNow = isBusy;
342-
343- TopFrame topFrame = getTopFrame();
344-
345- topFrame.setBusy(isBusy);
346- if(msg != null){
347- this.topView.updateSysMessage(msg);
348- }
349-
350- return;
351- }
352-
353- /**
354326 * ステータスバーを更新する。
355327 * @param message メッセージ
356328 */
@@ -360,91 +332,6 @@ public class Controller
360332 }
361333
362334 /**
363- * ビジー状態を設定する。
364- *
365- * <p>EDT以外から呼ばれると実際の処理が次回のEDT移行に遅延される。
366- *
367- * @param isBusy ビジーならtrue
368- * @param message ステータスバー表示。nullなら変更なし
369- */
370- public void submitBusyStatus(boolean isBusy, String message){
371- Runnable task = () -> {
372- setBusy(isBusy, message);
373- };
374-
375- if(EventQueue.isDispatchThread()){
376- task.run();
377- }else{
378- try{
379- EventQueue.invokeAndWait(task);
380- }catch(InvocationTargetException | InterruptedException e){
381- LOGGER.log(Level.SEVERE, "ビジー処理で失敗", e);
382- }
383- }
384-
385- return;
386- }
387-
388- /**
389- * 軽量タスクをEDTで実行する。
390- *
391- * <p>タスク実行中はビジー状態となる。
392- *
393- * <p>軽量タスク実行中はイベントループが停止するので、
394- * 入出力待ちを伴わなずに早急に終わるタスクでなければならない。
395- *
396- * @param task 軽量タスク
397- * @param beforeMsg ビジー中ステータス文字列
398- * @param afterMsg ビジー復帰時のステータス文字列
399- */
400- public void submitLightBusyTask(Runnable task,
401- String beforeMsg,
402- String afterMsg ){
403- submitBusyStatus(true, beforeMsg);
404- EventQueue.invokeLater(task);
405- submitBusyStatus(false, afterMsg);
406-
407- return;
408- }
409-
410- /**
411- * 重量級タスクをEDTとは別のスレッドで実行する。
412- *
413- * <p>タスク実行中はビジー状態となる。
414- *
415- * @param heavyTask 重量級タスク
416- * @param beforeMsg ビジー中ステータス文字列
417- * @param afterMsg ビジー復帰時のステータス文字列
418- */
419- public void submitHeavyBusyTask(final Runnable heavyTask,
420- final String beforeMsg,
421- final String afterMsg ){
422- submitBusyStatus(true, beforeMsg);
423-
424- EventQueue.invokeLater(() -> {
425- fork(() -> {
426- try{
427- heavyTask.run();
428- }finally{
429- submitBusyStatus(false, afterMsg);
430- }
431- });
432- });
433-
434- return;
435- }
436-
437- /**
438- * スレッドプールを用いて非EDTなタスクを投入する。
439- *
440- * @param task タスク
441- */
442- private void fork(Runnable task){
443- this.executor.execute(task);
444- return;
445- }
446-
447- /**
448335 * About画面を表示する。
449336 */
450337 private void actionAbout(){
@@ -454,8 +341,7 @@ public class Controller
454341 JOptionPane.DEFAULT_OPTION,
455342 GUIUtils.getLogoIcon());
456343
457- JDialog dialog = pane.createDialog(getTopFrame(),
458- VerInfo.TITLE + "について");
344+ JDialog dialog = pane.createDialog(VerInfo.TITLE + "について");
459345
460346 dialog.pack();
461347 dialog.setVisible(true);
@@ -601,7 +487,7 @@ public class Controller
601487 String className = this.actionManager.getSelectedLookAndFeel();
602488 if(className == null) return;
603489
604- submitLightBusyTask(
490+ this.busyStatus.submitLightBusyTask(
605491 () -> {taskChangeLaF(className);},
606492 "Look&Feelを更新中…",
607493 "Look&Feelが更新されました"
@@ -758,7 +644,7 @@ public class Controller
758644 JOptionPane pane = new JOptionPane(message,
759645 JOptionPane.WARNING_MESSAGE,
760646 JOptionPane.DEFAULT_OPTION );
761- JDialog dialog = pane.createDialog(getTopFrame(), title);
647+ JDialog dialog = pane.createDialog(title);
762648 dialog.pack();
763649 dialog.setVisible(true);
764650 dialog.dispose();
@@ -776,7 +662,7 @@ public class Controller
776662 });
777663 };
778664
779- submitHeavyBusyTask(
665+ this.busyStatus.submitHeavyBusyTask(
780666 task,
781667 "一括読み込み開始",
782668 "一括読み込み完了"
@@ -862,7 +748,7 @@ public class Controller
862748 * 一括検索処理。
863749 */
864750 private void bulkSearch(){
865- submitHeavyBusyTask(
751+ this.busyStatus.submitHeavyBusyTask(
866752 () -> {taskBulkSearch();},
867753 null, null
868754 );
@@ -1016,7 +902,7 @@ public class Controller
1016902 * 全日程の一括ロード。
1017903 */
1018904 private void actionLoadAllPeriod(){
1019- submitHeavyBusyTask(
905+ this.busyStatus.submitHeavyBusyTask(
1020906 () -> {taskLoadAllPeriod();},
1021907 "一括読み込み開始",
1022908 "一括読み込み完了"
@@ -1182,7 +1068,7 @@ public class Controller
11821068
11831069 };
11841070
1185- submitHeavyBusyTask(
1071+ this.busyStatus.submitHeavyBusyTask(
11861072 task,
11871073 "ジャンプ先の読み込み中…",
11881074 null
@@ -1199,7 +1085,7 @@ public class Controller
11991085 if(result != JFileChooser.APPROVE_OPTION) return;
12001086 File selected = this.xmlFileChooser.getSelectedFile();
12011087
1202- submitHeavyBusyTask(() -> {
1088+ this.busyStatus.submitHeavyBusyTask(() -> {
12031089 Village village;
12041090
12051091 try{
@@ -1237,7 +1123,7 @@ public class Controller
12371123 * @param land 国
12381124 */
12391125 private void submitReloadVillageList(final Land land){
1240- submitHeavyBusyTask(
1126+ this.busyStatus.submitHeavyBusyTask(
12411127 () -> {taskReloadVillageList(land);},
12421128 "村一覧を読み込み中…",
12431129 "村一覧の読み込み完了"
@@ -1303,7 +1189,7 @@ public class Controller
13031189 });
13041190 };
13051191
1306- submitHeavyBusyTask(
1192+ this.busyStatus.submitHeavyBusyTask(
13071193 task,
13081194 "会話の読み込み中",
13091195 "会話の表示が完了"
@@ -1362,7 +1248,7 @@ public class Controller
13621248 JOptionPane.DEFAULT_OPTION );
13631249
13641250 String title = VerInfo.getFrameTitle("通信異常発生");
1365- JDialog dialog = pane.createDialog(getTopFrame(), title);
1251+ JDialog dialog = pane.createDialog(title);
13661252
13671253 dialog.pack();
13681254 dialog.setVisible(true);
@@ -1416,7 +1302,7 @@ public class Controller
14161302 });
14171303 };
14181304
1419- submitHeavyBusyTask(
1305+ this.busyStatus.submitHeavyBusyTask(
14201306 task,
14211307 "村情報を読み込み中…",
14221308 "村情報の読み込み完了"
@@ -1436,7 +1322,7 @@ public class Controller
14361322 */
14371323 @Override
14381324 public void actionPerformed(ActionEvent ev){
1439- if(this.isBusyNow) return;
1325+ if(this.busyStatus.isBusy()) return;
14401326
14411327 String cmd = ev.getActionCommand();
14421328 if(cmd == null) return;
@@ -1571,7 +1457,7 @@ public class Controller
15711457 }
15721458 };
15731459
1574- submitHeavyBusyTask(
1460+ this.busyStatus.submitHeavyBusyTask(
15751461 task,
15761462 "アンカーの展開中…",
15771463 null
@@ -1584,12 +1470,12 @@ public class Controller
15841470 * アプリ正常終了処理。
15851471 */
15861472 private void shutdown(){
1587- ConfigStore configStore = this.appSetting.getConfigStore();
1473+ JsonIo jsonIo = this.appSetting.getJsonIo();
15881474
15891475 FindPanel findPanel = this.windowManager.getFindPanel();
15901476 JsObject findConf = findPanel.getJson();
15911477 if( ! findPanel.hasConfChanged(findConf) ){
1592- configStore.saveHistoryConfig(findConf);
1478+ jsonIo.saveHistoryConfig(findConf);
15931479 }
15941480
15951481 this.appSetting.saveConfig();
--- a/src/main/java/jp/sfjp/jindolf/JindolfMain.java
+++ b/src/main/java/jp/sfjp/jindolf/JindolfMain.java
@@ -12,14 +12,17 @@ import java.awt.EventQueue;
1212 import java.io.PrintStream;
1313 import java.lang.reflect.InvocationTargetException;
1414 import java.text.MessageFormat;
15+import java.util.Locale;
1516 import java.util.logging.Level;
1617 import java.util.logging.Logger;
1718 import javax.swing.JFrame;
1819 import javax.swing.UIManager;
1920 import jp.sfjp.jindolf.config.AppSetting;
2021 import jp.sfjp.jindolf.config.CmdOption;
22+import jp.sfjp.jindolf.config.ConfigDirUi;
2123 import jp.sfjp.jindolf.config.ConfigStore;
2224 import jp.sfjp.jindolf.config.EnvInfo;
25+import jp.sfjp.jindolf.config.FileUtils;
2326 import jp.sfjp.jindolf.config.OptionInfo;
2427 import jp.sfjp.jindolf.data.LandsTreeModel;
2528 import jp.sfjp.jindolf.log.LogUtils;
@@ -103,9 +106,8 @@ public final class JindolfMain {
103106
104107 /**
105108 * 起動時の諸々の情報をログ出力する。
106- * @param appSetting アプリ設定
107109 */
108- private static void dumpBootInfo(AppSetting appSetting){
110+ private static void logBootInfo(){
109111 Object[] logArgs;
110112
111113 logArgs = new Object[]{
@@ -125,28 +127,59 @@ public final class JindolfMain {
125127 };
126128 LOGGER.log(Level.INFO, LOG_HEAP, logArgs);
127129
128- OptionInfo optinfo = appSetting.getOptionInfo();
130+ if(FileUtils.isWindowsOSFs()){
131+ LOGGER.info("Windows環境と認識されました。");
132+ }
133+
134+ if(FileUtils.isMacOSXFs()){
135+ LOGGER.info("macOS環境と認識されました。");
136+ }
137+
138+ Locale locale = Locale.getDefault();
139+ String localeTxt = locale.toString();
140+ LOGGER.log(Level.INFO, "ロケールに{0}が用いられます。", localeTxt);
141+
142+ return;
143+ }
144+
145+ /**
146+ * 起動時の諸々の情報をログ出力する。
147+ *
148+ * @param optinfo コマンドラインオプション
149+ */
150+ private static void logBootInfo(OptionInfo optinfo){
129151 StringBuilder bootArgs = new StringBuilder();
152+
130153 bootArgs.append("\n\n").append("起動時引数:\n");
131- for(String arg : optinfo.getInvokeArgList()){
132- bootArgs.append("\u0020\u0020").append(arg).append('\n');
133- }
154+ optinfo.getInvokeArgList().forEach(arg ->
155+ bootArgs.append("\u0020\u0020").append(arg).append('\n')
156+ );
134157 bootArgs.append('\n');
158+
135159 bootArgs.append(EnvInfo.getVMInfo());
160+
136161 LOGGER.info(bootArgs.toString());
137162
138- ConfigStore configStore = appSetting.getConfigStore();
163+ return;
164+ }
165+
166+ /**
167+ * 起動時の諸々の情報をログ出力する。
168+ *
169+ * @param configStore 設定ディレクトリ情報
170+ */
171+ private static void logBootInfo(ConfigStore configStore){
139172 if(configStore.useStoreFile()){
140173 LOGGER.log(Level.INFO, LOG_CONF, configStore.getConfigDir());
141174 }else{
142175 LOGGER.info(LOG_NOCONF);
143176 }
144-
145177 return;
146178 }
147179
148180 /**
149181 * JindolfMain のスタートアップエントリ。
182+ *
150183 * @param args コマンドライン引数
151184 * @return 起動に成功すれば0。失敗したら0以外。
152185 */
@@ -175,6 +208,7 @@ public final class JindolfMain {
175208
176209 /**
177210 * JindolfMain のスタートアップエントリ。
211+ *
178212 * @param optinfo コマンドライン引数情報
179213 * @return 起動に成功すれば0。失敗したら0以外。
180214 */
@@ -229,44 +263,26 @@ public final class JindolfMain {
229263 LogUtils.initRootLogger(optinfo.hasOption(CmdOption.OPT_CONSOLELOG));
230264 // ここからロギング解禁
231265
232- final AppSetting appSetting = new AppSetting(optinfo);
233- dumpBootInfo(appSetting);
266+ logBootInfo();
267+ logBootInfo(optinfo);
234268
269+ final AppSetting appSetting = new AppSetting(optinfo);
235270 ConfigStore configStore = appSetting.getConfigStore();
236- configStore.prepareConfigDir();
237- configStore.tryLock();
271+ logBootInfo(configStore);
272+
273+ ConfigDirUi.prepareConfigDir(configStore);
274+ ConfigDirUi.tryLock(configStore);
238275 // ここから設定格納ディレクトリ解禁
239276
240277 appSetting.loadConfig();
241278
242- final Runtime runtime = Runtime.getRuntime();
243- runtime.addShutdownHook(new Thread(){
244- /** {@inheritDoc} */
245- @Override
246- @SuppressWarnings("CallToThreadYield")
247- public void run(){
248- LOGGER.info("シャットダウン処理に入ります…");
249- flush();
250- runtime.gc();
251- Thread.yield();
252- runtime.runFinalization(); // 危険?
253- Thread.yield();
254- return;
255- }
256- });
257-
258279 LoggingDispatcher.replaceEventQueue();
259280
260281 int exitCode = 0;
261282 try{
262- EventQueue.invokeAndWait(new Runnable(){
263- /** {@inheritDoc} */
264- @Override
265- public void run(){
266- startGUI(appSetting);
267- return;
268- }
269- });
283+ EventQueue.invokeAndWait(() ->
284+ startGUI(appSetting)
285+ );
270286 }catch(InvocationTargetException | InterruptedException e){
271287 LOGGER.log(Level.SEVERE, FATALMSG_INITFAIL, e);
272288 e.printStackTrace(STDERR);
@@ -278,35 +294,38 @@ public final class JindolfMain {
278294
279295 /**
280296 * AWTイベントディスパッチスレッド版スタートアップエントリ。
297+ *
281298 * @param appSetting アプリ設定
282299 */
283300 private static void startGUI(AppSetting appSetting){
284301 JFrame topFrame = buildMVC(appSetting);
285-
286302 GUIUtils.modifyWindowAttributes(topFrame, true, false, true);
287-
288303 topFrame.pack();
289304
290- Dimension initGeometry =
291- new Dimension(appSetting.initialFrameWidth(),
292- appSetting.initialFrameHeight());
305+ int frameWidth = appSetting.initialFrameWidth();
306+ int frameHeight = appSetting.initialFrameHeight();
307+ Dimension initGeometry = new Dimension(frameWidth, frameHeight);
293308 topFrame.setSize(initGeometry);
294309
295- if( appSetting.initialFrameXpos() <= Integer.MIN_VALUE
296- || appSetting.initialFrameYpos() <= Integer.MIN_VALUE ){
310+ int frameXpos = appSetting.initialFrameXpos();
311+ int frameYpos = appSetting.initialFrameYpos();
312+
313+ if( frameXpos <= Integer.MIN_VALUE
314+ || frameYpos <= Integer.MIN_VALUE ){
297315 topFrame.setLocationByPlatform(true);
298316 }else{
299- topFrame.setLocation(appSetting.initialFrameXpos(),
300- appSetting.initialFrameYpos() );
317+ topFrame.setLocation(frameXpos, frameYpos);
301318 }
302319
303320 topFrame.setVisible(true);
304321
322+ // GOOD BYE BUT EVENT-LOOP WILL CONTINUE
305323 return;
306324 }
307325
308326 /**
309327 * モデル・ビュー・コントローラの結合。
328+ *
310329 * @param appSetting アプリ設定
311330 * @return アプリケーションのトップフレーム
312331 */
@@ -315,8 +334,6 @@ public final class JindolfMain {
315334 WindowManager windowManager = new WindowManager();
316335 ActionManager actionManager = new ActionManager();
317336
318- model.loadLandList();
319-
320337 Controller controller = new Controller(model,
321338 windowManager,
322339 actionManager,
--- a/src/main/java/jp/sfjp/jindolf/config/AppSetting.java
+++ b/src/main/java/jp/sfjp/jindolf/config/AppSetting.java
@@ -59,6 +59,7 @@ public class AppSetting{
5959
6060 private final OptionInfo optInfo;
6161 private final ConfigStore configStore;
62+ private final JsonIo jsonIo;
6263 private final Rectangle frameRect;
6364
6465 private FontInfo fontInfo;
@@ -83,6 +84,7 @@ public class AppSetting{
8384
8485 this.optInfo = info;
8586 this.configStore = parseConfigStore(this.optInfo);
87+ this.jsonIo = new JsonIo(this.configStore);
8688 this.frameRect = parseGeometrySetting(this.optInfo);
8789
8890 return;
@@ -97,29 +99,27 @@ public class AppSetting{
9799 private static ConfigStore parseConfigStore(OptionInfo option){
98100 CmdOption opt = option.getExclusiveOption(CmdOption.OPT_CONFDIR,
99101 CmdOption.OPT_NOCONF );
100-
101- boolean useConfig;
102- boolean isImplicitPath;
103- File configPath;
104-
105- if(opt == CmdOption.OPT_NOCONF){
106- useConfig = false;
107- isImplicitPath = true;
108- configPath = null;
109- }else if(opt == CmdOption.OPT_CONFDIR){
110- useConfig = true;
111- isImplicitPath = false;
112- String optPath = option.getStringArg(opt);
113- configPath = FileUtils.supplyFullPath(new File(optPath));
102+ ConfigStore result;
103+ if(opt == null){
104+ result = new ConfigStore(null);
114105 }else{
115- useConfig = true;
116- isImplicitPath = true;
117- configPath = ConfigFile.getImplicitConfigDirectory();
106+ switch(opt){
107+ case OPT_NOCONF:
108+ result = new ConfigStore();
109+ break;
110+ case OPT_CONFDIR:
111+ String optArg = option.getStringArg(opt);
112+ Path configPath = Paths.get(optArg);
113+ configPath = configPath.toAbsolutePath();
114+ result = new ConfigStore(configPath);
115+ break;
116+ default:
117+ result = null;
118+ assert false;
119+ break;
120+ }
118121 }
119122
120- ConfigStore result =
121- new ConfigStore(useConfig, isImplicitPath, configPath);
122-
123123 return result;
124124 }
125125
@@ -203,6 +203,15 @@ public class AppSetting{
203203 }
204204
205205 /**
206+ * JSON入出力設定を返す。
207+ *
208+ * @return 入出力設定
209+ */
210+ public JsonIo getJsonIo(){
211+ return this.jsonIo;
212+ }
213+
214+ /**
206215 * 初期のフレーム幅を返す。
207216 * @return 初期のフレーム幅
208217 */
@@ -300,7 +309,7 @@ public class AppSetting{
300309 * ネットワーク設定をロードする。
301310 */
302311 private void loadNetConfig(){
303- JsObject root = this.configStore.loadNetConfig();
312+ JsObject root = this.jsonIo.loadNetConfig();
304313 if(root == null) return;
305314 this.loadedNetConfig = root;
306315
@@ -319,7 +328,7 @@ public class AppSetting{
319328 * 会話表示設定をロードする。
320329 */
321330 private void loadTalkConfig(){
322- JsObject root = this.configStore.loadTalkConfig();
331+ JsObject root = this.jsonIo.loadTalkConfig();
323332 if(root == null) return;
324333 this.loadedTalkConfig = root;
325334
@@ -411,7 +420,7 @@ public class AppSetting{
411420 * ローカル画像設定をロードする。
412421 */
413422 private void loadLocalImageConfig(){
414- JsObject root = this.configStore.loadLocalImgConfig();
423+ JsObject root = this.jsonIo.loadLocalImgConfig();
415424 if(root == null) return;
416425
417426 JsValue faceConfig = root.getValue("avatarFace");
@@ -482,7 +491,7 @@ public class AppSetting{
482491 if(this.loadedNetConfig.equals(root)) return;
483492 }
484493
485- this.configStore.saveNetConfig(root);
494+ this.jsonIo.saveNetConfig(root);
486495
487496 return;
488497 }
@@ -516,7 +525,7 @@ public class AppSetting{
516525 if(this.loadedTalkConfig.equals(root)) return;
517526 }
518527
519- this.configStore.saveTalkConfig(root);
528+ this.jsonIo.saveTalkConfig(root);
520529
521530 return;
522531 }
--- a/src/main/java/jp/sfjp/jindolf/config/CmdOption.java
+++ b/src/main/java/jp/sfjp/jindolf/config/CmdOption.java
@@ -15,6 +15,10 @@ import jp.sfjp.jindolf.ResourceManager;
1515
1616 /**
1717 * コマンドラインオプションの列挙。
18+ *
19+ * <p>1オプションは複数の別名を持ちうる。
20+ *
21+ * <p>1引数を持つオプションと持たないオプションは区別される。
1822 */
1923 public enum CmdOption {
2024
@@ -58,7 +62,8 @@ public enum CmdOption {
5862 OPT_FRACTIONAL
5963 );
6064
61- private static final String RES_HELPTEXT = "resources/help.txt";
65+ private static final String RES_DIR = "resources";
66+ private static final String RES_HELPTEXT = RES_DIR + "/help.txt";
6267
6368
6469 private final List<String> nameList;
@@ -66,6 +71,7 @@ public enum CmdOption {
6671
6772 /**
6873 * コンストラクタ。
74+ *
6975 * @param names オプション名の一覧
7076 */
7177 private CmdOption(String ... names){
@@ -77,6 +83,7 @@ public enum CmdOption {
7783
7884 /**
7985 * ヘルプメッセージ(オプションの説明)を返す。
86+ *
8087 * @return ヘルプメッセージ
8188 */
8289 public static String getHelpText(){
@@ -86,6 +93,7 @@ public enum CmdOption {
8693
8794 /**
8895 * オプション名に合致するEnumを返す。
96+ *
8997 * @param arg 個別のコマンドライン引数
9098 * @return 合致したEnum。どれとも合致しなければnull
9199 */
@@ -96,40 +104,43 @@ public enum CmdOption {
96104 return null;
97105 }
98106
107+
99108 /**
100109 * 任意のオプション文字列がこのオプションに合致するか判定する。
110+ *
101111 * @param option ハイフンの付いたオプション文字列
102112 * @return 合致すればtrue
103113 */
104114 public boolean matches(String option){
105- for(String name : this.nameList){
106- if(option.equals(name)) return true;
107- }
108-
109- return false;
115+ boolean result = this.nameList.contains(option);
116+ return result;
110117 }
111118
112119 /**
113120 * 単体で意味をなすオプションか判定する。
121+ *
114122 * @return 単体で意味をなすならtrue
115123 */
116124 public boolean isIndepOption(){
117- if(OPTS_INDEPENDENT.contains(this)) return true;
118- return false;
125+ boolean result = OPTS_INDEPENDENT.contains(this);
126+ return result;
119127 }
120128
121129 /**
122130 * 真偽指定を一つ必要とするオプションか判定する。
131+ *
123132 * @return 真偽指定を一つ必要とするオプションならtrue
124133 */
125134 public boolean isBooleanOption(){
126- if(OPTS_BOOLEAN.contains(this)) return true;
127- return false;
135+ boolean result = OPTS_BOOLEAN.contains(this);
136+ return result;
128137 }
129138
130139 /**
131140 * オプション名を返す。
132- * オプション別名が複数指定されている場合は最初のオプション名
141+ *
142+ * <p>オプション別名が複数指定されている場合は最初のオプション名
143+ *
133144 * @return オプション名
134145 */
135146 @Override
--- /dev/null
+++ b/src/main/java/jp/sfjp/jindolf/config/ConfigDirUi.java
@@ -0,0 +1,526 @@
1+/*
2+ * configuration file & directory UI
3+ *
4+ * License : The MIT License
5+ * Copyright(c) 2009 olyutorskii
6+ */
7+
8+package jp.sfjp.jindolf.config;
9+
10+import java.io.BufferedInputStream;
11+import java.io.File;
12+import java.io.IOException;
13+import java.io.InputStream;
14+import java.nio.file.Files;
15+import java.nio.file.Path;
16+import java.nio.file.Paths;
17+import java.text.MessageFormat;
18+import java.util.logging.Level;
19+import java.util.logging.Logger;
20+import javax.swing.JDialog;
21+import javax.swing.JOptionPane;
22+import jp.sfjp.jindolf.ResourceManager;
23+import jp.sfjp.jindolf.VerInfo;
24+import jp.sfjp.jindolf.view.LockErrorPane;
25+
26+/**
27+ * ユーザとのインタラクションを伴う、
28+ * 設定格納ディレクトリに関する各種操作。
29+ *
30+ * <p>ディレクトリ作成やロック確保に伴う、
31+ * 各種異常系に対する判断をユーザーに求める。
32+ *
33+ * <p>場合によってはそのままVMごと終了し、呼び出し元に制御を返さない。
34+ *
35+ * <p>ユーザとの各種インタラクションは、
36+ * アプリのメインウィンドウが表示される前に行われる。
37+ */
38+public final class ConfigDirUi{
39+
40+ private static final Logger LOGGER = Logger.getAnonymousLogger();
41+
42+ private static final String TITLE_BUILDCONF =
43+ VerInfo.TITLE + "設定格納ディレクトリの設定";
44+
45+ private static final Path FILE_README = Paths.get("README.txt");
46+ private static final Path FILE_AVATARJSON = Paths.get("avatarCache.json");
47+
48+ private static final String RES_DIR = "resources";
49+ private static final String RES_README = RES_DIR + "/README.txt";
50+ private static final String RES_IMGDIR = RES_DIR + "/image";
51+ private static final String RES_AVATARJSON =
52+ RES_IMGDIR + "/avatarCache.json";
53+
54+ private static final String MSG_POST =
55+ "<ul>"
56+ + "<li><code>" + CmdOption.OPT_CONFDIR + "</code>"
57+ + "&nbsp;オプション指定により、<br/>"
58+ + "任意の設定格納ディレクトリを指定することができます。<br/>"
59+ + "<li><code>" + CmdOption.OPT_NOCONF + "</code>"
60+ + "&nbsp;オプション指定により、<br/>"
61+ + "設定格納ディレクトリを使わずに起動することができます。<br/>"
62+ + "</ul>";
63+ private static final String MSG_NOCONF =
64+ "<html>"
65+ + "設定ディレクトリを使わずに起動を続行します。<br/>"
66+ + "今回、各種設定の読み込み・保存はできません。<br/>"
67+ + "<code>"
68+ + CmdOption.OPT_NOCONF
69+ + "</code> オプション"
70+ + "を使うとこの警告は出なくなります。"
71+ + "</html>";
72+ private static final String MSG_ABORT =
73+ "<html>"
74+ + "設定ディレクトリの作成をせずに起動を中止します。<br/>"
75+ + MSG_POST
76+ + "</html>";
77+ private static final String FORM_FAILRM =
78+ "<html>"
79+ + "ロックファイルの強制解除に失敗しました。<br/>"
80+ + "他に動いているJindolf"
81+ + "が見つからないのであれば、<br/>"
82+ + "なんとかしてロックファイル<br/>"
83+ + "{0}"
84+ + "を削除してください。<br/>"
85+ + "起動を中止します。"
86+ + "</html>";
87+ private static final String FORM_ILLLOCK =
88+ "<html>"
89+ + "ロックファイル<br/>"
90+ + "{0}"
91+ + "を確保することができません。<br/>"
92+ + "起動を中止します。"
93+ + "</html>";
94+ private static final String FORM_MKDIRFAIL =
95+ "<html>"
96+ + "ディレクトリ<br/>"
97+ + "{0}"
98+ + "の作成に失敗しました。"
99+ + "起動を中止します。<br/>"
100+ + MSG_POST
101+ + "</html>";
102+ private static final String FORM_ACCERR =
103+ "<html>"
104+ + "ディレクトリ<br/>"
105+ + "{0}"
106+ + "へのアクセスができません。"
107+ + "起動を中止します。<br/>"
108+ + "このディレクトリへのアクセス権を調整し"
109+ + "読み書きできるようにしてください。<br/>"
110+ + MSG_POST
111+ + "</html>";
112+ private static final String FORM_WRITEERR =
113+ "<html>"
114+ + "ファイル<br/>"
115+ + "{0}"
116+ + "への書き込みができません。"
117+ + "起動を中止します。<br/>"
118+ + "</html>";
119+ private static final String FORM_MKCONF =
120+ "<html>"
121+ + "設定ファイル格納ディレクトリ<br/>"
122+ + "{0}を作成します。<br/>"
123+ + "このディレクトリを今から作成して構いませんか?<br/>"
124+ + "このディレクトリ名は、後からいつでもヘルプウィンドウで<br/>"
125+ + "確認することができます。"
126+ + "</html>";
127+
128+ private static final String LOG_MKDIRERR =
129+ "ディレクトリ{0}を生成できません";
130+ private static final String LOG_CREATEERR =
131+ "ファイル{0}を生成できません";
132+ private static final String LOG_RESCPY =
133+ "内部リソースから{0}へコピーが行われました。";
134+
135+ private static final int ERR_ABORT = 1;
136+
137+
138+ /**
139+ * 隠れコンストラクタ。
140+ */
141+ private ConfigDirUi(){
142+ assert false;
143+ }
144+
145+
146+ /**
147+ * VMごとアプリを異常終了させる。
148+ *
149+ * <p>終了コードは1。
150+ */
151+ private static void abort(){
152+ System.exit(ERR_ABORT);
153+ assert false;
154+ return;
155+ }
156+
157+ /**
158+ * ダイアログを表示し、閉じられるまで待つ。
159+ *
160+ * @param pane ダイアログの元となるペイン
161+ */
162+ private static void showDialog(JOptionPane pane){
163+ JDialog dialog = pane.createDialog(TITLE_BUILDCONF);
164+ dialog.setResizable(true);
165+ dialog.pack();
166+
167+ dialog.setVisible(true);
168+ dialog.dispose();
169+
170+ return;
171+ }
172+
173+ /**
174+ * 設定ディレクトリ操作の
175+ * 共通エラーメッセージ確認ダイアログを表示する。
176+ *
177+ * <p>閉じるまで待つ。
178+ *
179+ * @param txt メッセージ
180+ */
181+ private static void showErrorMessage(String txt){
182+ JOptionPane pane = new JOptionPane(
183+ txt, JOptionPane.ERROR_MESSAGE);
184+ showDialog(pane);
185+ return;
186+ }
187+
188+ /**
189+ * センタリングされたファイル名表示のHTML表記を出力する。
190+ *
191+ * @param path ファイル
192+ * @return HTML表記
193+ */
194+ private static String getCenteredFileName(Path path){
195+ String form = "<center>[&nbsp;{0}&nbsp;]</center><br/>";
196+ String fileName = FileUtils.getHtmledFileName(path);
197+ String result = MessageFormat.format(form, fileName);
198+ return result;
199+ }
200+
201+ /**
202+ * ディレクトリが生成できないエラーをダイアログで提示し、
203+ * VM終了する。
204+ *
205+ * @param path 生成できなかったディレクトリ
206+ */
207+ private static void abortCantBuildDir(Path path){
208+ String fileName = getCenteredFileName(path);
209+ String msg = MessageFormat.format(FORM_MKDIRFAIL, fileName);
210+
211+ showErrorMessage(msg);
212+ abort();
213+ assert false;
214+
215+ return;
216+ }
217+
218+ /**
219+ * ディレクトリへアクセスできないエラーをダイアログで提示し、
220+ * VM終了する。
221+ *
222+ * @param path アクセスできないディレクトリ
223+ */
224+ private static void abortCantAccessDir(Path path){
225+ String fileName = getCenteredFileName(path);
226+ String msg = MessageFormat.format(FORM_ACCERR, fileName);
227+
228+ showErrorMessage(msg);
229+ abort();
230+ assert false;
231+
232+ return;
233+ }
234+
235+ /**
236+ * ディレクトリが生成できない異常系をログ出力する。
237+ *
238+ * @param dirPath 生成できなかったディレクトリ
239+ * @param cause 異常系原因
240+ */
241+ private static void logMkdirErr(Path dirPath, Throwable cause){
242+ String pathTxt = dirPath.toString();
243+ String msg = MessageFormat.format(LOG_MKDIRERR, pathTxt);
244+ LOGGER.log(Level.SEVERE, msg, cause);
245+ return;
246+ }
247+
248+ /**
249+ * リソースからローカルファイルへコピーする。
250+ *
251+ * @param resource リソース名
252+ * @param dest ローカルファイル
253+ */
254+ private static void copyResource(String resource, Path dest){
255+ try(InputStream ris =
256+ ResourceManager.getResourceAsStream(resource)){
257+ InputStream is = new BufferedInputStream(ris);
258+ Files.copy(is, dest);
259+ }catch(IOException | SecurityException e){
260+ String destName = dest.toString();
261+ String logMsg = MessageFormat.format(LOG_CREATEERR, destName);
262+ LOGGER.log(Level.SEVERE, logMsg, e);
263+
264+ String destHtml = getCenteredFileName(dest);
265+ String diagMsg = MessageFormat.format(FORM_WRITEERR, destHtml);
266+ showErrorMessage(diagMsg);
267+ abort();
268+
269+ assert false;
270+ }
271+
272+ String destName = dest.toString();
273+ String msg = MessageFormat.format(LOG_RESCPY, destName);
274+ LOGGER.info(msg);
275+
276+ return;
277+ }
278+
279+ /**
280+ * 設定ディレクトリがアクセス可能でなければ
281+ * エラーダイアログを出してVM終了する。
282+ *
283+ * @param confDir 設定ディレクトリ
284+ */
285+ private static void checkDirPerm(Path confDir){
286+ if( ! FileUtils.isAccessibleDirectory(confDir) ){
287+ abortCantAccessDir(confDir);
288+ }
289+
290+ return;
291+ }
292+
293+ /**
294+ * 設定ディレクトリの存在を確認し、なければ作る。
295+ *
296+ * <p>設定ディレクトリを使わない場合は何もしない。
297+ *
298+ * @param configStore 設定ディレクトリ情報
299+ */
300+ public static void prepareConfigDir(ConfigStore configStore){
301+ if( ! configStore.useStoreFile() ) return;
302+
303+ Path conf = configStore.getConfigDir();
304+ if(Files.exists(conf)){
305+ checkDirPerm(conf);
306+ }else{
307+ buildConfDirPath(conf);
308+ }
309+
310+ Path imgDir = configStore.getLocalImgDir();
311+ if(Files.exists(imgDir)){
312+ checkDirPerm(imgDir);
313+ }else{
314+ buildImageCacheDir(imgDir);
315+ }
316+
317+ return;
318+ }
319+
320+ /**
321+ * まだ存在しない設定格納ディレクトリを新規に作成する。
322+ *
323+ * <p>エラーがあればダイアログ提示とともにVM終了する。
324+ *
325+ * <p>既に存在すればなにもしない。
326+ *
327+ * @param confPath 設定格納ディレクトリ
328+ * @return 新規に作成した設定格納ディレクトリの絶対パス
329+ */
330+ private static Path buildConfDirPath(Path confPath){
331+ assert confPath.isAbsolute();
332+ if(Files.exists(confPath)) return confPath;
333+
334+ boolean confirmed = confirmBuildConfigDir(confPath);
335+ if( ! confirmed ){
336+ JOptionPane pane = new JOptionPane(
337+ MSG_ABORT, JOptionPane.WARNING_MESSAGE);
338+ showDialog(pane);
339+ abort();
340+ assert false;
341+ }
342+
343+ try{
344+ Files.createDirectories(confPath);
345+ }catch(IOException | SecurityException e){
346+ logMkdirErr(confPath, e);
347+ abortCantBuildDir(confPath);
348+ assert false;
349+ }
350+
351+ // FileUtils.setOwnerOnlyAccess(absPath);
352+
353+ checkDirPerm(confPath);
354+
355+ Path readme = confPath.resolve(FILE_README);
356+ copyResource(RES_README, readme);
357+
358+ return confPath;
359+ }
360+
361+ /**
362+ * 設定ディレクトリを新規に生成してよいかダイアログで問い合わせる。
363+ *
364+ * @param confDir 設定ディレクトリ
365+ * @return 生成してよいと指示があればtrue
366+ */
367+ private static boolean confirmBuildConfigDir(Path confDir){
368+ String confName = getCenteredFileName(confDir);
369+ String msg = MessageFormat.format(FORM_MKCONF, confName);
370+
371+ JOptionPane pane;
372+ pane = new JOptionPane(msg,
373+ JOptionPane.QUESTION_MESSAGE,
374+ JOptionPane.YES_NO_OPTION);
375+ showDialog(pane);
376+
377+ Object val = pane.getValue();
378+ if( ! (val instanceof Integer) ) return false;
379+ int ival = (int) val;
380+ boolean result = ival == JOptionPane.YES_OPTION;
381+
382+ return result;
383+ }
384+
385+ /**
386+ * ローカル画像キャッシュディレクトリを作る。
387+ *
388+ * <p>作られたディレクトリ内に
389+ * ファイルavatarCache.jsonが作られる。
390+ *
391+ * @param imgCacheDir ローカル画像キャッシュディレクトリ
392+ */
393+ private static void buildImageCacheDir(Path imgCacheDir){
394+ assert imgCacheDir.isAbsolute();
395+ if(Files.exists(imgCacheDir)) return;
396+
397+ try{
398+ Files.createDirectories(imgCacheDir);
399+ }catch(IOException | SecurityException e){
400+ logMkdirErr(imgCacheDir, e);
401+ abortCantBuildDir(imgCacheDir);
402+ assert false;
403+ }
404+
405+ checkDirPerm(imgCacheDir);
406+
407+ Path jsonPath = imgCacheDir.resolve(FILE_AVATARJSON);
408+ copyResource(RES_AVATARJSON, jsonPath);
409+
410+ return;
411+ }
412+
413+ /**
414+ * ロックファイルの取得を試みる。
415+ *
416+ * <p>ロックに失敗したが処理を続行する場合、
417+ * 設定ディレクトリは使わないものとして続行する。
418+ *
419+ * @param configStore 設定ディレクトリ情報
420+ */
421+ public static void tryLock(ConfigStore configStore){
422+ if( ! configStore.useStoreFile() ) return;
423+
424+ Path lockPath = configStore.getLockFile();
425+ File lockFile = lockPath.toFile();
426+ InterVMLock lock = new InterVMLock(lockFile);
427+
428+ lock.tryLock();
429+
430+ if( ! lock.isFileOwner() ){
431+ confirmLockError(lock);
432+ if( ! lock.isFileOwner() ){
433+ configStore.setNoConf();
434+ }
435+ }
436+
437+ return;
438+ }
439+
440+ /**
441+ * ロックエラーダイアログの表示。
442+ *
443+ * <p>呼び出しから戻ってもまだロックオブジェクトが
444+ * ロックファイルのオーナーでない場合、
445+ * 今後設定ディレクトリは一切使わずに起動を続行するものとする。
446+ *
447+ * <p>ロックファイルの強制解除に失敗した場合はVM終了する。
448+ *
449+ * @param lock エラーを起こしたロック
450+ */
451+ private static void confirmLockError(InterVMLock lock){
452+ File lockFile = lock.getLockFile();
453+
454+ LockErrorPane lockPane = new LockErrorPane(lockFile.toPath());
455+ JDialog lockDialog = lockPane.createDialog(TITLE_BUILDCONF);
456+
457+ lockDialog.setResizable(true);
458+ lockDialog.pack();
459+
460+ do{
461+ lockDialog.setVisible(true);
462+
463+ Object result = lockPane.getValue();
464+ boolean aborted = LockErrorPane.isAborted(result);
465+ boolean windowClosed = result == null;
466+
467+ if(aborted || windowClosed){
468+ abort();
469+ assert false;
470+ break;
471+ }
472+
473+ if(lockPane.isRadioRetry()){
474+ lock.tryLock();
475+ }else if(lockPane.isRadioContinue()){
476+ JOptionPane pane = new JOptionPane(
477+ MSG_NOCONF, JOptionPane.INFORMATION_MESSAGE);
478+ showDialog(pane);
479+ break;
480+ }else if(lockPane.isRadioForce()){
481+ forceRemove(lock);
482+ break;
483+ }
484+ }while( ! lock.isFileOwner());
485+
486+ lockDialog.dispose();
487+
488+ return;
489+ }
490+
491+ /**
492+ * ロックファイルの強制削除を試みる。
493+ *
494+ * <p>削除とそれに後続する再ロック取得に成功したときだけ制御を戻す。
495+ *
496+ * <p>削除できないまたは再ロックできない場合は、
497+ * 制御を戻さずVMごとアプリを終了する。
498+ *
499+ * @param lock ロック
500+ */
501+ private static void forceRemove(InterVMLock lock){
502+ File lockFile = lock.getLockFile();
503+
504+ lock.forceRemove();
505+ if(lock.isExistsFile()){
506+ String fileName = getCenteredFileName(lockFile.toPath());
507+ String msg = MessageFormat.format(FORM_FAILRM, fileName);
508+ showErrorMessage(msg);
509+ abort();
510+ assert false;
511+ return;
512+ }
513+
514+ lock.tryLock();
515+ if(lock.isFileOwner()) return;
516+
517+ String fileName = getCenteredFileName(lockFile.toPath());
518+ String msg = MessageFormat.format(FORM_ILLLOCK, fileName);
519+ showErrorMessage(msg);
520+ abort();
521+ assert false;
522+
523+ return;
524+ }
525+
526+}
--- a/src/main/java/jp/sfjp/jindolf/config/ConfigFile.java
+++ /dev/null
@@ -1,573 +0,0 @@
1-/*
2- * configuration file & directory
3- *
4- * License : The MIT License
5- * Copyright(c) 2009 olyutorskii
6- */
7-
8-package jp.sfjp.jindolf.config;
9-
10-import java.io.File;
11-import java.io.FileOutputStream;
12-import java.io.IOException;
13-import java.io.InputStream;
14-import java.io.OutputStream;
15-import java.io.OutputStreamWriter;
16-import java.io.PrintWriter;
17-import java.io.Writer;
18-import java.nio.charset.Charset;
19-import java.nio.charset.StandardCharsets;
20-import java.nio.file.Files;
21-import java.nio.file.Path;
22-import java.nio.file.Paths;
23-import javax.swing.JDialog;
24-import javax.swing.JOptionPane;
25-import jp.sfjp.jindolf.ResourceManager;
26-import jp.sfjp.jindolf.VerInfo;
27-import jp.sfjp.jindolf.view.LockErrorPane;
28-
29-/**
30- * Jindolf設定格納ディレクトリに関するあれこれ。
31- */
32-public final class ConfigFile{
33-
34- private static final String TITLE_BUILDCONF =
35- VerInfo.TITLE + "設定格納ディレクトリの設定";
36-
37- private static final String JINCONF = "Jindolf";
38- private static final String JINCONF_DOT = ".jindolf";
39- private static final String FILE_README = "README.txt";
40- private static final Charset CHARSET_README = StandardCharsets.UTF_8;
41-
42- private static final String MSG_POST =
43- "<ul>"
44- + "<li><code>" + CmdOption.OPT_CONFDIR + "</code>"
45- + "&nbsp;オプション指定により、<br>"
46- + "任意の設定格納ディレクトリを指定することができます。<br>"
47- + "<li><code>" + CmdOption.OPT_NOCONF + "</code>"
48- + "&nbsp;オプション指定により、<br>"
49- + "設定格納ディレクトリを使わずに起動することができます。<br>"
50- + "</ul>";
51-
52-
53- /**
54- * 隠れコンストラクタ。
55- */
56- private ConfigFile(){
57- assert false;
58- return;
59- }
60-
61-
62- /**
63- * 暗黙的な設定格納ディレクトリを返す。
64- *
65- * <ul>
66- *
67- * <li>起動元JARファイルと同じディレクトリに、
68- * アクセス可能なディレクトリ"Jindolf"が
69- * すでに存在していればそれを返す。
70- *
71- * <li>起動元JARファイルおよび"Jindolf"が発見できなければ、
72- * MacOSX環境の場合"~/Library/Application Support/Jindolf/"を返す。
73- * Windows環境の場合"%USERPROFILE%\Jindolf\"を返す。
74- *
75- * <li>それ以外の環境(Linux,etc?)の場合"~/.jindolf/"を返す。
76- *
77- * </ul>
78- *
79- * <p>返すディレクトリが存在しているか否か、
80- * アクセス可能か否かは呼び出し元で判断せよ。
81- *
82- * @return 設定格納ディレクトリ
83- */
84- public static File getImplicitConfigDirectory(){
85- File result;
86-
87- File jarParent = FileUtils.getJarDirectory();
88- if(jarParent != null && FileUtils.isAccessibleDirectory(jarParent)){
89- result = new File(jarParent, JINCONF);
90- if(FileUtils.isAccessibleDirectory(result)){
91- return result;
92- }
93- }
94-
95- File appset = FileUtils.getAppSetDir();
96- if(appset == null) return null;
97-
98- if(FileUtils.isMacOSXFs() || FileUtils.isWindowsOSFs()){
99- result = new File(appset, JINCONF);
100- }else{
101- result = new File(appset, JINCONF_DOT);
102- }
103-
104- return result;
105- }
106-
107- /**
108- * まだ存在しない設定格納ディレクトリを新規に作成する。
109- *
110- * <p>エラーがあればダイアログ提示とともにVM終了する。
111- *
112- * @param confPath 設定格納ディレクトリ
113- * @param isImplicitPath ディレクトリが暗黙的に指定されたものならtrue。
114- * @return 新規に作成した設定格納ディレクトリ
115- * @throws IllegalArgumentException すでにそのディレクトリは存在する。
116- */
117- public static File buildConfigDirectory(File confPath,
118- boolean isImplicitPath )
119- throws IllegalArgumentException{
120- if(confPath.exists()) throw new IllegalArgumentException();
121-
122- File absPath = FileUtils.supplyFullPath(confPath);
123-
124- String preErrMessage =
125- "設定格納ディレクトリ<br>"
126- + getCenteredFileName(absPath)
127- + "の作成に失敗しました。";
128- if( ! isImplicitPath ){
129- preErrMessage =
130- "<code>"
131- + CmdOption.OPT_CONFDIR
132- + "</code>&nbsp;オプション"
133- + "で指定された、<br>"
134- + preErrMessage;
135- }
136-
137- File existsAncestor = FileUtils.findExistsAncestor(absPath);
138- if(existsAncestor == null){
139- abortNoRoot(absPath, preErrMessage);
140- }else if( ! existsAncestor.canWrite() ){
141- abortCantWriteAncestor(existsAncestor, preErrMessage);
142- }
143-
144- String prompt =
145- "設定ファイル格納ディレクトリ<br>"
146- + getCenteredFileName(absPath)
147- + "を作成します。";
148- boolean confirmed = confirmBuildConfigDir(existsAncestor, prompt);
149- if( ! confirmed ){
150- abortQuitBuildConfigDir();
151- }
152-
153- boolean success;
154- try{
155- success = absPath.mkdirs();
156- }catch(SecurityException e){
157- success = false;
158- }
159-
160- if( ! success || ! absPath.exists() ){
161- abortCantBuildConfigDir(absPath);
162- }
163-
164- FileUtils.setOwnerOnlyAccess(absPath);
165-
166- checkAccessibility(absPath);
167-
168- touchReadme(absPath);
169-
170- return absPath;
171- }
172-
173- /**
174- * ローカル画像キャッシュディレクトリを作る。
175- *
176- * <p>作られたディレクトリ内に
177- * ファイルavatarCache.jsonが作られる。
178- *
179- * @param imgCacheDir ローカル画像キャッシュディレクトリ
180- */
181- public static void buildImageCacheDir(File imgCacheDir){
182- if(imgCacheDir.exists()) return;
183-
184- String jsonRes = "resources/image/avatarCache.json";
185- InputStream is = ResourceManager.getResourceAsStream(jsonRes);
186- if(is == null) return;
187-
188- imgCacheDir.mkdirs();
189- ConfigFile.checkAccessibility(imgCacheDir);
190-
191- Path cachePath = imgCacheDir.toPath();
192- Path jsonLeaf = Paths.get("avatarCache.json");
193- Path path = cachePath.resolve(jsonLeaf);
194- try{
195- Files.copy(is, path);
196- }catch(IOException e){
197- abortCantAccessConfigDir(path.toFile());
198- }
199-
200- return;
201- }
202-
203- /**
204- * 設定ディレクトリ操作の
205- * 共通エラーメッセージ確認ダイアログを表示する。
206- *
207- * <p>閉じるまで待つ。
208- *
209- * @param seq メッセージ
210- */
211- private static void showErrorMessage(CharSequence seq){
212- JOptionPane pane =
213- new JOptionPane(seq.toString(),
214- JOptionPane.ERROR_MESSAGE);
215- showDialog(pane);
216- return;
217- }
218-
219- /**
220- * 設定ディレクトリ操作の
221- * 共通エラーメッセージ確認ダイアログを表示する。
222- *
223- * <p>閉じるまで待つ。
224- *
225- * @param seq メッセージ
226- */
227- private static void showWarnMessage(CharSequence seq){
228- JOptionPane pane =
229- new JOptionPane(seq.toString(),
230- JOptionPane.WARNING_MESSAGE);
231- showDialog(pane);
232- return;
233- }
234-
235- /**
236- * 設定ディレクトリ操作の
237- * 情報提示メッセージ確認ダイアログを表示する。
238- *
239- * <p>閉じるまで待つ。
240- *
241- * @param seq メッセージ
242- */
243- private static void showInfoMessage(CharSequence seq){
244- JOptionPane pane =
245- new JOptionPane(seq.toString(),
246- JOptionPane.INFORMATION_MESSAGE);
247- showDialog(pane);
248- return;
249- }
250-
251- /**
252- * ダイアログを表示し、閉じられるまで待つ。
253- *
254- * @param pane ダイアログの元となるペイン
255- */
256- private static void showDialog(JOptionPane pane){
257- JDialog dialog = pane.createDialog(null, TITLE_BUILDCONF);
258- dialog.setResizable(true);
259- dialog.pack();
260-
261- dialog.setVisible(true);
262- dialog.dispose();
263-
264- return;
265- }
266-
267- /**
268- * VMを異常終了させる。
269- */
270- private static void abort(){
271- System.exit(1);
272- assert false;
273- return;
274- }
275-
276- /**
277- * 設定ディレクトリのルートファイルシステムもしくはドライブレターに
278- * アクセスできないエラーをダイアログに提示し、VM終了する。
279- *
280- * @param path 設定ディレクトリ
281- * @param preMessage メッセージ前半
282- */
283- private static void abortNoRoot(File path, String preMessage){
284- File root = FileUtils.findRootFile(path);
285- showErrorMessage(
286- "<html>"
287- + preMessage + "<br>"
288- + getCenteredFileName(root)
289- + "を用意する方法が不明です。<br>"
290- + "起動を中止します。<br>"
291- + MSG_POST
292- + "</html>" );
293- abort();
294- return;
295- }
296-
297- /**
298- * 設定ディレクトリの祖先に書き込めないエラーをダイアログで提示し、
299- * VM終了する。
300- *
301- * @param existsAncestor 存在するもっとも近い祖先
302- * @param preMessage メッセージ前半
303- */
304- private static void abortCantWriteAncestor(File existsAncestor,
305- String preMessage ){
306- showErrorMessage(
307- "<html>"
308- + preMessage + "<br>"
309- + getCenteredFileName(existsAncestor)
310- + "への書き込みができないため、"
311- + "処理の続行は不可能です。<br>"
312- + "起動を中止します。<br>"
313- + MSG_POST
314- + "</html>" );
315- abort();
316- return;
317- }
318-
319- /**
320- * 設定ディレクトリを新規に生成してよいかダイアログで問い合わせる。
321- *
322- * @param existsAncestor 存在するもっとも近い祖先
323- * @param preMessage メッセージ前半
324- * @return 生成してよいと指示があればtrue
325- */
326- private static boolean confirmBuildConfigDir(File existsAncestor,
327- String preMessage){
328- String message =
329- "<html>"
330- + preMessage + "<br>"
331- + "このディレクトリを今から<br>"
332- + getCenteredFileName(existsAncestor)
333- + "に作成して構いませんか?<br>"
334- + "このディレクトリ名は、後からいつでもヘルプウィンドウで<br>"
335- + "確認することができます。"
336- + "</html>";
337-
338- JOptionPane pane =
339- new JOptionPane(message,
340- JOptionPane.QUESTION_MESSAGE,
341- JOptionPane.YES_NO_OPTION);
342-
343- showDialog(pane);
344-
345- Object result = pane.getValue();
346- if(result == null) return false;
347- else if( ! (result instanceof Integer) ) return false;
348-
349- int ival = (Integer) result;
350- if(ival == JOptionPane.YES_OPTION) return true;
351-
352- return false;
353- }
354-
355- /**
356- * 設定ディレクトリ生成をやめた操作への警告をダイアログで提示し、
357- * VM終了する。
358- */
359- private static void abortQuitBuildConfigDir(){
360- showWarnMessage(
361- "<html>"
362- + "設定ディレクトリの作成をせずに起動を中止します。<br>"
363- + MSG_POST
364- + "</html>" );
365- abort();
366- return;
367- }
368-
369- /**
370- * 設定ディレクトリが生成できないエラーをダイアログで提示し、
371- * VM終了する。
372- *
373- * @param path 生成できなかったディレクトリ
374- */
375- private static void abortCantBuildConfigDir(File path){
376- showErrorMessage(
377- "<html>"
378- + "設定ディレクトリ<br>"
379- + getCenteredFileName(path)
380- + "の作成に失敗しました。"
381- + "起動を中止します。<br>"
382- + MSG_POST
383- + "</html>" );
384- abort();
385- return;
386- }
387-
388- /**
389- * 設定ディレクトリへアクセスできないエラーをダイアログで提示し、
390- * VM終了する。
391- *
392- * @param path アクセスできないディレクトリ
393- */
394- private static void abortCantAccessConfigDir(File path){
395- showErrorMessage(
396- "<html>"
397- + "設定ディレクトリ<br>"
398- + getCenteredFileName(path)
399- + "へのアクセスができません。"
400- + "起動を中止します。<br>"
401- + "このディレクトリへのアクセス権を調整し"
402- + "読み書きできるようにしてください。<br>"
403- + MSG_POST
404- + "</html>" );
405- abort();
406- return;
407- }
408-
409- /**
410- * ファイルに書き込めないエラーをダイアログで提示し、VM終了する。
411- *
412- * @param file 書き込めなかったファイル
413- */
414- private static void abortCantWrite(File file){
415- showErrorMessage(
416- "<html>"
417- + "ファイル<br>"
418- + getCenteredFileName(file)
419- + "への書き込みができません。"
420- + "起動を中止します。<br>"
421- + "</html>" );
422- abort();
423- return;
424- }
425-
426- /**
427- * 指定されたディレクトリにREADMEファイルを生成する。
428- *
429- * <p>生成できなければダイアログ表示とともにVM終了する。
430- *
431- * @param path READMEの格納ディレクトリ
432- */
433- private static void touchReadme(File path){
434- File file = new File(path, FILE_README);
435-
436- try{
437- file.createNewFile();
438- }catch(IOException e){
439- abortCantAccessConfigDir(path);
440- }
441-
442- PrintWriter writer = null;
443- try{
444- OutputStream ostream = new FileOutputStream(file);
445- Writer owriter = new OutputStreamWriter(ostream, CHARSET_README);
446- writer = new PrintWriter(owriter);
447- writer.println(CHARSET_README.name() + " Japanese");
448- writer.println(
449- "このディレクトリは、"
450- + "Jindolfの各種設定が格納されるディレクトリです。");
451- writer.println(
452- "Jindolfの詳細は "
453- + "http://jindolf.osdn.jp/"
454- + " を参照してください。");
455- writer.println(
456- "このディレクトリを"
457- + "「" + JINCONF + "」"
458- + "の名前で起動元JARファイルと"
459- + "同じ位置に");
460- writer.println(
461- "コピーすれば、そちらの設定が優先して使われます。");
462- writer.println(
463- "「lock」の名前を持つファイルはロックファイルです。");
464- }catch(IOException | SecurityException e){
465- abortCantWrite(file);
466- }finally{
467- if(writer != null){
468- writer.close();
469- }
470- }
471-
472- return;
473- }
474-
475- /**
476- * 設定ディレクトリがアクセス可能でなければ
477- * エラーダイアログを出してVM終了する。
478- *
479- * @param confDir 設定ディレクトリ
480- */
481- public static void checkAccessibility(File confDir){
482- if( ! FileUtils.isAccessibleDirectory(confDir) ){
483- abortCantAccessConfigDir(confDir);
484- }
485-
486- return;
487- }
488-
489- /**
490- * センタリングされたファイル名表示のHTML表記を出力する。
491- *
492- * @param path ファイル
493- * @return HTML表記
494- */
495- public static String getCenteredFileName(File path){
496- return "<center>[&nbsp;"
497- + FileUtils.getHtmledFileName(path)
498- + "&nbsp;]</center>"
499- + "<br>";
500- }
501-
502- /**
503- * ロックエラーダイアログの表示。
504- *
505- * <p>呼び出しから戻ってもまだロックオブジェクトが
506- * ロックファイルのオーナーでない場合、
507- * 今後設定ディレクトリは一切使わずに起動を続行するものとする。
508- *
509- * <p>ロックファイルの強制解除に失敗した場合はVM終了する。
510- *
511- * @param lock エラーを起こしたロック
512- */
513- public static void confirmLockError(InterVMLock lock){
514- LockErrorPane pane = new LockErrorPane(lock);
515- JDialog dialog = pane.createDialog(null, TITLE_BUILDCONF);
516- dialog.setResizable(true);
517- dialog.pack();
518-
519- for(;;){
520- dialog.setVisible(true);
521- dialog.dispose();
522-
523- if(pane.isAborted() || pane.getValue() == null){
524- abort();
525- break;
526- }else if(pane.isRadioRetry()){
527- lock.tryLock();
528- if(lock.isFileOwner()) break;
529- }else if(pane.isRadioContinue()){
530- showInfoMessage(
531- "<html>"
532- + "設定ディレクトリを使わずに起動を続行します。<br>"
533- + "今回、各種設定の読み込み・保存はできません。<br>"
534- + "<code>"
535- + CmdOption.OPT_NOCONF
536- + "</code> オプション"
537- + "を使うとこの警告は出なくなります。"
538- + "</html>");
539- break;
540- }else if(pane.isRadioForce()){
541- lock.forceRemove();
542- if(lock.isExistsFile()){
543- showErrorMessage(
544- "<html>"
545- + "ロックファイルの強制解除に失敗しました。<br>"
546- + "他に動いているJindolf"
547- + "が見つからないのであれば、<br>"
548- + "なんとかしてロックファイル<br>"
549- + getCenteredFileName(lock.getLockFile())
550- + "を削除してください。<br>"
551- + "起動を中止します。"
552- + "</html>");
553- abort();
554- break;
555- }
556- lock.tryLock();
557- if(lock.isFileOwner()) break;
558- showErrorMessage(
559- "<html>"
560- + "ロックファイル<br>"
561- + getCenteredFileName(lock.getLockFile())
562- + "を確保することができません。<br>"
563- + "起動を中止します。"
564- + "</html>");
565- abort();
566- break;
567- }
568- }
569-
570- return;
571- }
572-
573-}
--- a/src/main/java/jp/sfjp/jindolf/config/ConfigStore.java
+++ b/src/main/java/jp/sfjp/jindolf/config/ConfigStore.java
@@ -7,457 +7,265 @@
77
88 package jp.sfjp.jindolf.config;
99
10-import java.io.BufferedInputStream;
11-import java.io.BufferedOutputStream;
12-import java.io.BufferedReader;
13-import java.io.BufferedWriter;
14-import java.io.File;
15-import java.io.FileInputStream;
16-import java.io.FileNotFoundException;
17-import java.io.FileOutputStream;
18-import java.io.IOException;
19-import java.io.InputStream;
20-import java.io.InputStreamReader;
21-import java.io.OutputStream;
22-import java.io.OutputStreamWriter;
23-import java.io.Reader;
24-import java.io.Writer;
25-import java.nio.charset.Charset;
26-import java.nio.charset.StandardCharsets;
2710 import java.nio.file.Path;
2811 import java.nio.file.Paths;
29-import java.util.logging.Level;
30-import java.util.logging.Logger;
31-import jp.sourceforge.jovsonz.JsComposition;
32-import jp.sourceforge.jovsonz.JsObject;
33-import jp.sourceforge.jovsonz.JsParseException;
34-import jp.sourceforge.jovsonz.JsTypes;
35-import jp.sourceforge.jovsonz.JsVisitException;
36-import jp.sourceforge.jovsonz.Json;
3712
3813 /**
39- * 各種設定の永続化関連。
14+ * Jindolf設定ディレクトリの管理を行う。
15+ *
16+ * <p>デフォルトの設定ディレクトリや
17+ * アプリのコマンドライン引数から構成された、
18+ * 設定ディレクトリに関する情報が保持管理される。
19+ *
20+ * <p>基本的に1アプリのみが設定ディレクトリへの入出力を許される。
21+ *
22+ * <p>インスタンス生成後に
23+ * 設定ディレクトリを使わない設定に変更することが可能。
24+ * (※ロック確保失敗後の続行等を想定)
25+ *
26+ * <p>設定ディレクトリには
27+ *
28+ * <ul>
29+ * <li>ロックファイル
30+ * <li>JSON設定ファイル
31+ * <li>Avatar代替イメージ格納ディレクトリ
32+ * </ul>
33+ *
34+ * <p>などが配置される。
35+ *
36+ * <p>コンストラクタに与えられるディレクトリは
37+ * 絶対パスでなければならない。
38+ *
39+ * <p>ロックファイル取得の失敗、
40+ * およびその後のユーザインタラクションによっては、
41+ * 設定ディレクトリを使わない設定に上書きされた後
42+ * 起動処理が続行される場合がありうる。
4043 */
4144 public class ConfigStore {
4245
43- /** 検索履歴ファイル。 */
44- public static final File HIST_FILE = new File("searchHistory.json");
45- /** ネットワーク設定ファイル。 */
46- public static final File NETCONFIG_FILE = new File("netconfig.json");
47- /** 台詞表示設定ファイル。 */
48- public static final File TALKCONFIG_FILE = new File("talkconfig.json");
49- /** ローカル画像格納ディレクトリ。 */
50- public static final Path LOCALIMG_DIR = Paths.get("img");
51- /** ローカル画像設定ファイル。 */
52- public static final Path LOCALIMGCONFIG_PATH =
53- Paths.get("avatarCache.json");
46+ private static final Path JINCONF = Paths.get("Jindolf");
47+ private static final Path JINCONF_DOT = Paths.get(".jindolf");
48+ private static final Path LOCKFILE = Paths.get("lock");
49+ private static final Path LOCALIMG_DIR = Paths.get("img");
5450
55- private static final String LOCKFILE = "lock";
56-
57- private static final Charset CHARSET_JSON = StandardCharsets.UTF_8;
58-
59- private static final Logger LOGGER = Logger.getAnonymousLogger();
51+ private static final Path MAC_LIB = Paths.get("Library");
52+ private static final Path MAC_APPSUPP = Paths.get("Application Support");
6053
6154
6255 private boolean useStoreFile;
63- private boolean isImplicitPath;
64- private File configDir;
56+ private Path configDir;
6557
6658
6759 /**
6860 * コンストラクタ。
6961 *
70- * @param useStoreFile 設定ディレクトリ内への
71- * セーブデータ機能を使うならtrue
72- * @param isImplicitPath 起動コマンドラインから指定された
73- * 設定ディレクトリの場合false
74- * @param configDirPath 設定ディレクトリ。
75- * 設定ディレクトリを使わない場合は無視される。
62+ * <p>このインスタンスでは、
63+ * 設定ディレクトリへの入出力を行わない。
7664 */
77- public ConfigStore(boolean useStoreFile,
78- boolean isImplicitPath,
79- File configDirPath ){
80- super();
81-
82- this.useStoreFile = useStoreFile;
83-
84- if(this.useStoreFile){
85- this.isImplicitPath = isImplicitPath;
86- this.configDir = configDirPath;
87- }else{
88- this.isImplicitPath = true;
89- this.configDir = null;
90- }
91-
65+ public ConfigStore(){
66+ this(false, null);
9267 return;
9368 }
9469
95-
9670 /**
97- * 設定ディレクトリを使うか否か判定する。
71+ * コンストラクタ。
9872 *
99- * @return 設定ディレクトリを使うならtrue。
100- */
101- public boolean useStoreFile(){
102- return this.useStoreFile;
103- }
104-
105- /**
106- * 設定ディレクトリを返す。
73+ * <p>このインスタンスでは、
74+ * 設定ディレクトリへの入出力を行うが、
75+ * デフォルトではない明示されたディレクトリが用いられる。
10776 *
108- * @return 設定ディレクトリ。設定ディレクトリを使わない場合はnull
77+ * @param configDirPath 設定ディレクトリの絶対パス。
78+ * nullの場合はデフォルトの設定ディレクトリが用いられる。
79+ * @throws IllegalArgumentException 絶対パスではない。
10980 */
110- public File getConfigDir(){
111- return this.configDir;
81+ public ConfigStore(Path configDirPath) throws IllegalArgumentException{
82+ this(true, configDirPath);
83+ return;
11284 }
11385
11486 /**
115- * ローカル画像格納ディレクトリを返す。
87+ * コンストラクタ。
11688 *
117- * @return 格納ディレクトリ。格納ディレクトリを使わない場合はnull
89+ * @param useStoreFile 設定ディレクトリ内への
90+ * 入出力機能を使うならtrue
91+ * @param configDirPath 設定ディレクトリの絶対パス。
92+ * 設定ディレクトリを使わない場合は無視される。
93+ * この時点でのディレクトリ存在の有無は関知しない。
94+ * 既存ディレクトリの各種属性チェックは後にチェックするものとする。
95+ * nullの場合デフォルトの設定ディレクトリが用いられる。
96+ * @throws IllegalArgumentException 絶対パスではない。
11897 */
119- public Path getLocalImgDir(){
120- if(this.configDir == null) return null;
121-
122- Path configPath = this.configDir.toPath();
123- Path result = configPath.resolve(LOCALIMG_DIR);
98+ protected ConfigStore(boolean useStoreFile,
99+ Path configDirPath )
100+ throws IllegalArgumentException{
101+ super();
124102
125- return result;
126- }
103+ this.useStoreFile = useStoreFile;
127104
128- /**
129- * 設定ディレクトリの存在を確認し、なければ作る。
130- *
131- * <p>設定ディレクトリを使わない場合は何もしない。
132- */
133- public void prepareConfigDir(){
134- if( ! this.useStoreFile ) return;
135-
136- if( ! this.configDir.exists() ){
137- File created =
138- ConfigFile.buildConfigDirectory(this.configDir,
139- this.isImplicitPath );
140- ConfigFile.checkAccessibility(created);
105+ if(this.useStoreFile){
106+ if(configDirPath != null){
107+ if( ! configDirPath.isAbsolute()){
108+ throw new IllegalArgumentException();
109+ }
110+ this.configDir = configDirPath;
111+ }else{
112+ this.configDir = getDefaultConfDirPath();
113+ }
141114 }else{
142- ConfigFile.checkAccessibility(this.configDir);
115+ this.configDir = null;
143116 }
144117
145- File imgDir = new File(this.configDir, "img");
146- if( ! imgDir.exists()){
147- ConfigFile.buildImageCacheDir(imgDir);
148- }
118+ assert ( this.useStoreFile && this.configDir != null)
119+ || ( ( ! this.useStoreFile) && this.configDir == null);
149120
150121 return;
151122 }
152123
124+
153125 /**
154- * ロックファイルの取得を試みる。
126+ * 暗黙的な設定格納ディレクトリを絶対パスで返す。
127+ *
128+ * <ul>
129+ *
130+ * <li>起動元JARファイルと同じディレクトリに、
131+ * アクセス可能なディレクトリ"Jindolf"が
132+ * すでに存在していればそれを返す。
133+ *
134+ * <li>起動元JARファイルおよび"Jindolf"が発見できなければ、
135+ * MacOSX環境の場合"~/Library/Application Support/Jindolf/"を返す。
136+ * Windows環境の場合"%USERPROFILE%\Jindolf\"を返す。
137+ *
138+ * <li>それ以外の環境(Linux,etc?)の場合"~/.jindolf/"を返す。
139+ *
140+ * </ul>
155141 *
156- * <p>ロックに失敗したが処理を続行する場合、
157- * 設定ディレクトリは使わないものとして続行する。
142+ * <p>返すディレクトリが存在しているか否か、
143+ * アクセス可能か否かは呼び出し元で判断せよ。
144+ *
145+ * @return 設定格納ディレクトリの絶対パス
158146 */
159- public void tryLock(){
160- if( ! this.useStoreFile ) return;
161-
162- File lockFile = new File(this.configDir, LOCKFILE);
163- InterVMLock lock = new InterVMLock(lockFile);
164-
165- lock.tryLock();
166-
167- if( ! lock.isFileOwner() ){
168- ConfigFile.confirmLockError(lock);
169- if( ! lock.isFileOwner() ){
170- this.useStoreFile = false;
171- this.isImplicitPath = true;
172- this.configDir = null;
147+ public static Path getDefaultConfDirPath(){
148+ Path jarParent = FileUtils.getJarDirectory();
149+ if(FileUtils.isAccessibleDirectory(jarParent)){
150+ Path confPath = jarParent.resolve(JINCONF);
151+ if(FileUtils.isAccessibleDirectory(confPath)){
152+ assert confPath.isAbsolute();
153+ return confPath;
173154 }
174155 }
175156
176- return;
177- }
157+ Path appset = getAppSetDir();
178158
179- /**
180- * 設定ディレクトリ上のOBJECT型JSONファイルを読み込む。
181- *
182- * @param file JSONファイルの相対パス。
183- * @return JSON object。
184- * 設定ディレクトリを使わない設定、
185- * もしくはJSONファイルが存在しない、
186- * もしくはOBJECT型でなかった、
187- * もしくは入力エラーがあればnull
188- */
189- public JsObject loadJsObject(File file){
190- JsComposition<?> root = loadJson(file);
191- if(root == null || root.getJsTypes() != JsTypes.OBJECT) return null;
192- JsObject result = (JsObject) root;
193- return result;
194- }
195-
196- /**
197- * 設定ディレクトリ上のJSONファイルを読み込む。
198- *
199- * @param file JSONファイルの相対パス
200- * @return JSON objectまたはarray。
201- * 設定ディレクトリを使わない設定、
202- * もしくはJSONファイルが存在しない、
203- * もしくは入力エラーがあればnull
204- */
205- public JsComposition<?> loadJson(File file){
206- if( ! this.useStoreFile ) return null;
207-
208- File absFile;
209- if(file.isAbsolute()){
210- absFile = file;
159+ Path leaf;
160+ if(FileUtils.isMacOSXFs() || FileUtils.isWindowsOSFs()){
161+ leaf = JINCONF;
211162 }else{
212- if(this.configDir == null) return null;
213- absFile = new File(this.configDir, file.getPath());
214- if( ! absFile.exists() ) return null;
215- if( ! absFile.isAbsolute() ) return null;
216- }
217- String absPath = absFile.getPath();
218-
219- InputStream istream;
220- try{
221- istream = new FileInputStream(absFile);
222- }catch(FileNotFoundException e){
223- assert false;
224- return null;
225- }
226- istream = new BufferedInputStream(istream);
227-
228- JsComposition<?> root;
229- try{
230- root = loadJson(istream);
231- }catch(IOException e){
232- LOGGER.log(Level.SEVERE,
233- "JSONファイル["
234- + absPath
235- + "]の読み込み時に支障がありました。", e);
236- return null;
237- }catch(JsParseException e){
238- LOGGER.log(Level.SEVERE,
239- "JSONファイル["
240- + absPath
241- + "]の内容に不備があります。", e);
242- return null;
243- }finally{
244- try{
245- istream.close();
246- }catch(IOException e){
247- LOGGER.log(Level.SEVERE,
248- "JSONファイル["
249- + absPath
250- + "]を閉じることができません。", e);
251- return null;
252- }
163+ leaf = JINCONF_DOT;
253164 }
254165
255- return root;
166+ Path result = appset.resolve(leaf);
167+ assert result.isAbsolute();
168+
169+ return result;
256170 }
257171
258172 /**
259- * バイトストリーム上のJSONデータを読み込む。
173+ * アプリケーション設定ディレクトリを絶対パスで返す。
260174 *
261- * <p>バイトストリームはUTF-8と解釈される。
175+ * <p>存在の有無、アクセスの可否は関知しない。
262176 *
263- * @param is バイトストリーム
264- * @return JSON objectまたはarray。
265- * @throws IOException 入力エラー
266- * @throws JsParseException 構文エラー
267- */
268- protected JsComposition<?> loadJson(InputStream is)
269- throws IOException, JsParseException {
270- Reader reader = new InputStreamReader(is, CHARSET_JSON);
271- reader = new BufferedReader(reader);
272- JsComposition<?> root = loadJson(reader);
273- return root;
274- }
275-
276- /**
277- * 文字ストリーム上のJSONデータを読み込む。
177+ * <p>WindowsやLinuxではホームディレクトリ。
178+ * Mac OS X ではさらにホームディレクトリの下の
179+ * "Library/Application Support/"
278180 *
279- * @param reader 文字ストリーム
280- * @return JSON objectまたはarray。
281- * @throws IOException 入力エラー
282- * @throws JsParseException 構文エラー
181+ * @return アプリケーション設定ディレクトリの絶対パス
283182 */
284- protected JsComposition<?> loadJson(Reader reader)
285- throws IOException, JsParseException {
286- JsComposition<?> root = Json.parseJson(reader);
287- return root;
288- }
183+ private static Path getAppSetDir(){
184+ Path home = getHomeDirectory();
289185
290- /**
291- * 設定ディレクトリ上のJSONファイルに書き込む。
292- *
293- * @param file JSONファイルの相対パス
294- * @param root JSON objectまたはarray
295- * @return 正しくセーブが行われればtrue。
296- * 何らかの理由でセーブが完了できなければfalse
297- */
298- public boolean saveJson(File file, JsComposition<?> root){
299- if( ! this.useStoreFile ) return false;
300-
301- // TODO テンポラリファイルを用いたより安全なファイル更新
302- File absFile = new File(this.configDir, file.getPath());
303- String absPath = absFile.getPath();
304-
305- absFile.delete();
306- try{
307- if(absFile.createNewFile() != true) return false;
308- }catch(IOException e){
309- LOGGER.log(Level.SEVERE,
310- "JSONファイル["
311- + absPath
312- + "]の新規生成ができません。", e);
313- return false;
314- }
186+ Path result = home;
315187
316- OutputStream ostream;
317- try{
318- ostream = new FileOutputStream(absFile);
319- }catch(FileNotFoundException e){
320- assert false;
321- return false;
322- }
323- ostream = new BufferedOutputStream(ostream);
324-
325- try{
326- saveJson(ostream, root);
327- }catch(JsVisitException e){
328- LOGGER.log(Level.SEVERE,
329- "JSONファイル["
330- + absPath
331- + "]の出力処理で支障がありました。", e);
332- return false;
333- }catch(IOException e){
334- LOGGER.log(Level.SEVERE,
335- "JSONファイル["
336- + absPath
337- + "]の書き込み時に支障がありました。", e);
338- return false;
339- }finally{
340- try{
341- ostream.close();
342- }catch(IOException e){
343- LOGGER.log(Level.SEVERE,
344- "JSONファイル["
345- + absPath
346- + "]を閉じることができません。", e);
347- return false;
348- }
188+ if(FileUtils.isMacOSXFs()){
189+ result = result.resolve(MAC_LIB);
190+ result = result.resolve(MAC_APPSUPP);
349191 }
350192
351- return true;
193+ return result;
352194 }
353195
354196 /**
355- * バイトストリームにJSONデータを書き込む。
197+ * ホームディレクトリを得る。
356198 *
357- * <p>バイトストリームはUTF-8と解釈される。
199+ * <p>システムプロパティuser.homeで示されたホームディレクトリを
200+ * 絶対パスで返す。
358201 *
359- * @param os バイトストリーム出力
360- * @param root JSON objectまたはarray
361- * @throws IOException 出力エラー
362- * @throws JsVisitException 構造エラー
202+ * @return ホームディレクトリの絶対パス。
363203 */
364- protected void saveJson(OutputStream os, JsComposition<?> root)
365- throws IOException, JsVisitException {
366- Writer writer = new OutputStreamWriter(os, CHARSET_JSON);
367- writer = new BufferedWriter(writer);
368- saveJson(writer, root);
369- return;
204+ private static Path getHomeDirectory(){
205+ String homeProp = System.getProperty("user.home");
206+ Path result = Paths.get(homeProp);
207+ result = result.toAbsolutePath();
208+ return result;
370209 }
371210
372- /**
373- * 文字ストリームにJSONデータを書き込む。
374- *
375- * @param writer 文字ストリーム出力
376- * @param root JSON objectまたはarray
377- * @throws IOException 出力エラー
378- * @throws JsVisitException 構造エラー
379- */
380- protected void saveJson(Writer writer, JsComposition<?> root)
381- throws IOException, JsVisitException {
382- Json.dumpJson(writer, root);
383- return;
384- }
385211
386212 /**
387- * 検索履歴ファイルを読み込む。
213+ * 設定ディレクトリを使うか否か判定する。
388214 *
389- * @return 履歴データ。履歴を読まないもしくは読めない場合はnull
215+ * @return 設定ディレクトリを使うならtrue。
390216 */
391- public JsObject loadHistoryConfig(){
392- JsObject result = loadJsObject(HIST_FILE);
393- return result;
217+ public boolean useStoreFile(){
218+ return this.useStoreFile;
394219 }
395220
396221 /**
397- * ネットワーク設定ファイルを読み込む。
222+ * 設定ディレクトリを絶対パスで返す。
398223 *
399- * @return ネットワーク設定データ。
400- * 設定を読まないもしくは読めない場合はnull
224+ * @return 設定ディレクトリの絶対パス。
225+ * 設定ディレクトリを使わない場合はnull
401226 */
402- public JsObject loadNetConfig(){
403- JsObject result = loadJsObject(NETCONFIG_FILE);
404- return result;
227+ public Path getConfigDir(){
228+ return this.configDir;
405229 }
406230
407231 /**
408- * 台詞表示設定ファイルを読み込む。
409- *
410- * @return 台詞表示設定データ。
411- * 設定を読まないもしくは読めない場合はnull
232+ * 設定ディレクトリを使わない設定に変更する。
412233 */
413- public JsObject loadTalkConfig(){
414- JsObject result = loadJsObject(TALKCONFIG_FILE);
415- return result;
234+ public void setNoConf(){
235+ this.useStoreFile = false;
236+ this.configDir = null;
237+ return;
416238 }
417239
418240 /**
419- * ローカル画像設定ファイルを読み込む。
241+ * ローカル画像格納ディレクトリを絶対パスで返す。
420242 *
421- * @return ローカル画像設定データ。
422- * 設定を読まないもしくは読めない場合はnull
243+ * @return 格納ディレクトリの絶対パス。
244+ * 格納ディレクトリを使わない場合はnull
423245 */
424- public JsObject loadLocalImgConfig(){
425- Path path = LOCALIMG_DIR.resolve(LOCALIMGCONFIG_PATH);
426- JsObject result = loadJsObject(path.toFile());
427- return result;
428- }
246+ public Path getLocalImgDir(){
247+ if( ! this.useStoreFile ) return null;
248+ if(this.configDir == null) return null;
429249
430- /**
431- * 検索履歴ファイルに書き込む。
432- *
433- * @param root 履歴データ
434- * @return 書き込まなかったもしくは書き込めなかった場合はfalse
435- */
436- public boolean saveHistoryConfig(JsComposition<?> root){
437- boolean result = saveJson(HIST_FILE, root);
438- return result;
439- }
250+ Path result = this.configDir.resolve(LOCALIMG_DIR);
251+ assert result.isAbsolute();
440252
441- /**
442- * ネットワーク設定ファイルに書き込む。
443- *
444- * @param root ネットワーク設定
445- * @return 書き込まなかったもしくは書き込めなかった場合はfalse
446- */
447- public boolean saveNetConfig(JsComposition<?> root){
448- boolean result = saveJson(NETCONFIG_FILE, root);
449253 return result;
450254 }
451255
452256 /**
453- * 台詞表示設定ファイルに書き込む。
257+ * ロックファイルを絶対パスで返す。
454258 *
455- * @param root 台詞表示設定
456- * @return 書き込まなかったもしくは書き込めなかった場合はfalse
259+ * @return ロックファイルの絶対パス。
260+ * 格納ディレクトリを使わない場合はnull
457261 */
458- public boolean saveTalkConfig(JsComposition<?> root){
459- boolean result = saveJson(TALKCONFIG_FILE, root);
460- return result;
262+ public Path getLockFile(){
263+ if( ! this.useStoreFile ) return null;
264+ if(this.configDir == null) return null;
265+
266+ Path lockFile = this.configDir.resolve(LOCKFILE);
267+
268+ return lockFile;
461269 }
462270
463271 }
--- a/src/main/java/jp/sfjp/jindolf/config/EnvInfo.java
+++ b/src/main/java/jp/sfjp/jindolf/config/EnvInfo.java
@@ -8,8 +8,10 @@
88 package jp.sfjp.jindolf.config;
99
1010 import java.io.File;
11-import java.text.NumberFormat;
12-import java.util.Set;
11+import java.text.MessageFormat;
12+import java.util.Arrays;
13+import java.util.Collections;
14+import java.util.List;
1315 import java.util.SortedMap;
1416 import java.util.TreeMap;
1517
@@ -32,36 +34,48 @@ public final class EnvInfo{
3234 /** 最大ヒープメモリ。 */
3335 public static final long MAX_MEMORY;
3436
35- private static final SortedMap<String, String> PROPERTY_MAP =
36- new TreeMap<>();
37-
38- private static final SortedMap<String, String> ENVIRONMENT_MAP =
39- new TreeMap<>();
40-
41- private static final String[] CLASSPATHS;
37+ private static final SortedMap<String, String> PROPERTY_MAP;
38+ private static final SortedMap<String, String> ENVIRONMENT_MAP;
39+
40+ private static final List<String> CLASSPATHS;
41+
42+ private static final String[] PROPNAMES = {
43+ "os.name",
44+ "os.version",
45+ "os.arch",
46+ "java.vendor",
47+ "java.version",
48+ "java.class.path",
49+ };
50+
51+ private static final String[] ENVNAMES = {
52+ "LANG",
53+ "DISPLAY",
54+ //"PATH",
55+ //"TEMP",
56+ //"USER",
57+ };
58+
59+ private static final String FORM_MEM =
60+ "最大ヒープメモリ量: {0,number} bytes\n";
61+ private static final String INDENT = "\u0020\u0020";
62+ private static final char NL = '\n';
4263
4364 static{
44- OS_NAME = getSecureProperty("os.name");
45- OS_VERSION = getSecureProperty("os.version");
46- OS_ARCH = getSecureProperty("os.arch");
47- JAVA_VENDOR = getSecureProperty("java.vendor");
48- JAVA_VERSION = getSecureProperty("java.version");
49-
50- getSecureEnvironment("LANG");
51- getSecureEnvironment("DISPLAY");
52-
5365 Runtime runtime = Runtime.getRuntime();
5466 MAX_MEMORY = runtime.maxMemory();
5567
56- String classpath = getSecureProperty("java.class.path");
57- String[] pathVec;
58- if(classpath != null){
59- pathVec = classpath.split(File.pathSeparator);
60- }else{
61- pathVec = new String[0];
62- }
63- CLASSPATHS = pathVec;
68+ ENVIRONMENT_MAP = buildEnvMap();
69+
70+ PROPERTY_MAP = buildPropMap();
71+ OS_NAME = PROPERTY_MAP.get("os.name");
72+ OS_VERSION = PROPERTY_MAP.get("os.version");
73+ OS_ARCH = PROPERTY_MAP.get("os.arch");
74+ JAVA_VENDOR = PROPERTY_MAP.get("java.vendor");
75+ JAVA_VERSION = PROPERTY_MAP.get("java.version");
6476
77+ String classpath = PROPERTY_MAP.get("java.class.path");
78+ CLASSPATHS = buildClassPathList(classpath);
6579 }
6680
6781
@@ -69,39 +83,78 @@ public final class EnvInfo{
6983 * 隠れコンストラクタ。
7084 */
7185 private EnvInfo(){
72- throw new AssertionError();
86+ assert false;
7387 }
7488
7589
7690 /**
77- * 可能ならシステムプロパティを読み込む。
78- * @param key キー
79- * @return プロパティ値。セキュリティ上読み込み禁止の場合はnull。
91+ * 主要環境変数マップを作成する。
92+ *
93+ * @return 主要環境変数マップ
8094 */
81- private static String getSecureProperty(String key){
82- String result;
83- try{
84- result = System.getProperty(key);
85- if(result != null) PROPERTY_MAP.put(key, result);
86- }catch(SecurityException e){
87- result = null;
95+ private static SortedMap<String, String> buildEnvMap(){
96+ SortedMap<String, String> envmap = new TreeMap<>();
97+
98+ for(String name : ENVNAMES){
99+ String val;
100+ try{
101+ val = System.getenv(name);
102+ }catch(SecurityException e){
103+ continue;
104+ }
105+ if(val == null) continue;
106+ envmap.put(name, val);
88107 }
108+
109+ SortedMap<String, String> result;
110+ result = Collections.unmodifiableSortedMap(envmap);
111+
89112 return result;
90113 }
91114
92115 /**
93- * 可能なら環境変数を読み込む。
94- * @param name 環境変数名
95- * @return 環境変数値。セキュリティ上読み込み禁止の場合はnull。
116+ * 主要システムプロパティ値マップを作成する。
117+ *
118+ * @return 主要システムプロパティ値マップ
96119 */
97- private static String getSecureEnvironment(String name){
98- String result;
99- try{
100- result = System.getenv(name);
101- if(result != null) ENVIRONMENT_MAP.put(name, result);
102- }catch(SecurityException e){
103- result = null;
120+ private static SortedMap<String, String> buildPropMap(){
121+ SortedMap<String, String> propmap = new TreeMap<>();
122+
123+ for(String name : PROPNAMES){
124+ String val;
125+ try{
126+ val = System.getProperty(name);
127+ }catch(SecurityException e){
128+ continue;
129+ }
130+ if(val == null) continue;
131+ propmap.put(name, val);
104132 }
133+
134+ SortedMap<String, String> result;
135+ result = Collections.unmodifiableSortedMap(propmap);
136+
137+ return result;
138+ }
139+
140+ /**
141+ * クラスパスリストを作成する。
142+ *
143+ * @param classpath 連結クラスパス値
144+ * @return クラスパスリスト
145+ */
146+ private static List<String> buildClassPathList(String classpath){
147+ String[] pathArray;
148+ if(classpath != null){
149+ pathArray = classpath.split(File.pathSeparator);
150+ }else{
151+ pathArray = new String[0];
152+ }
153+
154+ List<String> result;
155+ result = Arrays.asList(pathArray);
156+ result = Collections.unmodifiableList(result);
157+
105158 return result;
106159 }
107160
@@ -111,44 +164,74 @@ public final class EnvInfo{
111164 */
112165 public static String getVMInfo(){
113166 StringBuilder result = new StringBuilder();
114- NumberFormat nform = NumberFormat.getNumberInstance();
115167
116- result.append("最大ヒープメモリ量: ")
117- .append(nform.format(MAX_MEMORY))
118- .append(" bytes\n");
168+ String memform = MessageFormat.format(FORM_MEM, MAX_MEMORY);
169+ result.append(memform).append(NL);
170+
171+ result.append(getSysPropInfo()).append(NL);
172+ result.append(getEnvInfo()).append(NL);
173+ result.append(getClassPathInfo()).append(NL);
119174
120- result.append("\n");
175+ return result.toString();
176+ }
121177
178+ /**
179+ * システムプロパティ要覧を返す。
180+ *
181+ * <p>java.class.pathの値は除く。
182+ *
183+ * @return システムプロパティ要覧
184+ */
185+ private static CharSequence getSysPropInfo(){
186+ StringBuilder result = new StringBuilder();
122187 result.append("主要システムプロパティ:\n");
123- Set<String> propKeys = PROPERTY_MAP.keySet();
124- for(String propKey : propKeys){
125- if(propKey.equals("java.class.path")) continue;
126- String value = PROPERTY_MAP.get(propKey);
127- result.append(" ");
128- result.append(propKey).append("=").append(value).append("\n");
129- }
130188
131- result.append("\n");
189+ PROPERTY_MAP.entrySet().stream()
190+ .filter(entry -> ! entry.getKey().equals("java.class.path"))
191+ .forEachOrdered(entry -> {
192+ result.append(INDENT);
193+ result.append(entry.getKey());
194+ result.append('=');
195+ result.append(entry.getValue());
196+ result.append(NL);
197+ });
132198
133- result.append("主要環境変数:\n");
134- Set<String> envKeys = ENVIRONMENT_MAP.keySet();
135- for(String envKey : envKeys){
136- String value = ENVIRONMENT_MAP.get(envKey);
137- result.append(" ");
138- result.append(envKey).append("=").append(value).append("\n");
139- }
199+ return result;
200+ }
140201
141- result.append("\n");
202+ /**
203+ * 環境変数要覧を返す。
204+ *
205+ * @return 環境変数要覧
206+ */
207+ private static CharSequence getEnvInfo(){
208+ StringBuilder result = new StringBuilder("主要環境変数:\n");
209+
210+ ENVIRONMENT_MAP.entrySet().stream()
211+ .forEachOrdered(entry -> {
212+ result.append(INDENT);
213+ result.append(entry.getKey());
214+ result.append('=');
215+ result.append(entry.getValue());
216+ result.append(NL);
217+ });
142218
143- result.append("クラスパス:\n");
144- for(String path : CLASSPATHS){
145- result.append(" ");
146- result.append(path).append("\n");
147- }
219+ return result;
220+ }
148221
149- result.append("\n");
222+ /**
223+ * クラスパス情報要覧を返す。
224+ *
225+ * @return クラスパス情報要覧
226+ */
227+ private static CharSequence getClassPathInfo(){
228+ StringBuilder result = new StringBuilder("クラスパス:\n");
150229
151- return result.toString();
230+ CLASSPATHS.stream().forEachOrdered(path -> {
231+ result.append(INDENT).append(path).append(NL);
232+ });
233+
234+ return result;
152235 }
153236
154237 }
--- a/src/main/java/jp/sfjp/jindolf/config/FileUtils.java
+++ b/src/main/java/jp/sfjp/jindolf/config/FileUtils.java
@@ -11,6 +11,9 @@ import java.io.File;
1111 import java.net.URI;
1212 import java.net.URISyntaxException;
1313 import java.net.URL;
14+import java.nio.file.Files;
15+import java.nio.file.Path;
16+import java.nio.file.Paths;
1417 import java.security.CodeSource;
1518 import java.security.ProtectionDomain;
1619 import java.util.Locale;
@@ -44,79 +47,6 @@ public final class FileUtils{
4447
4548
4649 /**
47- * なるべく自分にだけ読み書き許可を与え
48- * 自分以外には読み書き許可を与えないように
49- * ファイル属性を操作する。
50- *
51- * @param file 操作対象ファイル
52- * @return 成功すればtrue
53- * @throws SecurityException セキュリティ上の許可が無い場合
54- */
55- public static boolean setOwnerOnlyAccess(File file)
56- throws SecurityException{
57- boolean result = true;
58-
59- result &= file.setReadable(false, false);
60- result &= file.setReadable(true, true);
61-
62- result &= file.setWritable(false, false);
63- result &= file.setWritable(true, true);
64-
65- return result;
66- }
67-
68- /**
69- * 任意の絶対パスの祖先の内、存在するもっとも近い祖先を返す。
70- *
71- * @param file 任意の絶対パス
72- * @return 存在するもっとも近い祖先。一つも存在しなければnull。
73- * @throws IllegalArgumentException 引数が絶対パスでない
74- */
75- public static File findExistsAncestor(File file)
76- throws IllegalArgumentException{
77- if(file == null) return null;
78- if( ! file.isAbsolute() ) throw new IllegalArgumentException();
79- if(file.exists()) return file;
80- File parent = file.getParentFile();
81- return findExistsAncestor(parent);
82- }
83-
84- /**
85- * 任意の絶対パスのルートファイルシステムもしくはドライブレターを返す。
86- *
87- * @param file 任意の絶対パス
88- * @return ルートファイルシステムもしくはドライブレター
89- * @throws IllegalArgumentException 引数が絶対パスでない
90- */
91- public static File findRootFile(File file)
92- throws IllegalArgumentException{
93- if( ! file.isAbsolute() ) throw new IllegalArgumentException();
94- File parent = file.getParentFile();
95- if(parent == null) return file;
96- return findRootFile(parent);
97- }
98-
99- /**
100- * 相対パスの絶対パス化を試みる。
101- *
102- * @param file 対象パス
103- * @return 絶対パス。絶対化に失敗した場合は元の引数。
104- */
105- public static File supplyFullPath(File file){
106- if(file.isAbsolute()) return file;
107-
108- File absFile;
109-
110- try{
111- absFile = file.getAbsoluteFile();
112- }catch(SecurityException e){
113- return file;
114- }
115-
116- return absFile;
117- }
118-
119- /**
12050 * 任意のディレクトリがアクセス可能な状態にあるか判定する。
12151 *
12252 * <p>アクセス可能の条件を満たすためには、与えられたパスが
@@ -131,39 +61,49 @@ public final class FileUtils{
13161 * @param path 任意のディレクトリ
13262 * @return アクセス可能ならtrue
13363 */
134- public static boolean isAccessibleDirectory(File path){
64+ public static boolean isAccessibleDirectory(Path path){
13565 if(path == null) return false;
13666
137- boolean result = true;
138-
139- if ( ! path.exists() ) result = false;
140- else if( ! path.isDirectory() ) result = false;
141- else if( ! path.canRead() ) result = false;
142- else if( ! path.canWrite() ) result = false;
67+ boolean result =
68+ Files.exists(path)
69+ && Files.isDirectory(path)
70+ && Files.isReadable(path)
71+ && Files.isWritable(path);
14372
14473 return result;
14574 }
14675
14776 /**
148- * クラスがローカルファイルからロードされたのであれば
149- * そのファイルを返す。
77+ * クラスのロード元のURLを返す。
15078 *
151- * @param klass 任意のクラス
152- * @return ロード元ファイル。見つからなければnull。
79+ * @param klass クラス
80+ * @return ロード元URL。不明ならnull
15381 */
154- public static File getClassSourceFile(Class<?> klass){
155- ProtectionDomain domain;
156- try{
157- domain = klass.getProtectionDomain();
158- }catch(SecurityException e){
159- return null;
160- }
82+ public static URL getClassSourceUrl(Class<?> klass){
83+ ProtectionDomain domain = klass.getProtectionDomain();
84+ if(domain == null) return null;
16185
16286 CodeSource src = domain.getCodeSource();
87+ if(src == null) return null;
16388
16489 URL location = src.getLocation();
90+
91+ return location;
92+ }
93+
94+ /**
95+ * クラスがローカルファイルからロードされたのであれば
96+ * その絶対パスを返す。
97+ *
98+ * @param klass 任意のクラス
99+ * @return ロード元ファイルの絶対パス。見つからなければnull。
100+ */
101+ public static Path getClassSourcePath(Class<?> klass){
102+ URL location = getClassSourceUrl(klass);
103+ if(location == null) return null;
104+
165105 String scheme = location.getProtocol();
166- if( ! scheme.equals(SCHEME_FILE) ) return null;
106+ if( ! SCHEME_FILE.equalsIgnoreCase(scheme) ) return null;
167107
168108 URI uri;
169109 try{
@@ -173,81 +113,68 @@ public final class FileUtils{
173113 return null;
174114 }
175115
176- File file = new File(uri);
116+ Path result = Paths.get(uri);
117+ result = result.toAbsolutePath();
177118
178- return file;
119+ return result;
179120 }
180121
181122 /**
182123 * すでに存在するJARファイルか判定する。
183124 *
184- * @param file 任意のファイル
125+ * <p>ファイルがすでに通常ファイルとしてローカルに存在し、
126+ * ファイル名の拡張子が「.jar」であれば真と判定される。
127+ *
128+ * @param path 任意のファイル
185129 * @return すでに存在するJARファイルであればtrue
186130 */
187- public static boolean isExistsJarFile(File file){
188- if(file == null) return false;
189- if( ! file.exists() ) return false;
190- if( ! file.isFile() ) return false;
191-
192- String name = file.getName();
193- if( ! name.matches("^.+\\.[jJ][aA][rR]$") ) return false;
131+ public static boolean isExistsJarFile(Path path){
132+ if(path == null) return false;
133+ if( ! Files.exists(path) ) return false;
134+ if( ! Files.isRegularFile(path) ) return false;
194135
195- // TODO ファイル先頭マジックナンバーのテストも必要?
136+ Path leaf = path.getFileName();
137+ assert leaf != null;
138+ String leafName = leaf.toString();
139+ boolean result = leafName.matches("^.+\\.[jJ][aA][rR]$");
196140
197- return true;
141+ return result;
198142 }
199143
200144 /**
201145 * クラスがローカルJARファイルからロードされたのであれば
202- * その格納ディレクトリを返す。
146+ * その格納ディレクトリの絶対パスを返す。
203147 *
204148 * @param klass 任意のクラス
205- * @return ロード元JARファイルの格納ディレクトリ。
149+ * @return ロード元JARファイルの格納ディレクトリの絶対パス。
206150 * JARが見つからない、もしくはロード元がJARファイルでなければnull。
207151 */
208- public static File getJarDirectory(Class<?> klass){
209- File jarFile = getClassSourceFile(klass);
152+ public static Path getJarDirectory(Class<?> klass){
153+ Path jarFile = getClassSourcePath(klass);
210154 if(jarFile == null) return null;
211155
212156 if( ! isExistsJarFile(jarFile) ){
213157 return null;
214158 }
215159
216- return jarFile.getParentFile();
160+ Path result = jarFile.getParent();
161+ assert result.isAbsolute();
162+
163+ return result;
217164 }
218165
219166 /**
220167 * このクラスがローカルJARファイルからロードされたのであれば
221- * その格納ディレクトリを返す。
168+ * その格納ディレクトリの絶対パスを返す。
222169 *
223- * @return ロード元JARファイルの格納ディレクトリ。
170+ * @return ロード元JARファイルの格納ディレクトリの絶対パス。
224171 * JARが見つからない、もしくはロード元がJARファイルでなければnull。
225172 */
226- public static File getJarDirectory(){
173+ public static Path getJarDirectory(){
227174 return getJarDirectory(THISKLASS);
228175 }
229176
230177 /**
231- * ホームディレクトリを得る。
232- *
233- * <p>システムプロパティuser.homeで示されたホームディレクトリを返す。
234- *
235- * @return ホームディレクトリ。何らかの事情でnullを返す場合もあり。
236- */
237- public static File getHomeDirectory(){
238- String homeProp;
239- try{
240- homeProp = System.getProperty("user.home");
241- }catch(SecurityException e){
242- return null;
243- }
244-
245- File homeFile = new File(homeProp);
246-
247- return homeFile;
248- }
249-
250- /**
251178 * MacOSX環境か否か判定する。
252179 *
253180 * @return MacOSX環境ならtrue
@@ -255,22 +182,13 @@ public final class FileUtils{
255182 public static boolean isMacOSXFs(){
256183 if(File.separatorChar != '/') return false;
257184
258- String osName;
259- try{
260- osName = System.getProperty(SYSPROP_OSNAME);
261- }catch(SecurityException e){
262- return false;
263- }
264-
185+ String osName = System.getProperty(SYSPROP_OSNAME);
265186 if(osName == null) return false;
266187
267188 osName = osName.toLowerCase(Locale.ROOT);
268189
269- if(osName.startsWith("mac os x")){
270- return true;
271- }
272-
273- return false;
190+ boolean result = osName.startsWith("mac os x");
191+ return result;
274192 }
275193
276194 /**
@@ -281,48 +199,12 @@ public final class FileUtils{
281199 public static boolean isWindowsOSFs(){
282200 if(File.separatorChar != '\\') return false;
283201
284- String osName;
285- try{
286- osName = System.getProperty(SYSPROP_OSNAME);
287- }catch(SecurityException e){
288- return false;
289- }
290-
202+ String osName = System.getProperty(SYSPROP_OSNAME);
291203 if(osName == null) return false;
292204
293205 osName = osName.toLowerCase(Locale.ROOT);
294206
295- if(osName.startsWith("windows")){
296- return true;
297- }
298-
299- return false;
300- }
301-
302- /**
303- * アプリケーション設定ディレクトリを返す。
304- *
305- * <p>存在の有無、アクセスの可否は関知しない。
306- *
307- * <p>WindowsやLinuxではホームディレクトリ。
308- * Mac OS X ではさらにホームディレクトリの下の
309- * "Library/Application Support/"
310- *
311- * @return アプリケーション設定ディレクトリ
312- */
313- public static File getAppSetDir(){
314- File home = getHomeDirectory();
315- if(home == null) return null;
316-
317- File result = home;
318-
319- if(isMacOSXFs()){
320- result = new File(result, "Library");
321- result = new File(result, "Application Support");
322- }
323-
324- // TODO Win環境での%APPDATA%サポート
325-
207+ boolean result = osName.startsWith("windows");
326208 return result;
327209 }
328210
@@ -331,11 +213,11 @@ public final class FileUtils{
331213 *
332214 * <p>Windows日本語環境では、バックスラッシュ記号が円通貨記号に置換される。
333215 *
334- * @param file 対象ファイル
216+ * @param path 対象ファイル
335217 * @return HTML文字列断片
336218 */
337- public static String getHtmledFileName(File file){
338- String pathName = file.getPath();
219+ public static String getHtmledFileName(Path path){
220+ String pathName = path.toString();
339221
340222 Locale locale = Locale.getDefault();
341223 String lang = locale.getLanguage();
--- /dev/null
+++ b/src/main/java/jp/sfjp/jindolf/config/JsonIo.java
@@ -0,0 +1,339 @@
1+/*
2+ * JSON I/O
3+ *
4+ * License : The MIT License
5+ * Copyright(c) 2020 olyutorskii
6+ */
7+
8+package jp.sfjp.jindolf.config;
9+
10+import java.io.BufferedInputStream;
11+import java.io.BufferedOutputStream;
12+import java.io.BufferedReader;
13+import java.io.BufferedWriter;
14+import java.io.IOException;
15+import java.io.InputStream;
16+import java.io.InputStreamReader;
17+import java.io.OutputStream;
18+import java.io.OutputStreamWriter;
19+import java.io.Reader;
20+import java.io.Writer;
21+import java.nio.charset.Charset;
22+import java.nio.charset.StandardCharsets;
23+import java.nio.file.Files;
24+import java.nio.file.Path;
25+import java.nio.file.Paths;
26+import java.util.Objects;
27+import java.util.logging.Level;
28+import java.util.logging.Logger;
29+import jp.sourceforge.jovsonz.JsComposition;
30+import jp.sourceforge.jovsonz.JsObject;
31+import jp.sourceforge.jovsonz.JsParseException;
32+import jp.sourceforge.jovsonz.JsTypes;
33+import jp.sourceforge.jovsonz.JsVisitException;
34+import jp.sourceforge.jovsonz.Json;
35+
36+/**
37+ * JSONファイルの入出力。
38+ */
39+public class JsonIo {
40+
41+ /** 検索履歴ファイル。 */
42+ public static final Path HIST_FILE = Paths.get("searchHistory.json");
43+ /** ネットワーク設定ファイル。 */
44+ public static final Path NETCONFIG_FILE = Paths.get("netconfig.json");
45+ /** 台詞表示設定ファイル。 */
46+ public static final Path TALKCONFIG_FILE = Paths.get("talkconfig.json");
47+
48+ /** ローカル画像設定ファイル。 */
49+ public static final Path LOCALIMGCONFIG_PATH =
50+ Paths.get("avatarCache.json");
51+
52+
53+ private static final Charset CHARSET_JSON = StandardCharsets.UTF_8;
54+ private static final Logger LOGGER = Logger.getAnonymousLogger();
55+
56+
57+ private final ConfigStore configStore;
58+
59+
60+ /**
61+ * Constructor.
62+ *
63+ * @param configStore 設定ディレクトリ
64+ */
65+ public JsonIo(ConfigStore configStore){
66+ super();
67+ Objects.nonNull(configStore);
68+ this.configStore = configStore;
69+ return;
70+ }
71+
72+
73+ /**
74+ * 設定ディレクトリ上のOBJECT型JSONファイルを読み込む。
75+ *
76+ * @param file JSONファイルの相対パス。
77+ * @return JSON object。
78+ * 設定ディレクトリを使わない設定、
79+ * もしくはJSONファイルが存在しない、
80+ * もしくはOBJECT型でなかった、
81+ * もしくは入力エラーがあればnull
82+ */
83+ public JsObject loadJsObject(Path file){
84+ if( ! this.configStore.useStoreFile()){
85+ return null;
86+ }
87+
88+ JsComposition<?> root = loadJson(file);
89+ if(root == null || root.getJsTypes() != JsTypes.OBJECT) return null;
90+ JsObject result = (JsObject) root;
91+ return result;
92+ }
93+
94+ /**
95+ * 設定ディレクトリ上のJSONファイルを読み込む。
96+ *
97+ * @param file JSONファイルの相対パス
98+ * @return JSON objectまたはarray。
99+ * 設定ディレクトリを使わない設定、
100+ * もしくはJSONファイルが存在しない、
101+ * もしくは入力エラーがあればnull
102+ */
103+ protected JsComposition<?> loadJson(Path file){
104+ Path absFile;
105+ if(file.isAbsolute()){
106+ absFile = file;
107+ }else{
108+ Path configDir = this.configStore.getConfigDir();
109+ if(configDir == null) return null;
110+ absFile = configDir.resolve(file);
111+ if( ! Files.exists(absFile) ) return null;
112+ if( ! absFile.isAbsolute() ) return null;
113+ }
114+ String absPath = absFile.toString();
115+
116+ JsComposition<?> root;
117+ try(InputStream is = Files.newInputStream(absFile)){
118+ InputStream bis = new BufferedInputStream(is);
119+ root = loadJson(bis);
120+ }catch(IOException e){
121+ LOGGER.log(Level.SEVERE,
122+ "JSONファイル["
123+ + absPath
124+ + "]の読み込み時に支障がありました。", e);
125+ return null;
126+ }catch(JsParseException e){
127+ LOGGER.log(Level.SEVERE,
128+ "JSONファイル["
129+ + absPath
130+ + "]の内容に不備があります。", e);
131+ return null;
132+ }
133+
134+ return root;
135+ }
136+
137+ /**
138+ * バイトストリーム上のJSONデータを読み込む。
139+ *
140+ * <p>バイトストリームはUTF-8と解釈される。
141+ *
142+ * @param is バイトストリーム
143+ * @return JSON objectまたはarray。
144+ * @throws IOException 入力エラー
145+ * @throws JsParseException 構文エラー
146+ */
147+ protected JsComposition<?> loadJson(InputStream is)
148+ throws IOException, JsParseException {
149+ Reader reader = new InputStreamReader(is, CHARSET_JSON);
150+ reader = new BufferedReader(reader);
151+ JsComposition<?> root = loadJson(reader);
152+ return root;
153+ }
154+
155+ /**
156+ * 文字ストリーム上のJSONデータを読み込む。
157+ *
158+ * @param reader 文字ストリーム
159+ * @return JSON objectまたはarray。
160+ * @throws IOException 入力エラー
161+ * @throws JsParseException 構文エラー
162+ */
163+ protected JsComposition<?> loadJson(Reader reader)
164+ throws IOException, JsParseException {
165+ JsComposition<?> root = Json.parseJson(reader);
166+ return root;
167+ }
168+
169+ /**
170+ * 設定ディレクトリ上のJSONファイルに書き込む。
171+ *
172+ * @param file JSONファイルの相対パス
173+ * @param root JSON objectまたはarray
174+ * @return 正しくセーブが行われればtrue。
175+ * 何らかの理由でセーブが完了できなければfalse
176+ */
177+ public boolean saveJson(Path file, JsComposition<?> root){
178+ if( ! this.configStore.useStoreFile()){
179+ return false;
180+ }
181+
182+ // TODO テンポラリファイルを用いたより安全なファイル更新
183+ Path configDir = this.configStore.getConfigDir();
184+ Path absFile = configDir.resolve(file);
185+ String absPath = absFile.toString();
186+
187+ try{
188+ Files.deleteIfExists(absFile);
189+ }catch(IOException e){
190+ // NOTHING
191+ assert true;
192+ }
193+
194+ try{
195+ Files.createFile(absFile);
196+ }catch(IOException e){
197+ LOGGER.log(Level.SEVERE,
198+ "JSONファイル["
199+ + absPath
200+ + "]の新規生成ができません。", e);
201+ return false;
202+ }
203+
204+ try(OutputStream os = Files.newOutputStream(absFile)){
205+ OutputStream bos = new BufferedOutputStream(os);
206+ saveJson(bos, root);
207+ }catch(IOException e){
208+ LOGGER.log(Level.SEVERE,
209+ "JSONファイル["
210+ + absPath
211+ + "]の書き込み時に支障がありました。", e);
212+ return false;
213+ }catch(JsVisitException e){
214+ LOGGER.log(Level.SEVERE,
215+ "JSONファイル["
216+ + absPath
217+ + "]の出力処理で支障がありました。", e);
218+ return false;
219+ }
220+
221+ return true;
222+ }
223+
224+ /**
225+ * バイトストリームにJSONデータを書き込む。
226+ *
227+ * <p>バイトストリームはUTF-8と解釈される。
228+ *
229+ * @param os バイトストリーム出力
230+ * @param root JSON objectまたはarray
231+ * @throws IOException 出力エラー
232+ * @throws JsVisitException 構造エラー
233+ */
234+ protected void saveJson(OutputStream os, JsComposition<?> root)
235+ throws IOException, JsVisitException {
236+ Writer writer = new OutputStreamWriter(os, CHARSET_JSON);
237+ writer = new BufferedWriter(writer);
238+ saveJson(writer, root);
239+ return;
240+ }
241+
242+ /**
243+ * 文字ストリームにJSONデータを書き込む。
244+ *
245+ * @param writer 文字ストリーム出力
246+ * @param root JSON objectまたはarray
247+ * @throws IOException 出力エラー
248+ * @throws JsVisitException 構造エラー
249+ */
250+ protected void saveJson(Writer writer, JsComposition<?> root)
251+ throws IOException, JsVisitException {
252+ Json.dumpJson(writer, root);
253+ return;
254+ }
255+
256+ /**
257+ * 検索履歴ファイルを読み込む。
258+ *
259+ * @return 履歴データ。履歴を読まないもしくは読めない場合はnull
260+ */
261+ public JsObject loadHistoryConfig(){
262+ JsObject result = loadJsObject(HIST_FILE);
263+ return result;
264+ }
265+
266+ /**
267+ * ネットワーク設定ファイルを読み込む。
268+ *
269+ * @return ネットワーク設定データ。
270+ * 設定を読まないもしくは読めない場合はnull
271+ */
272+ public JsObject loadNetConfig(){
273+ JsObject result = loadJsObject(NETCONFIG_FILE);
274+ return result;
275+ }
276+
277+ /**
278+ * 台詞表示設定ファイルを読み込む。
279+ *
280+ * @return 台詞表示設定データ。
281+ * 設定を読まないもしくは読めない場合はnull
282+ */
283+ public JsObject loadTalkConfig(){
284+ JsObject result = loadJsObject(TALKCONFIG_FILE);
285+ return result;
286+ }
287+
288+ /**
289+ * ローカル画像設定ファイルを読み込む。
290+ *
291+ * @return ローカル画像設定データ。
292+ * 設定を読まないもしくは読めない場合はnull
293+ */
294+ public JsObject loadLocalImgConfig(){
295+ if( ! this.configStore.useStoreFile()){
296+ return null;
297+ }
298+
299+ Path imgDir = this.configStore.getLocalImgDir();
300+ Path path = imgDir.resolve(LOCALIMGCONFIG_PATH);
301+ JsObject result = loadJsObject(path);
302+
303+ return result;
304+ }
305+
306+ /**
307+ * 検索履歴ファイルに書き込む。
308+ *
309+ * @param root 履歴データ
310+ * @return 書き込まなかったもしくは書き込めなかった場合はfalse
311+ */
312+ public boolean saveHistoryConfig(JsComposition<?> root){
313+ boolean result = saveJson(HIST_FILE, root);
314+ return result;
315+ }
316+
317+ /**
318+ * ネットワーク設定ファイルに書き込む。
319+ *
320+ * @param root ネットワーク設定
321+ * @return 書き込まなかったもしくは書き込めなかった場合はfalse
322+ */
323+ public boolean saveNetConfig(JsComposition<?> root){
324+ boolean result = saveJson(NETCONFIG_FILE, root);
325+ return result;
326+ }
327+
328+ /**
329+ * 台詞表示設定ファイルに書き込む。
330+ *
331+ * @param root 台詞表示設定
332+ * @return 書き込まなかったもしくは書き込めなかった場合はfalse
333+ */
334+ public boolean saveTalkConfig(JsComposition<?> root){
335+ boolean result = saveJson(TALKCONFIG_FILE, root);
336+ return result;
337+ }
338+
339+}
--- a/src/main/java/jp/sfjp/jindolf/config/OptionInfo.java
+++ b/src/main/java/jp/sfjp/jindolf/config/OptionInfo.java
@@ -24,6 +24,10 @@ import java.util.regex.Pattern;
2424 */
2525 public class OptionInfo{
2626
27+ /*
28+ ex) 1000x800
29+ ex) 1290x1024-256+128
30+ */
2731 private static final String REGEX_DIMNO =
2832 "([1-9][0-9]{0,5})";
2933 private static final String REGEX_SIGN =
@@ -96,9 +100,9 @@ public class OptionInfo{
96100 * @throws IllegalArgumentException 引数の構文エラー
97101 */
98102 private static void parseBooleanSwitch(OptionInfo info,
99- CmdOption option,
100- String optTxt,
101- String onoff )
103+ CmdOption option,
104+ String optTxt,
105+ String onoff )
102106 throws IllegalArgumentException{
103107 Boolean flag;
104108
@@ -128,8 +132,8 @@ public class OptionInfo{
128132 * @throws IllegalArgumentException 引数の構文エラー
129133 */
130134 private static void parseGeometry(OptionInfo info,
131- String optTxt,
132- String geometry )
135+ String optTxt,
136+ String geometry )
133137 throws IllegalArgumentException{
134138 Matcher matcher = PATTERN_GEOMETRY.matcher(geometry);
135139 if( ! matcher.matches() ){
@@ -176,18 +180,17 @@ public class OptionInfo{
176180 * @throws IllegalArgumentException オプションの引数がない
177181 */
178182 private static void parseOptionArg(OptionInfo info,
179- String optTxt,
180- CmdOption option,
181- Iterator<String> iterator )
183+ String optTxt,
184+ CmdOption option,
185+ Iterator<String> iterator )
182186 throws IllegalArgumentException {
183- String nextArg;
184- if(iterator.hasNext()){
185- nextArg = iterator.next();
186- }else{
187+ if( ! iterator.hasNext()){
187188 String errMsg = MessageFormat.format(ERRFORM_NOARG, optTxt);
188189 throw new IllegalArgumentException(errMsg);
189190 }
190191
192+ String nextArg = iterator.next();
193+
191194 if(option == CmdOption.OPT_GEOMETRY){
192195 parseGeometry(info, optTxt, nextArg);
193196 }else if(option.isBooleanOption()){
@@ -254,8 +257,8 @@ public class OptionInfo{
254257 * @return 指定されていたらtrue
255258 */
256259 public boolean hasOption(CmdOption option){
257- if(this.optionList.contains(option)) return true;
258- return false;
260+ boolean result = this.optionList.contains(option);
261+ return result;
259262 }
260263
261264 /**
--- a/src/main/java/jp/sfjp/jindolf/data/Land.java
+++ b/src/main/java/jp/sfjp/jindolf/data/Land.java
@@ -12,8 +12,8 @@ import java.io.IOException;
1212 import java.net.MalformedURLException;
1313 import java.net.URI;
1414 import java.net.URL;
15+import java.util.ArrayList;
1516 import java.util.Collections;
16-import java.util.LinkedList;
1717 import java.util.List;
1818 import java.util.logging.Level;
1919 import java.util.logging.Logger;
@@ -22,6 +22,8 @@ import jp.sourceforge.jindolf.corelib.LandDef;
2222
2323 /**
2424 * いわゆる「国」。
25+ *
26+ * 人狼BBSのサーバと1:1の概念。
2527 */
2628 public class Land {
2729
@@ -31,11 +33,12 @@ public class Land {
3133 private final LandDef landDef;
3234 private final ServerAccess serverAccess;
3335
34- private final List<Village> villageList = new LinkedList<>();
36+ private final List<Village> villageList = new ArrayList<>(1000);
3537
3638
3739 /**
3840 * コンストラクタ。
41+ *
3942 * @param landDef 国定義
4043 * @throws java.lang.IllegalArgumentException 不正な国定義
4144 */
@@ -58,6 +61,7 @@ public class Land {
5861
5962 /**
6063 * 国定義を得る。
64+ *
6165 * @return 国定義
6266 */
6367 public LandDef getLandDef(){
@@ -66,6 +70,7 @@ public class Land {
6670
6771 /**
6872 * サーバ接続を返す。
73+ *
6974 * @return ServerAccessインスタンス
7075 */
7176 public ServerAccess getServerAccess(){
@@ -74,6 +79,7 @@ public class Land {
7479
7580 /**
7681 * 指定されたインデックス位置の村を返す。
82+ *
7783 * @param index 0から始まるインデックス値
7884 * @return 村
7985 */
@@ -87,6 +93,7 @@ public class Land {
8793
8894 /**
8995 * 村の総数を返す。
96+ *
9097 * @return 村の総数
9198 */
9299 public int getVillageCount(){
@@ -96,6 +103,7 @@ public class Land {
96103
97104 /**
98105 * 村のリストを返す。
106+ *
99107 * @return 村のリスト
100108 */
101109 // TODO インスタンス変数でいいはず。
@@ -105,7 +113,9 @@ public class Land {
105113
106114 /**
107115 * 絶対または相対URLの指すパーマネントなイメージ画像をダウンロードする。
108- * ※ A,B,D 国の顔アイコンは絶対パスらしい…。
116+ *
117+ * <p>※ A,B,D 国の顔アイコンは絶対パスらしい…。
118+ *
109119 * @param imageURL 画像URL文字列
110120 * @return 画像イメージ
111121 */
@@ -126,6 +136,7 @@ public class Land {
126136
127137 /**
128138 * 墓アイコンイメージを取得する。
139+ *
129140 * @return 墓アイコンイメージ
130141 */
131142 public BufferedImage getGraveIconImage(){
@@ -136,6 +147,7 @@ public class Land {
136147
137148 /**
138149 * 墓アイコンイメージ(大)を取得する。
150+ *
139151 * @return 墓アイコンイメージ(大)
140152 */
141153 public BufferedImage getGraveBodyImage(){
@@ -146,6 +158,7 @@ public class Land {
146158
147159 /**
148160 * 村リストを更新する。
161+ *
149162 * @param vset ソート済みの村一覧
150163 */
151164 public void updateVillageList(List<Village> vset){
@@ -157,6 +170,7 @@ public class Land {
157170
158171 /**
159172 * 国の文字列表現を返す。
173+ *
160174 * @return 文字列表現
161175 */
162176 @Override
--- a/src/main/java/jp/sfjp/jindolf/data/LandsTreeModel.java
+++ b/src/main/java/jp/sfjp/jindolf/data/LandsTreeModel.java
@@ -7,12 +7,13 @@
77
88 package jp.sfjp.jindolf.data;
99
10+import java.text.MessageFormat;
11+import java.util.ArrayList;
1012 import java.util.Collections;
1113 import java.util.HashMap;
12-import java.util.LinkedList;
1314 import java.util.List;
1415 import java.util.Map;
15-import java.util.logging.Logger;
16+import java.util.Objects;
1617 import javax.swing.event.EventListenerList;
1718 import javax.swing.event.TreeModelEvent;
1819 import javax.swing.event.TreeModelListener;
@@ -21,101 +22,161 @@ import javax.swing.tree.TreePath;
2122 import jp.sourceforge.jindolf.corelib.LandDef;
2223
2324 /**
24- * 国の集合。あらゆるデータモデルの大元。
25- * 国一覧と村一覧を管理。
26- * JTreeのモデルも兼用。
25+ * {@link javax.swing.JTree}のモデルとして国一覧と村一覧を管理。
26+ *
27+ * <p>ツリー階層は ROOT - 国 - 範囲セクション - 村 の4階層。
28+ *
29+ * <p>昇順/降順の切り替えをサポート。
2730 */
28-public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付けるか?
31+public class LandsTreeModel implements TreeModel{
2932
30- private static final String ROOT = "ROOT";
33+ private static final Object ROOT = new Object();
3134 private static final int SECTION_INTERVAL = 100;
3235
33- private static final Logger LOGGER = Logger.getAnonymousLogger();
34-
3536
36- private final List<Land> landList = new LinkedList<>();
37- private final List<Land> unmodList =
38- Collections.unmodifiableList(this.landList);
39- private final Map<Land, List<VillageSection>> sectionMap =
40- new HashMap<>();
41- private boolean isLandListLoaded = false;
37+ private final EventListenerList listeners;
4238
43- private final EventListenerList listeners = new EventListenerList();
39+ private final List<Land> landList;
40+ private final Map<Land, List<VillageSection> > sectionMap;
4441
4542 private boolean ascending = false;
4643
44+
4745 /**
4846 * コンストラクタ。
49- * この時点ではまだ国一覧が読み込まれない。
5047 */
5148 public LandsTreeModel(){
5249 super();
50+
51+ this.listeners = new EventListenerList();
52+
53+ this.landList = buildLandList();
54+ this.sectionMap = new HashMap<>();
55+
5356 return;
5457 }
5558
59+
5660 /**
57- * 指定した国の村一覧を更新しイベントを投げる。
58- * @param land 国
61+ * ツリーのルートオブジェクトか否か判定する。
62+ *
63+ * @param obj オブジェクト
64+ * @return ルートならtrue
5965 */
60- public void updateVillageList(Land land){
61- List<VillageSection> sectionList =
62- VillageSection.getSectionList(land, SECTION_INTERVAL);
63- this.sectionMap.put(land, sectionList);
66+ private static boolean isRoot(Object obj){
67+ boolean result = Objects.equals(ROOT, obj);
68+ return result;
69+ }
6470
65- int[] childIndices = new int[sectionList.size()];
66- for(int ct = 0; ct < childIndices.length; ct++){
67- childIndices[ct] = ct;
68- }
69- Object[] children = sectionList.toArray();
71+ /**
72+ * 国一覧を読み込む。
73+ *
74+ * <p>村一覧はまだ読み込まれない。
75+ *
76+ * @return 国リスト
77+ */
78+ private static List<Land> buildLandList(){
79+ List<LandDef> landDefList = CoreData.getLandDefList();
80+ List<Land> newList = new ArrayList<>(landDefList.size());
7081
71- Object[] path = {ROOT, land};
72- TreePath treePath = new TreePath(path);
73- TreeModelEvent event = new TreeModelEvent(this,
74- treePath,
75- childIndices,
76- children );
77- fireTreeStructureChanged(event);
82+ landDefList.stream().map(landDef ->
83+ new Land(landDef)
84+ ).forEachOrdered(land -> {
85+ newList.add(land);
86+ });
7887
79- return;
88+ return Collections.unmodifiableList(newList);
8089 }
8190
8291 /**
83- * 国一覧を読み込む。
92+ * 与えられた国の全ての村を指定されたinterval間隔で格納するために、
93+ * 範囲セクションのリストを生成する。
94+ *
95+ * @param land 国
96+ * @param interval 範囲セクション間の村ID間隔
97+ * @return 範囲セクションのリスト
98+ * @throws java.lang.IllegalArgumentException intervalが正でない
8499 */
85- // TODO static にできない?
86- public void loadLandList(){
87- if(this.isLandListLoaded) return;
100+ private static List<VillageSection> getSectionList(Land land,
101+ int interval )
102+ throws IllegalArgumentException{
103+ if(interval <= 0){
104+ throw new IllegalArgumentException();
105+ }
88106
89- this.landList.clear();
107+ String pfx = land.getLandDef().getLandPrefix();
108+ List<Village> span = new ArrayList<>(interval);
90109
91- List<LandDef> landDefList = CoreData.getLandDefList();
92- landDefList.stream().map((landDef) ->
93- new Land(landDef)
94- ).forEachOrdered((land) -> {
95- this.landList.add(land);
96- });
110+ List<VillageSection> result = new ArrayList<>(2500 / interval);
97111
98- this.isLandListLoaded = true;
112+ boolean loop1st = true;
113+ int rangeStart = -1;
114+ int rangeEnd = -1;
99115
100- fireLandListChanged();
116+ for(Village village : land.getVillageList()){
117+ int vid = village.getVillageIDNum();
101118
102- return;
119+ if(loop1st){
120+ rangeStart = vid / interval * interval;
121+ rangeEnd = rangeStart + interval - 1;
122+ loop1st = false;
123+ }
124+
125+ if(rangeEnd < vid){
126+ VillageSection section = new VillageSection(
127+ pfx, rangeStart, rangeEnd, span);
128+ span.clear();
129+ result.add(section);
130+
131+ rangeStart = vid / interval * interval;
132+ rangeEnd = rangeStart + interval - 1;
133+ }
134+
135+ span.add(village);
136+ }
137+
138+ if( ! span.isEmpty()){
139+ VillageSection section = new VillageSection(
140+ pfx, rangeStart, rangeEnd, span);
141+ span.clear();
142+ result.add(section);
143+ }
144+
145+ return result;
103146 }
104147
148+
105149 /**
106- * ツリー内容が更新された事をリスナーに通知する。
150+ * 国リストを得る。
151+ *
152+ * @return 国のリスト
107153 */
108- private void fireLandListChanged(){
109- int size = this.landList.size();
110- int[] childIndices = new int[size];
111- for(int ct = 0; ct < size; ct++){
112- int index = ct;
113- childIndices[ct] = index;
114- }
154+ public List<Land> getLandList(){
155+ return this.landList;
156+ }
115157
116- Object[] children = this.landList.toArray();
158+ /**
159+ * 指定した国の村一覧でツリーリストを更新し、
160+ * 更新イベントをリスナに投げる。
161+ *
162+ * <p>2020-04現在、もはや村一覧が増減することはない。
163+ *
164+ * @param land 国
165+ */
166+ public void updateVillageList(Land land){
167+ List<VillageSection> sectionList =
168+ getSectionList(land, SECTION_INTERVAL);
169+ this.sectionMap.put(land, sectionList);
170+
171+ int[] childIndices = new int[sectionList.size()];
172+ for(int ct = 0; ct < childIndices.length; ct++){
173+ childIndices[ct] = ct;
174+ }
175+ Object[] children = sectionList.toArray();
117176
118177 TreePath treePath = new TreePath(ROOT);
178+ treePath = treePath.pathByAddingChild(land);
179+
119180 TreeModelEvent event = new TreeModelEvent(this,
120181 treePath,
121182 childIndices,
@@ -127,7 +188,9 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
127188
128189 /**
129190 * ツリーの並び順を設定する。
130- * 場合によってはTreeModelEventが発生する。
191+ *
192+ * <p>場合によってはTreeModelEventが発生する。
193+ *
131194 * @param ascending trueなら昇順
132195 */
133196 public void setAscending(boolean ascending){
@@ -141,26 +204,29 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
141204
142205 /**
143206 * {@inheritDoc}
144- * @param l {@inheritDoc}
207+ *
208+ * @param lst {@inheritDoc}
145209 */
146210 @Override
147- public void addTreeModelListener(TreeModelListener l){
148- this.listeners.add(TreeModelListener.class, l);
211+ public void addTreeModelListener(TreeModelListener lst){
212+ this.listeners.add(TreeModelListener.class, lst);
149213 return;
150214 }
151215
152216 /**
153217 * {@inheritDoc}
154- * @param l {@inheritDoc}
218+ *
219+ * @param lst {@inheritDoc}
155220 */
156221 @Override
157- public void removeTreeModelListener(TreeModelListener l){
158- this.listeners.remove(TreeModelListener.class, l);
222+ public void removeTreeModelListener(TreeModelListener lst){
223+ this.listeners.remove(TreeModelListener.class, lst);
159224 return;
160225 }
161226
162227 /**
163228 * 登録中のリスナーのリストを得る。
229+ *
164230 * @return リスナーのリスト
165231 */
166232 private TreeModelListener[] getTreeModelListeners(){
@@ -169,6 +235,7 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
169235
170236 /**
171237 * 全リスナーにイベントを送出する。
238+ *
172239 * @param event ツリーイベント
173240 */
174241 protected void fireTreeStructureChanged(TreeModelEvent event){
@@ -179,15 +246,31 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
179246 }
180247
181248 /**
182- * 国リストを得る。
183- * @return 国のリスト
249+ * ツリー内容の国一覧が更新された事をリスナーに通知する。
184250 */
185- public List<Land> getLandList(){
186- return this.unmodList;
251+ private void fireLandListChanged(){
252+ int size = getLandList().size();
253+ int[] childIndices = new int[size];
254+ for(int ct = 0; ct < size; ct++){
255+ int index = ct;
256+ childIndices[ct] = index;
257+ }
258+
259+ Object[] children = getLandList().toArray();
260+
261+ TreePath treePath = new TreePath(ROOT);
262+ TreeModelEvent event = new TreeModelEvent(this,
263+ treePath,
264+ childIndices,
265+ children );
266+ fireTreeStructureChanged(event);
267+
268+ return;
187269 }
188270
189271 /**
190272 * {@inheritDoc}
273+ *
191274 * @param parent {@inheritDoc}
192275 * @param index {@inheritDoc}
193276 * @return {@inheritDoc}
@@ -197,58 +280,63 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
197280 if(index < 0) return null;
198281 if(index >= getChildCount(parent)) return null;
199282
200- if(parent == ROOT){
283+ Object result = null;
284+
285+ if(isRoot(parent)){
201286 List<Land> list = getLandList();
202287 int landIndex = index;
203288 if( ! this.ascending) landIndex = list.size() - index - 1;
204289 Land land = list.get(landIndex);
205- return land;
206- }
207- if(parent instanceof Land){
290+ result = land;
291+ }else if(parent instanceof Land){
208292 Land land = (Land) parent;
209293 List<VillageSection> sectionList = this.sectionMap.get(land);
210294 int sectIndex = index;
211295 if( ! this.ascending) sectIndex = sectionList.size() - index - 1;
212296 VillageSection section = sectionList.get(sectIndex);
213- return section;
214- }
215- if(parent instanceof VillageSection){
297+ result = section;
298+ }else if(parent instanceof VillageSection){
216299 VillageSection section = (VillageSection) parent;
217300 int vilIndex = index;
218301 if( ! this.ascending){
219302 vilIndex = section.getVillageCount() - index - 1;
220303 }
221304 Village village = section.getVillage(vilIndex);
222- return village;
305+ result = village;
223306 }
224- return null;
307+
308+ return result;
225309 }
226310
227311 /**
228312 * {@inheritDoc}
313+ *
229314 * @param parent {@inheritDoc}
230315 * @return {@inheritDoc}
231316 */
232317 @Override
233318 public int getChildCount(Object parent){
234- if(parent == ROOT){
235- return getLandList().size();
236- }
237- if(parent instanceof Land){
319+ int result = 0;
320+
321+ if(isRoot(parent)){
322+ result = getLandList().size();
323+ }else if(parent instanceof Land){
238324 Land land = (Land) parent;
239325 List<VillageSection> sectionList = this.sectionMap.get(land);
240- if(sectionList == null) return 0;
241- return sectionList.size();
242- }
243- if(parent instanceof VillageSection){
326+ if(sectionList != null){
327+ result = sectionList.size();
328+ }
329+ }else if(parent instanceof VillageSection){
244330 VillageSection section = (VillageSection) parent;
245- return section.getVillageCount();
331+ result = section.getVillageCount();
246332 }
247- return 0;
333+
334+ return result;
248335 }
249336
250337 /**
251338 * {@inheritDoc}
339+ *
252340 * @param parent {@inheritDoc}
253341 * @param child {@inheritDoc}
254342 * @return {@inheritDoc}
@@ -256,32 +344,35 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
256344 @Override
257345 public int getIndexOfChild(Object parent, Object child){
258346 if(child == null) return -1;
259- if(parent == ROOT){
347+
348+ int result = -1;
349+
350+ if(isRoot(parent)){
260351 List<Land> list = getLandList();
261352 int index = list.indexOf(child);
262353 if( ! this.ascending) index = list.size() - index - 1;
263- return index;
264- }
265- if(parent instanceof Land){
354+ result = index;
355+ }else if(parent instanceof Land){
266356 Land land = (Land) parent;
267357 List<VillageSection> sectionList = this.sectionMap.get(land);
268358 int index = sectionList.indexOf(child);
269359 if( ! this.ascending) index = sectionList.size() - index - 1;
270- return index;
271- }
272- if(parent instanceof VillageSection){
360+ result = index;
361+ }else if(parent instanceof VillageSection){
273362 VillageSection section = (VillageSection) parent;
274363 int index = section.getIndexOfVillage(child);
275364 if( ! this.ascending){
276365 index = section.getVillageCount() - index - 1;
277366 }
278- return index;
367+ result = index;
279368 }
280- return -1;
369+
370+ return result;
281371 }
282372
283373 /**
284374 * {@inheritDoc}
375+ *
285376 * @return {@inheritDoc}
286377 */
287378 @Override
@@ -291,21 +382,24 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
291382
292383 /**
293384 * {@inheritDoc}
385+ *
294386 * @param node {@inheritDoc}
295387 * @return {@inheritDoc}
296388 */
297389 @Override
298390 public boolean isLeaf(Object node){
299- if(node == ROOT) return false;
300- if(node instanceof Land) return false;
301- if(node instanceof VillageSection) return false;
302391 if(node instanceof Village) return true;
392+ if(node instanceof VillageSection) return false;
393+ if(node instanceof Land) return false;
394+ if(isRoot(node)) return false;
303395 return true;
304396 }
305397
306398 /**
307399 * {@inheritDoc}
308- * ※ たぶん使わないので必ず失敗させている。
400+ *
401+ * <p>※ たぶん使わないので必ず失敗させている。
402+ *
309403 * @param path {@inheritDoc}
310404 * @param newValue {@inheritDoc}
311405 */
@@ -314,121 +408,109 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
314408 throw new UnsupportedOperationException("Not supported yet.");
315409 }
316410
411+
317412 /**
318413 * 村IDで範囲指定した、村のセクション集合。国-村間の中間ツリー。
414+ *
319415 * @see javax.swing.tree.TreeModel
320416 */
321417 private static final class VillageSection{
322418
323- private final int startID;
324- private final int endID;
325- private final String prefix;
419+ private static final String FORM_NODE =
420+ "{0}{1,number,#} ~ {0}{2,number,#}";
421+ private static final String FORM_NODE_G =
422+ "{0}{1,number,#000} ~ {0}{2,number,#000}";
423+
424+
425+ private final int startId;
426+ private final int endId;
427+
428+ private final String text;
326429
327- private final List<Village> villageList = new LinkedList<>();
430+ private final List<Village> villageList;
328431
329432
330433 /**
331434 * セクション集合を生成する。
332- * @param land 国
333- * @param startID 開始村ID
334- * @param endID 終了村ID
435+ *
436+ * @param prefix 国名プレフィクス
437+ * @param startId 区間開始村ID
438+ * @param endId 区間終了村ID
439+ * @param spanList 村の区間リスト
335440 * @throws java.lang.IndexOutOfBoundsException IDの範囲指定が変
336441 */
337- private VillageSection(Land land, int startID, int endID)
442+ VillageSection(
443+ String prefix, int startId, int endId, List<Village> spanList)
338444 throws IndexOutOfBoundsException{
339445 super();
340446
341- if(startID < 0 || startID > endID){
447+ if(startId < 0 || startId > endId){
342448 throw new IndexOutOfBoundsException();
343449 }
344450
345- this.startID = startID;
346- this.endID = endID;
347- this.prefix = land.getLandDef().getLandPrefix();
451+ this.startId = startId;
452+ this.endId = endId;
348453
349- for(Village village : land.getVillageList()){
350- int id = village.getVillageIDNum();
351- if(startID <= id && id <= endID){
352- this.villageList.add(village);
353- }
354- }
355-
356- return;
357- }
358-
359-
360- /**
361- * 与えられた国の全ての村を、指定されたinterval間隔でセクション化する。
362- * @param land 国
363- * @param interval セクションの間隔
364- * @return セクションのリスト
365- * @throws java.lang.IllegalArgumentException intervalが正でない
366- */
367- private static List<VillageSection> getSectionList(Land land,
368- int interval )
369- throws IllegalArgumentException{
370- if(interval <= 0){
371- throw new IllegalArgumentException();
372- }
373-
374- List<Village> villageList = land.getVillageList();
375- Village village1st = villageList.get(0);
376- Village villageLast = villageList.get(villageList.size() - 1);
454+ String format;
455+ if("G".equals(prefix)) format = FORM_NODE_G;
456+ else format = FORM_NODE;
457+ this.text = MessageFormat.format(
458+ format, prefix, this.startId, this.endId);
377459
378- int startID = village1st.getVillageIDNum();
379- int endID = villageLast.getVillageIDNum();
460+ List<Village> newList = new ArrayList<>(spanList);
461+ this.villageList = Collections.unmodifiableList(newList);
380462
381- List<VillageSection> result = new LinkedList<>();
382-
383- int fixedStart = startID / interval * interval;
384- for(int ct = fixedStart; ct <= endID; ct += interval){
385- VillageSection section =
386- new VillageSection(land, ct, ct + interval - 1);
387- result.add(section);
388- }
463+ assert this.endId - this.startId + 1 >= this.villageList.size();
389464
390- return Collections.unmodifiableList(result);
465+ return;
391466 }
392467
468+
393469 /**
394- * セクションに含まれる村の総数を返す。
470+ * セクション内に含まれる村の総数を返す。
471+ *
472+ * <p>ほとんどの場合はintervalと同じ数。
473+ *
395474 * @return 村の総数
396475 */
397- private int getVillageCount(){
476+ int getVillageCount(){
398477 return this.villageList.size();
399478 }
400479
401480 /**
402- * セクションに含まれるindex番目の村を返す。
481+ * セクション内に含まれるindex番目の村を返す。
482+ *
403483 * @param index インデックス
404484 * @return index番目の村
405485 */
406- private Village getVillage(int index){
486+ Village getVillage(int index){
407487 return this.villageList.get(index);
408488 }
409489
410490 /**
411- * セクションにおける、指定された子(村)のインデックス位置を返す。
491+ * セクション内における、指定された子(村)のインデックス位置を返す。
492+ *
412493 * @param child 子
413494 * @return インデックス位置
414495 */
415- private int getIndexOfVillage(Object child){
496+ int getIndexOfVillage(Object child){
416497 return this.villageList.indexOf(child);
417498 }
418499
419500 /**
420501 * セクションの文字列表記。
421- * JTree描画に反映される。
502+ *
503+ * <p>JTree描画に反映される。
504+ *
505+ * <p>例:「G800 ~ G899」
506+ *
422507 * @return 文字列表記
423508 */
424509 @Override
425510 public String toString(){
426- StringBuilder result = new StringBuilder();
427- result.append(this.prefix).append(this.startID);
428- result.append(" ~ ");
429- result.append(this.prefix).append(this.endID);
430- return result.toString();
511+ return this.text;
431512 }
513+
432514 }
433515
434516 }
--- a/src/main/java/jp/sfjp/jindolf/log/LogFrame.java
+++ b/src/main/java/jp/sfjp/jindolf/log/LogFrame.java
@@ -7,136 +7,202 @@
77
88 package jp.sfjp.jindolf.log;
99
10+import io.github.olyutorskii.quetexj.HeightKeeper;
11+import io.github.olyutorskii.quetexj.MaxTracker;
12+import io.github.olyutorskii.quetexj.MvcFacade;
13+import io.github.olyutorskii.quetexj.SwingLogHandler;
1014 import java.awt.Container;
11-import java.awt.Frame;
12-import java.awt.GridBagConstraints;
13-import java.awt.GridBagLayout;
14-import java.awt.Insets;
15-import java.awt.event.ActionEvent;
16-import java.awt.event.ActionListener;
15+import java.awt.Dialog;
1716 import java.util.logging.Handler;
17+import javax.swing.Action;
18+import javax.swing.BorderFactory;
19+import javax.swing.BoundedRangeModel;
20+import javax.swing.Box;
21+import javax.swing.BoxLayout;
1822 import javax.swing.JButton;
23+import javax.swing.JCheckBox;
1924 import javax.swing.JDialog;
20-import javax.swing.JSeparator;
25+import javax.swing.JPopupMenu;
26+import javax.swing.JScrollBar;
27+import javax.swing.JScrollPane;
28+import javax.swing.JTextArea;
29+import javax.swing.JToggleButton;
30+import javax.swing.border.Border;
31+import javax.swing.text.Document;
32+import jp.sfjp.jindolf.dxchg.TextPopup;
33+import jp.sfjp.jindolf.util.Monodizer;
2134
2235 /**
2336 * ログ表示ウィンドウ。
2437 */
2538 @SuppressWarnings("serial")
26-public class LogFrame extends JDialog {
39+public final class LogFrame extends JDialog {
2740
28- private static final String CMD_CLOSELOG = "CMD_CLOSE_LOG";
29- private static final String CMD_CLEARLOG = "CMD_CLEAR_LOG";
41+ private static final int HEIGHT_LIMIT = 5000;
42+ private static final int HEIGHT_NEW = 4000;
3043
44+ private static final int AROUND_TEXT = 3;
45+ private static final int AROUND_BUTTON = 5;
3146
32- private final LogPanel logPanel = new LogPanel();
33- private final JButton clearButton = new JButton("クリア");
34- private final JButton closeButton = new JButton("閉じる");
47+
48+ private final MvcFacade facade;
49+
50+ private final JScrollPane scrollPane;
51+ private final JButton clearButton;
52+ private final JButton closeButton;
53+ private final JCheckBox trackButton;
54+
55+ private final Handler handler;
3556
3657
3758 /**
3859 * コンストラクタ。
39- * @param owner フレームオーナー
4060 */
41- public LogFrame(Frame owner){
42- super(owner);
61+ public LogFrame(){
62+ super((Dialog)null);
63+ // We need unowned dialog
4364
44- design();
65+ this.facade = new MvcFacade();
66+
67+ this.scrollPane = buildScrollPane(this.facade);
4568
46- this.clearButton.setActionCommand(CMD_CLEARLOG);
47- this.closeButton.setActionCommand(CMD_CLOSELOG);
69+ this.clearButton = new JButton();
70+ this.closeButton = new JButton();
71+ this.trackButton = new JCheckBox();
4872
49- ActionListener actionListener = new ActionWatcher();
50- this.clearButton.addActionListener(actionListener);
51- this.closeButton.addActionListener(actionListener);
73+ setupButtons();
74+
75+ MaxTracker tracker = this.facade.getMaxTracker();
76+ HeightKeeper keeper = this.facade.getHeightKeeper();
77+
78+ tracker.setTrackingMode(true);
79+ keeper.setConditions(HEIGHT_LIMIT, HEIGHT_NEW);
80+
81+ Handler logHandler = null;
82+ if(LogUtils.hasLoggingPermission()){
83+ Document document = this.facade.getDocument();
84+ logHandler = new SwingLogHandler(document);
85+ }
86+ this.handler = logHandler;
5287
5388 setResizable(true);
5489 setLocationByPlatform(true);
5590 setModal(false);
5691
92+ design();
93+
5794 return;
5895 }
5996
97+
6098 /**
61- * デザインを行う。
99+ * ログ用スクロール領域を生成する。
100+ *
101+ * @param facadeArg ファサード
102+ * @return スクロール領域
62103 */
63- private void design(){
64- Container content = getContentPane();
104+ private static JScrollPane buildScrollPane(MvcFacade facadeArg){
105+ JScrollPane scrollPane = new JScrollPane();
65106
66- GridBagLayout layout = new GridBagLayout();
67- GridBagConstraints constraints = new GridBagConstraints();
68- content.setLayout(layout);
107+ JScrollBar vbar = scrollPane.getVerticalScrollBar();
108+ BoundedRangeModel rangeModel =
109+ facadeArg.getVerticalBoundedRangeModel();
110+ vbar.setModel(rangeModel);
69111
70- constraints.weightx = 1.0;
71- constraints.weighty = 1.0;
72- constraints.fill = GridBagConstraints.BOTH;
73- constraints.gridwidth = GridBagConstraints.REMAINDER;
112+ JTextArea textArea = buildTextArea(facadeArg);
113+ scrollPane.setViewportView(textArea);
74114
75- content.add(this.logPanel, constraints);
115+ return scrollPane;
116+ }
76117
77- constraints.weighty = 0.0;
78- constraints.fill = GridBagConstraints.HORIZONTAL;
79- constraints.insets = new Insets(5, 5, 5, 5);
118+ /**
119+ * ログ用テキストエリアを生成する。
120+ *
121+ * @param facadeArg ファサード
122+ * @return テキストエリア
123+ */
124+ private static JTextArea buildTextArea(MvcFacade facadeArg){
125+ JTextArea textArea = facadeArg.getTextArea();
80126
81- content.add(new JSeparator(), constraints);
127+ textArea.setEditable(false);
128+ textArea.setLineWrap(true);
129+ Monodizer.monodize(textArea);
82130
83- constraints.anchor = GridBagConstraints.WEST;
84- constraints.fill = GridBagConstraints.NONE;
85- constraints.gridwidth = 1;
86- content.add(this.clearButton, constraints);
131+ Border border = BorderFactory.createEmptyBorder(
132+ AROUND_TEXT,
133+ AROUND_TEXT,
134+ AROUND_TEXT,
135+ AROUND_TEXT
136+ );
137+ textArea.setBorder(border);
87138
88- constraints.weightx = 0.0;
89- constraints.anchor = GridBagConstraints.EAST;
90- content.add(this.closeButton, constraints);
139+ JPopupMenu popup = new TextPopup();
140+ textArea.setComponentPopupMenu(popup);
91141
92- return;
142+ return textArea;
93143 }
94144
95- /**
96- * ロギングハンドラを返す。
97- * @return ロギングハンドラ
98- */
99- public Handler getHandler(){
100- return this.logPanel.getHandler();
101- }
102145
103146 /**
104- * ログ内容をクリアする。
147+ * ボタンの各種設定。
105148 */
106- public void clearLog(){
107- this.logPanel.clearLog();
149+ private void setupButtons(){
150+ Action clearAction = this.facade.getClearAction();
151+ this.clearButton.setAction(clearAction);
152+ this.clearButton.setText("クリア");
153+
154+ this.closeButton.addActionListener(event -> {
155+ if(event.getSource() == this.closeButton){
156+ setVisible(false);
157+ }
158+ });
159+ this.closeButton.setText("閉じる");
160+
161+ JToggleButton.ToggleButtonModel toggleModel;
162+ toggleModel = this.facade.getTrackSwitchButtonModel();
163+ this.trackButton.setModel(toggleModel);
164+ this.trackButton.setText("末尾に追従");
165+
108166 return;
109167 }
110168
111-
112169 /**
113- * ボタン操作を監視する。
170+ * レイアウトデザインを行う。
114171 */
115- private final class ActionWatcher implements ActionListener{
116-
117- /**
118- * コンストラクタ。
119- */
120- ActionWatcher(){
121- super();
122- return;
123- }
172+ private void design(){
173+ Box buttonPanel = Box.createHorizontalBox();
174+
175+ buttonPanel.add(this.clearButton);
176+ buttonPanel.add(Box.createHorizontalStrut(AROUND_BUTTON));
177+ buttonPanel.add(this.trackButton);
178+ buttonPanel.add(Box.createHorizontalGlue());
179+ buttonPanel.add(this.closeButton);
180+
181+ Border border = BorderFactory.createEmptyBorder(
182+ AROUND_BUTTON,
183+ AROUND_BUTTON,
184+ AROUND_BUTTON,
185+ AROUND_BUTTON
186+ );
187+ buttonPanel.setBorder(border);
124188
125- /**
126- * {@inheritDoc}
127- * ボタン押下イベント処理。
128- * @param event {@inheritDoc}
129- */
130- @Override
131- public void actionPerformed(ActionEvent event){
132- String cmd = event.getActionCommand();
189+ Container content = getContentPane();
190+ BoxLayout layout = new BoxLayout(content, BoxLayout.Y_AXIS);
191+ content.setLayout(layout);
133192
134- if (CMD_CLEARLOG.equals(cmd)) clearLog();
135- else if(CMD_CLOSELOG.equals(cmd)) setVisible(false);
193+ content.add(this.scrollPane);
194+ content.add(buttonPanel);
136195
137- return;
138- }
196+ return;
197+ }
139198
199+ /**
200+ * ロギングハンドラを返す。
201+ *
202+ * @return ロギングハンドラ
203+ */
204+ public Handler getHandler(){
205+ return this.handler;
140206 }
141207
142208 }
--- a/src/main/java/jp/sfjp/jindolf/log/LogPanel.java
+++ /dev/null
@@ -1,231 +0,0 @@
1-/*
2- * Log panel
3- *
4- * License : The MIT License
5- * Copyright(c) 2012 olyutorskii
6- */
7-
8-package jp.sfjp.jindolf.log;
9-
10-import java.awt.Adjustable;
11-import java.awt.EventQueue;
12-import java.util.logging.Handler;
13-import javax.swing.BorderFactory;
14-import javax.swing.JPopupMenu;
15-import javax.swing.JScrollPane;
16-import javax.swing.JTextArea;
17-import javax.swing.border.Border;
18-import javax.swing.event.AncestorEvent;
19-import javax.swing.event.AncestorListener;
20-import javax.swing.event.DocumentEvent;
21-import javax.swing.event.DocumentListener;
22-import javax.swing.text.BadLocationException;
23-import javax.swing.text.Document;
24-import javax.swing.text.PlainDocument;
25-import jp.sfjp.jindolf.dxchg.TextPopup;
26-import jp.sfjp.jindolf.util.Monodizer;
27-
28-/**
29- * スクロールバー付きログ表示パネル。
30- * 垂直スクロールバーは自動的に最下部へトラックする。
31- */
32-@SuppressWarnings("serial")
33-public class LogPanel extends JScrollPane {
34-
35- private static final Document DOC_EMPTY = new PlainDocument();
36-
37-
38- private final Document document = new PlainDocument();
39- private final Handler handler;
40-
41- private final JTextArea textarea = new JTextArea();
42-
43-
44- /**
45- * コンストラクタ。
46- */
47- public LogPanel(){
48- super();
49-
50- if(LogUtils.hasLoggingPermission()){
51- this.handler = new SwingDocHandler(this.document);
52- }else{
53- this.handler = null;
54- }
55-
56- this.textarea.setDocument(DOC_EMPTY);
57- this.textarea.setEditable(false);
58- this.textarea.setLineWrap(true);
59- Monodizer.monodize(this.textarea);
60-
61- Border border = BorderFactory.createEmptyBorder(3, 3, 3, 3);
62- this.textarea.setBorder(border);
63-
64- JPopupMenu popup = new TextPopup();
65- this.textarea.setComponentPopupMenu(popup);
66-
67- setViewportView(this.textarea);
68-
69- DocumentListener docListener = new DocWatcher();
70- this.document.addDocumentListener(docListener);
71-
72- AncestorListener ancestorListener = new AncestorWatcher();
73- addAncestorListener(ancestorListener);
74-
75- return;
76- }
77-
78- /**
79- * ロギングハンドラを返す。
80- * @return ロギングハンドラ
81- */
82- public Handler getHandler(){
83- return this.handler;
84- }
85-
86- /**
87- * 垂直スクロールバーをドキュメント下端に設定し、
88- * ログの最新部を表示する。
89- * 不可視状態なら何もしない。
90- */
91- private void showLastPos(){
92- if(this.textarea.getDocument() != this.document) return;
93-
94- final Adjustable yPos = getVerticalScrollBar();
95- EventQueue.invokeLater(new Runnable(){
96- @Override
97- public void run(){
98- yPos.setValue(Integer.MAX_VALUE);
99- return;
100- }
101- });
102-
103- return;
104- }
105-
106- /**
107- * モデルとビューを連携させる。
108- * スクロール位置は末端に。
109- */
110- private void attachModel(){
111- if(this.textarea.getDocument() != this.document){
112- this.textarea.setDocument(this.document);
113- }
114- showLastPos();
115- return;
116- }
117-
118- /**
119- * モデルとビューを切り離す。
120- */
121- private void detachModel(){
122- if(this.textarea.getDocument() == DOC_EMPTY) return;
123- this.textarea.setDocument(DOC_EMPTY);
124- return;
125- }
126-
127- /**
128- * ログ内容をクリアする。
129- */
130- public void clearLog(){
131- try{
132- int docLength = this.document.getLength();
133- this.document.remove(0, docLength);
134- }catch(BadLocationException e){
135- assert false;
136- }
137- return;
138- }
139-
140-
141- /**
142- * 画面更新が必要な状態か監視し、必要に応じてモデルとビューを切り離す。
143- */
144- private final class AncestorWatcher implements AncestorListener{
145-
146- /**
147- * コンストラクタ。
148- */
149- AncestorWatcher(){
150- super();
151- return;
152- }
153-
154- /**
155- * {@inheritDoc}
156- * @param event {@inheritDoc}
157- */
158- @Override
159- public void ancestorAdded(AncestorEvent event){
160- attachModel();
161- return;
162- }
163-
164- /**
165- * {@inheritDoc}
166- * @param event {@inheritDoc}
167- */
168- @Override
169- public void ancestorRemoved(AncestorEvent event){
170- detachModel();
171- return;
172- }
173-
174- /**
175- * {@inheritDoc}
176- * @param event {@inheritDoc}
177- */
178- @Override
179- public void ancestorMoved(AncestorEvent event){
180- return;
181- }
182-
183- }
184-
185-
186- /**
187- * ドキュメント操作を監視し、スクロールバーを更新する。
188- */
189- private final class DocWatcher implements DocumentListener{
190-
191- /**
192- * コンストラクタ。
193- */
194- DocWatcher(){
195- super();
196- return;
197- }
198-
199- /**
200- * {@inheritDoc}
201- * @param event {@inheritDoc}
202- */
203- @Override
204- public void changedUpdate(DocumentEvent event){
205- showLastPos();
206- return;
207- }
208-
209- /**
210- * {@inheritDoc}
211- * @param event {@inheritDoc}
212- */
213- @Override
214- public void insertUpdate(DocumentEvent event){
215- showLastPos();
216- return;
217- }
218-
219- /**
220- * {@inheritDoc}
221- * @param event {@inheritDoc}
222- */
223- @Override
224- public void removeUpdate(DocumentEvent event){
225- showLastPos();
226- return;
227- }
228-
229- }
230-
231-}
--- a/src/main/java/jp/sfjp/jindolf/log/LogUtils.java
+++ b/src/main/java/jp/sfjp/jindolf/log/LogUtils.java
@@ -24,7 +24,7 @@ public final class LogUtils {
2424 new LoggingPermission("control", null);
2525
2626 private static final PrintStream STDERR = System.err;
27- private static final String ERRMSG_LOGSECURITY =
27+ private static final String ERRMSG_LOGPERM =
2828 "セキュリティ設定により、ログ設定を変更できませんでした";
2929
3030
@@ -38,6 +38,7 @@ public final class LogUtils {
3838
3939 /**
4040 * ログ操作のアクセス権があるか否か判定する。
41+ *
4142 * @return アクセス権があればtrue
4243 */
4344 public static boolean hasLoggingPermission(){
@@ -48,6 +49,7 @@ public final class LogUtils {
4849
4950 /**
5051 * ログ操作のアクセス権があるか否か判定する。
52+ *
5153 * @param manager セキュリティマネージャ
5254 * @return アクセス権があればtrue
5355 */
@@ -65,6 +67,7 @@ public final class LogUtils {
6567
6668 /**
6769 * ルートロガーを返す。
70+ *
6871 * @return ルートロガー
6972 */
7073 public static Logger getRootLogger(){
@@ -74,14 +77,16 @@ public final class LogUtils {
7477
7578 /**
7679 * ルートロガーの初期化を行う。
77- * ルートロガーの既存ハンドラを全解除し、
80+ *
81+ * <p>ルートロガーの既存ハンドラを全解除し、
7882 * {@link MomentaryHandler}ハンドラを登録する。
83+ *
7984 * @param useConsoleLog trueなら
8085 * {@link java.util.logging.ConsoleHandler}も追加する。
8186 */
8287 public static void initRootLogger(boolean useConsoleLog){
8388 if( ! hasLoggingPermission() ){
84- STDERR.println(ERRMSG_LOGSECURITY);
89+ STDERR.println(ERRMSG_LOGPERM);
8590 return;
8691 }
8792
@@ -105,10 +110,14 @@ public final class LogUtils {
105110
106111 /**
107112 * ルートロガーに新ハンドラを追加する。
108- * ルートロガー中の全{@link MomentaryHandler}型ハンドラに
113+ *
114+ * <p>ルートロガー中の全{@link MomentaryHandler}型ハンドラに
109115 * 蓄積されていたログは、新ハンドラに一気に転送される。
110- * {@link MomentaryHandler}型ハンドラはルートロガーから削除される。
111- * ログ操作のパーミッションがない場合、何もしない。
116+ *
117+ * <p>{@link MomentaryHandler}型ハンドラはルートロガーから削除される。
118+ *
119+ * <p>ログ操作のパーミッションがない場合、何もしない。
120+ *
112121 * @param newHandler 新ハンドラ
113122 */
114123 public static void switchHandler(Handler newHandler){
@@ -122,10 +131,10 @@ public final class LogUtils {
122131
123132 logger.addHandler(newHandler);
124133
125- for(MomentaryHandler momentaryHandler : momentaryHandlers){
134+ momentaryHandlers.forEach(momentaryHandler -> {
126135 momentaryHandler.transfer(newHandler);
127136 momentaryHandler.close();
128- }
137+ });
129138
130139 return;
131140 }
--- a/src/main/java/jp/sfjp/jindolf/log/LoggingDispatcher.java
+++ b/src/main/java/jp/sfjp/jindolf/log/LoggingDispatcher.java
@@ -18,11 +18,11 @@ import java.util.logging.Logger;
1818 */
1919 public class LoggingDispatcher extends EventQueue{
2020
21+ private static final Logger LOGGER = Logger.getAnonymousLogger();
22+
2123 private static final String FATALMSG =
2224 "イベントディスパッチ中に異常が起きました。";
2325
24- private static final Logger LOGGER = Logger.getAnonymousLogger();
25-
2626
2727 /**
2828 * コンストラクタ。
@@ -47,17 +47,20 @@ public class LoggingDispatcher extends EventQueue{
4747
4848 /**
4949 * 異常系を匿名ロガーに出力する。
50- * @param e 例外
50+ *
51+ * @param throwable 例外
5152 */
52- private static void logThrowable(Throwable e){
53- LOGGER.log(Level.SEVERE, FATALMSG, e);
53+ private static void logThrowable(Throwable throwable){
54+ LOGGER.log(Level.SEVERE, FATALMSG, throwable);
5455 return;
5556 }
5657
5758
5859 /**
5960 * {@inheritDoc}
60- * イベントディスパッチにより発生した例外を匿名ログ出力する。
61+ *
62+ * <p>イベントディスパッチにより発生した例外を匿名ログ出力する。
63+ *
6164 * @param event {@inheritDoc}
6265 */
6366 @Override
@@ -70,11 +73,11 @@ public class LoggingDispatcher extends EventQueue{
7073 }catch(Exception e){
7174 logThrowable(e);
7275 }
73- // TODO Toolkit#beep()もするべきか
74- // TODO モーダルダイアログを出すべきか
75- // TODO 標準エラー出力抑止オプションを用意すべきか
76- // TODO セキュリティバイパス
7776 return;
7877 }
7978
79+ // TODO モーダルダイアログを出すべきか
80+ // TODO 標準エラー出力抑止オプションを用意すべきか
81+ // TODO セキュリティバイパス
82+
8083 }
--- a/src/main/java/jp/sfjp/jindolf/log/MomentaryHandler.java
+++ b/src/main/java/jp/sfjp/jindolf/log/MomentaryHandler.java
@@ -7,24 +7,28 @@
77
88 package jp.sfjp.jindolf.log;
99
10+import java.util.Arrays;
1011 import java.util.Collections;
1112 import java.util.LinkedList;
1213 import java.util.List;
14+import java.util.Objects;
1315 import java.util.logging.Handler;
1416 import java.util.logging.Level;
1517 import java.util.logging.LogRecord;
1618 import java.util.logging.Logger;
19+import java.util.stream.Collectors;
1720
1821 /**
1922 * なにもしない一時的なロギングハンドラ。
20- * なにがロギングされたのかあとから一括して取得することができる。
23+ *
24+ * <p>なにがロギングされたのかあとから一括して取得することができる。
2125 *
2226 * <p>知らないうちにメモリを圧迫しないよう注意。
2327 */
2428 public class MomentaryHandler extends Handler{
2529
2630 private final List<LogRecord> logList =
27- Collections.synchronizedList(new LinkedList<LogRecord>());
31+ Collections.synchronizedList(new LinkedList<>());
2832 private final List<LogRecord> unmodList =
2933 Collections.unmodifiableList(this.logList);
3034
@@ -40,36 +44,39 @@ public class MomentaryHandler extends Handler{
4044
4145 /**
4246 * ロガーに含まれる{@link MomentaryHandler}型ハンドラのリストを返す。
47+ *
4348 * @param logger ロガー
4449 * @return {@link MomentaryHandler}型ハンドラのリスト
4550 */
4651 public static List<MomentaryHandler>
4752 getMomentaryHandlers(Logger logger){
48- List<MomentaryHandler> result = new LinkedList<>();
53+ List<MomentaryHandler> result;
4954
50- for(Handler handler : logger.getHandlers()){
51- if( ! (handler instanceof MomentaryHandler) ) continue;
52- MomentaryHandler momentaryHandler = (MomentaryHandler) handler;
53- result.add(momentaryHandler);
54- }
55+ result = Arrays.stream(logger.getHandlers())
56+ .filter(handler -> handler instanceof MomentaryHandler)
57+ .map(handler -> (MomentaryHandler) handler)
58+ .collect(Collectors.toList());
5559
5660 return result;
5761 }
5862
5963 /**
6064 * ロガーに含まれる{@link MomentaryHandler}型ハンドラを全て削除する。
65+ *
6166 * @param logger ロガー
6267 */
6368 public static void removeMomentaryHandlers(Logger logger){
64- for(MomentaryHandler handler : getMomentaryHandlers(logger)){
69+ getMomentaryHandlers(logger).forEach(handler -> {
6570 logger.removeHandler(handler);
66- }
71+ });
6772 return;
6873 }
6974
7075 /**
7176 * 蓄積されたログレコードのリストを返す。
72- * 古いログが先頭に来る。
77+ *
78+ * <p>古いログが先頭に来る。
79+ *
7380 * @return 刻一刻と成長するログレコードのリスト。変更不可。
7481 */
7582 public List<LogRecord> getRecordList(){
@@ -78,7 +85,9 @@ public class MomentaryHandler extends Handler{
7885
7986 /**
8087 * {@inheritDoc}
81- * ログを内部に溜め込む。
88+ *
89+ * <p>ログを内部に溜め込む。
90+ *
8291 * @param record {@inheritDoc}
8392 */
8493 @Override
@@ -89,6 +98,7 @@ public class MomentaryHandler extends Handler{
8998 return;
9099 }
91100
101+ // recording caller method
92102 record.getSourceMethodName();
93103
94104 this.logList.add(record);
@@ -98,7 +108,8 @@ public class MomentaryHandler extends Handler{
98108
99109 /**
100110 * {@inheritDoc}
101- * (何もしない)。
111+ *
112+ * <p>(何もしない)。
102113 */
103114 @Override
104115 public void flush(){
@@ -107,7 +118,8 @@ public class MomentaryHandler extends Handler{
107118
108119 /**
109120 * {@inheritDoc}
110- * 以降のログ出力を無視する。
121+ *
122+ * <p>以降のログ出力を無視する。
111123 */
112124 @Override
113125 public void close(){
@@ -119,19 +131,21 @@ public class MomentaryHandler extends Handler{
119131 /**
120132 * 自分自身をクローズし、
121133 * 蓄積したログを他のハンドラへまとめて出力する。
122- * 最後に蓄積されたログを解放する。
134+ *
135+ * <p>最後に蓄積されたログを解放する。
136+ *
123137 * @param handler 他のハンドラ
124138 * @throws NullPointerException 引数がnull
125139 */
126140 public void transfer(Handler handler) throws NullPointerException {
127- if(handler == null) throw new NullPointerException();
141+ Objects.nonNull(handler);
128142 if(handler == this) return;
129143
130144 close();
131145
132- for(LogRecord record : this.logList){
146+ this.logList.forEach(record -> {
133147 handler.publish(record);
134- }
148+ });
135149
136150 handler.flush();
137151 this.logList.clear();
--- a/src/main/java/jp/sfjp/jindolf/log/SwingDocHandler.java
+++ /dev/null
@@ -1,140 +0,0 @@
1-/*
2- * Logging handler for Swing text component
3- *
4- * License : The MIT License
5- * Copyright(c) 2011 olyutorskii
6- */
7-
8-package jp.sfjp.jindolf.log;
9-
10-import java.awt.EventQueue;
11-import java.util.logging.Formatter;
12-import java.util.logging.Handler;
13-import java.util.logging.Level;
14-import java.util.logging.LogRecord;
15-import java.util.logging.SimpleFormatter;
16-import javax.swing.text.AttributeSet;
17-import javax.swing.text.BadLocationException;
18-import javax.swing.text.Document;
19-
20-/**
21- * Swingテキストコンポーネント用データモデル
22- * {@link javax.swing.text.Document}
23- * に出力する{@link java.util.logging.Handler}。
24- *
25- * <p>スレッド間競合はEDTで解決される。
26- *
27- * <p>一定の文字数を超えないよう、古い記録は消去される。
28- */
29-public class SwingDocHandler extends Handler{
30-
31- private static final int DOCLIMIT = 100 * 1000; // 単位は文字
32- private static final float CHOPRATIO = 0.9f;
33- private static final int CHOPPEDLEN = (int) (DOCLIMIT * CHOPRATIO);
34-
35- static{
36- assert DOCLIMIT > CHOPPEDLEN;
37- }
38-
39-
40- private final Document document;
41-
42-
43- /**
44- * ログハンドラの生成。
45- * @param document ドキュメントモデル
46- */
47- public SwingDocHandler(Document document){
48- super();
49-
50- this.document = document;
51-
52- Formatter formatter = new SimpleFormatter();
53- setFormatter(formatter);
54-
55- return;
56- }
57-
58- /**
59- * ドキュメント末尾に文字列を追加する。
60- *
61- * <p>EDTから呼ばなければならない。
62- *
63- * @param logMessage 文字列
64- */
65- private void appendLog(String logMessage){
66- try{
67- this.document.insertString(this.document.getLength(),
68- logMessage,
69- (AttributeSet) null );
70- }catch(BadLocationException e){
71- assert false;
72- }
73- return;
74- }
75-
76- /**
77- * ドキュメント先頭部をチョップして最大長に納める。
78- *
79- * <p>EDTから呼ばなければならない。
80- */
81- private void chopHead(){
82- int docLength = this.document.getLength();
83- if(docLength <= DOCLIMIT) return;
84-
85- int offset = docLength - CHOPPEDLEN;
86- try{
87- this.document.remove(0, offset);
88- }catch(BadLocationException e){
89- assert false;
90- }
91-
92- return;
93- }
94-
95- /**
96- * {@inheritDoc}
97- * @param record {@inheritDoc}
98- */
99- @Override
100- public void publish(LogRecord record){
101- if( ! isLoggable(record) ){
102- return;
103- }
104-
105- Formatter formatter = getFormatter();
106- final String message = formatter.format(record);
107-
108- EventQueue.invokeLater(new Runnable(){
109- @Override
110- public void run(){
111- appendLog(message);
112- chopHead();
113- return;
114- }
115- });
116-
117- return;
118- }
119-
120- /**
121- * {@inheritDoc}
122- * (何もしない)。
123- */
124- @Override
125- public void flush(){
126- return;
127- }
128-
129- /**
130- * {@inheritDoc}
131- * ログ受け入れを締め切る。
132- */
133- @Override
134- public void close(){
135- setLevel(Level.OFF);
136- flush();
137- return;
138- }
139-
140-}
--- a/src/main/java/jp/sfjp/jindolf/summary/DaySummary.java
+++ b/src/main/java/jp/sfjp/jindolf/summary/DaySummary.java
@@ -10,8 +10,8 @@ package jp.sfjp.jindolf.summary;
1010 import java.awt.Color;
1111 import java.awt.Component;
1212 import java.awt.Container;
13+import java.awt.Dialog;
1314 import java.awt.Dimension;
14-import java.awt.Frame;
1515 import java.awt.GridBagConstraints;
1616 import java.awt.GridBagLayout;
1717 import java.awt.Image;
@@ -57,7 +57,7 @@ import jp.sourceforge.jindolf.corelib.TalkType;
5757 * その日ごとの集計。
5858 */
5959 @SuppressWarnings("serial")
60-public class DaySummary extends JDialog
60+public final class DaySummary extends JDialog
6161 implements WindowListener, ActionListener, ItemListener{
6262
6363 private static final NumberFormat AVERAGE_FORM;
@@ -92,11 +92,13 @@ public class DaySummary extends JDialog
9292
9393 /**
9494 * コンストラクタ。
95- * 集計結果を表示するモーダルダイアログを生成する。
96- * @param owner オーナー
95+ *
96+ * <p>集計結果を表示するモーダルダイアログを生成する。
9797 */
98- public DaySummary(Frame owner){
99- super(owner);
98+ public DaySummary(){
99+ super((Dialog)null);
100+ // We need unowned dialog
101+
100102 setModal(true);
101103
102104 GUIUtils.modifyWindowAttributes(this, true, false, true);
@@ -154,6 +156,7 @@ public class DaySummary extends JDialog
154156
155157 /**
156158 * 初期のデータモデルを生成する。
159+ *
157160 * @return データモデル
158161 */
159162 private static DefaultTableModel createInitModel(){
@@ -180,6 +183,7 @@ public class DaySummary extends JDialog
180183
181184 /**
182185 * 行を追加する。
186+ *
183187 * @param avatar アバター
184188 * @param talkCount 発言回数
185189 * @param totalChars 発言文字総数
@@ -256,6 +260,7 @@ public class DaySummary extends JDialog
256260
257261 /**
258262 * 与えられたPeriodで集計を更新する。
263+ *
259264 * @param newPeriod 日
260265 */
261266 public void summaryPeriod(Period newPeriod){
@@ -322,6 +327,7 @@ public class DaySummary extends JDialog
322327
323328 /**
324329 * {@inheritDoc}
330+ *
325331 * @param event {@inheritDoc}
326332 */
327333 @Override
@@ -331,6 +337,7 @@ public class DaySummary extends JDialog
331337
332338 /**
333339 * {@inheritDoc}
340+ *
334341 * @param event {@inheritDoc}
335342 */
336343 @Override
@@ -340,6 +347,7 @@ public class DaySummary extends JDialog
340347
341348 /**
342349 * {@inheritDoc}
350+ *
343351 * @param event {@inheritDoc}
344352 */
345353 @Override
@@ -349,6 +357,7 @@ public class DaySummary extends JDialog
349357
350358 /**
351359 * {@inheritDoc}
360+ *
352361 * @param event {@inheritDoc}
353362 */
354363 @Override
@@ -358,6 +367,7 @@ public class DaySummary extends JDialog
358367
359368 /**
360369 * {@inheritDoc}
370+ *
361371 * @param event {@inheritDoc}
362372 */
363373 @Override
@@ -367,7 +377,9 @@ public class DaySummary extends JDialog
367377
368378 /**
369379 * {@inheritDoc}
370- * ダイアログのクローズボタン押下処理を行う。
380+ *
381+ * <p>ダイアログのクローズボタン押下処理を行う。
382+ *
371383 * @param event ウィンドウ変化イベント {@inheritDoc}
372384 */
373385 @Override
@@ -378,6 +390,7 @@ public class DaySummary extends JDialog
378390
379391 /**
380392 * {@inheritDoc}
393+ *
381394 * @param event {@inheritDoc}
382395 */
383396 @Override
@@ -387,7 +400,9 @@ public class DaySummary extends JDialog
387400
388401 /**
389402 * {@inheritDoc}
390- * クローズボタン押下処理。
403+ *
404+ * <p>クローズボタン押下処理。
405+ *
391406 * @param event イベント {@inheritDoc}
392407 */
393408 @Override
@@ -399,7 +414,9 @@ public class DaySummary extends JDialog
399414
400415 /**
401416 * {@inheritDoc}
402- * コンボボックス操作処理。
417+ *
418+ * <p>コンボボックス操作処理。
419+ *
403420 * @param event イベント {@inheritDoc}
404421 */
405422 @Override
@@ -443,7 +460,9 @@ public class DaySummary extends JDialog
443460
444461 /**
445462 * {@inheritDoc}
446- * セルに{@link Avatar}がきたら顔アイコンと名前を表示する。
463+ *
464+ * <p>セルに{@link Avatar}がきたら顔アイコンと名前を表示する。
465+ *
447466 * @param value {@inheritDoc}
448467 */
449468 @Override
@@ -484,8 +503,10 @@ public class DaySummary extends JDialog
484503 }
485504
486505 /**
487- * {@inheritDoc}
488- * 統計種別によってセル色を変える。
506+ * {@inheritDoc}
507+ *
508+ * <p>統計種別によってセル色を変える。
509+ *
489510 * @param table {@inheritDoc}
490511 * @param value {@inheritDoc}
491512 * @param isSelected {@inheritDoc}
@@ -530,6 +551,7 @@ public class DaySummary extends JDialog
530551
531552 return result;
532553 }
554+
533555 }
534556
535557 }
--- a/src/main/java/jp/sfjp/jindolf/summary/VillageDigest.java
+++ b/src/main/java/jp/sfjp/jindolf/summary/VillageDigest.java
@@ -8,9 +8,9 @@
88 package jp.sfjp.jindolf.summary;
99
1010 import java.awt.Container;
11+import java.awt.Dialog;
1112 import java.awt.Dimension;
1213 import java.awt.EventQueue;
13-import java.awt.Frame;
1414 import java.awt.GridBagConstraints;
1515 import java.awt.GridBagLayout;
1616 import java.awt.Image;
@@ -60,7 +60,7 @@ import jp.sourceforge.jindolf.corelib.Team;
6060 * 決着のついた村のダイジェストを表示する。
6161 */
6262 @SuppressWarnings("serial")
63-public class VillageDigest
63+public final class VillageDigest
6464 extends JDialog
6565 implements ActionListener,
6666 ItemListener {
@@ -109,10 +109,12 @@ public class VillageDigest
109109
110110 /**
111111 * コンストラクタ。
112- * @param owner 親フレーム
113112 */
114- public VillageDigest(Frame owner){
115- super(owner);
113+ @SuppressWarnings("LeakingThisInConstructor")
114+ public VillageDigest(){
115+ super((Dialog)null);
116+ // We need unowned dialog
117+
116118 setModal(true);
117119
118120 GUIUtils.modifyWindowAttributes(this, true, false, true);
@@ -177,6 +179,7 @@ public class VillageDigest
177179
178180 /**
179181 * キャプション付き項目をコンテナに追加。
182+ *
180183 * @param container コンテナ
181184 * @param caption 項目キャプション名
182185 * @param delimiter デリミタ文字
@@ -222,6 +225,7 @@ public class VillageDigest
222225
223226 /**
224227 * キャプション付き項目をコンテナに追加。
228+ *
225229 * @param container コンテナ
226230 * @param caption 項目キャプション名
227231 * @param item 項目アイテム
@@ -235,6 +239,7 @@ public class VillageDigest
235239
236240 /**
237241 * レイアウトの最後に詰め物をする。
242+ *
238243 * @param container コンテナ
239244 */
240245 private static void addFatPad(Container container){
@@ -257,6 +262,7 @@ public class VillageDigest
257262
258263 /**
259264 * GridBagLayoutでレイアウトする空コンポーネントを生成する。
265+ *
260266 * @return 空コンポーネント
261267 */
262268 private static JComponent createGridBagComponent(){
@@ -268,6 +274,7 @@ public class VillageDigest
268274
269275 /**
270276 * 村サマリ画面の生成。
277+ *
271278 * @return 村サマリ画面
272279 */
273280 private JComponent buildSummaryPanel(){
@@ -281,6 +288,7 @@ public class VillageDigest
281288
282289 /**
283290 * プレイヤーサマリ画面の生成。
291+ *
284292 * @return プレイヤーサマリ画面
285293 */
286294 private JComponent buildPlayerPanel(){
@@ -323,6 +331,7 @@ public class VillageDigest
323331
324332 /**
325333 * キャスト表生成画面を生成する。
334+ *
326335 * @return キャスト表生成画面
327336 */
328337 private JComponent buildCastPanel(){
@@ -354,6 +363,7 @@ public class VillageDigest
354363
355364 /**
356365 * 投票Box生成画面を生成する。
366+ *
357367 * @return 投票Box生成画面
358368 */
359369 private JComponent buildVotePanel(){
@@ -376,6 +386,7 @@ public class VillageDigest
376386
377387 /**
378388 * 村詳細Wiki生成画面を生成する。
389+ *
379390 * @return 村詳細Wiki生成画面
380391 */
381392 private JComponent buildVillageWikiPanel(){
@@ -398,6 +409,7 @@ public class VillageDigest
398409
399410 /**
400411 * Wikiテキスト領域GUIの生成。
412+ *
401413 * @return Wikiテキスト領域GUI
402414 */
403415 private JComponent buildClipText(){
@@ -439,6 +451,7 @@ public class VillageDigest
439451
440452 /**
441453 * テンプレート生成画面を生成する。
454+ *
442455 * @return テンプレート生成画面
443456 */
444457 private JComponent buildClipboardPanel(){
@@ -480,6 +493,7 @@ public class VillageDigest
480493
481494 /**
482495 * 画面レイアウトを行う。
496+ *
483497 * @param container コンテナ
484498 */
485499 private void design(Container container){
@@ -533,6 +547,7 @@ public class VillageDigest
533547
534548 /**
535549 * 村を設定する。
550+ *
536551 * @param village 村
537552 */
538553 public void setVillage(Village village){
@@ -646,6 +661,7 @@ public class VillageDigest
646661
647662 /**
648663 * アクションイベントの振り分け。
664+ *
649665 * @param event アクションイベント
650666 */
651667 @Override
@@ -713,7 +729,9 @@ public class VillageDigest
713729
714730 /**
715731 * Wikiテキストをテキストボックスに出力する。
716- * スクロール位置は一番上に。
732+ *
733+ * <p>スクロール位置は一番上に。
734+ *
717735 * @param wikiText Wikiテキスト
718736 */
719737 private void putWikiText(CharSequence wikiText){
@@ -741,6 +759,7 @@ public class VillageDigest
741759
742760 /**
743761 * プレイヤーの選択操作。
762+ *
744763 * @param avatar 選択されたAvatar
745764 */
746765 private void selectPlayer(Avatar avatar){
@@ -805,6 +824,7 @@ public class VillageDigest
805824
806825 /**
807826 * 顔アイコンセットの選択操作。
827+ *
808828 * @param iconSet 顔アイコンセット
809829 */
810830 private void selectIconSet(FaceIconSet iconSet){
@@ -817,6 +837,7 @@ public class VillageDigest
817837
818838 /**
819839 * コンボボックス操作の受信。
840+ *
820841 * @param event コンボボックス操作イベント
821842 */
822843 @Override
--- a/src/main/java/jp/sfjp/jindolf/view/FilterPanel.java
+++ b/src/main/java/jp/sfjp/jindolf/view/FilterPanel.java
@@ -8,7 +8,7 @@
88 package jp.sfjp.jindolf.view;
99
1010 import java.awt.Container;
11-import java.awt.Frame;
11+import java.awt.Dialog;
1212 import java.awt.GridBagConstraints;
1313 import java.awt.GridBagLayout;
1414 import java.awt.Insets;
@@ -43,7 +43,7 @@ import jp.sourceforge.jindolf.corelib.TalkType;
4343 * 発言フィルタ GUI。
4444 */
4545 @SuppressWarnings("serial")
46-public class FilterPanel extends JDialog
46+public final class FilterPanel extends JDialog
4747 implements ActionListener, TopicFilter{
4848
4949 private static final int COLS = 4;
@@ -71,11 +71,12 @@ public class FilterPanel extends JDialog
7171
7272 /**
7373 * 発言フィルタを生成する。
74- * @param owner フレームオーナー
7574 */
7675 @SuppressWarnings("LeakingThisInConstructor")
77- public FilterPanel(Frame owner){
78- super(owner);
76+ public FilterPanel(){
77+ super((Dialog)null);
78+ // We need unowned dialog
79+
7980 setModal(false);
8081
8182 GUIUtils.modifyWindowAttributes(this, true, false, true);
@@ -109,6 +110,7 @@ public class FilterPanel extends JDialog
109110
110111 /**
111112 * レイアウトデザインを行う。
113+ *
112114 * @param topicPanel システムイベント選択
113115 * @param avatarPanel キャラ一覧
114116 * @param buttonPanel ボタン群
@@ -167,6 +169,7 @@ public class FilterPanel extends JDialog
167169
168170 /**
169171 * システムイベントチェックボックス群パネルを作成。
172+ *
170173 * @return システムイベントチェックボックス群パネル
171174 */
172175 private JComponent createTopicPanel(){
@@ -210,6 +213,7 @@ public class FilterPanel extends JDialog
210213
211214 /**
212215 * キャラ一覧チェックボックス群パネルを作成。
216+ *
213217 * @return キャラ一覧チェックボックス群パネル
214218 */
215219 private JComponent createAvatarPanel(){
@@ -248,6 +252,7 @@ public class FilterPanel extends JDialog
248252
249253 /**
250254 * ボタン群パネルを生成。
255+ *
251256 * @return ボタン群パネル
252257 */
253258 private JComponent createButtonPanel(){
@@ -276,6 +281,7 @@ public class FilterPanel extends JDialog
276281
277282 /**
278283 * 下段パネルを生成する。
284+ *
279285 * @return 下段パネル
280286 */
281287 private JComponent createBottomPanel(){
@@ -296,6 +302,7 @@ public class FilterPanel extends JDialog
296302
297303 /**
298304 * リスナを登録する。
305+ *
299306 * @param listener リスナ
300307 */
301308 public void addChangeListener(ChangeListener listener){
@@ -304,6 +311,7 @@ public class FilterPanel extends JDialog
304311
305312 /**
306313 * リスナを削除する。
314+ *
307315 * @param listener リスナ
308316 */
309317 public void removeChangeListener(ChangeListener listener){
@@ -312,6 +320,7 @@ public class FilterPanel extends JDialog
312320
313321 /**
314322 * 全リスナを取得する。
323+ *
315324 * @return リスナの配列
316325 */
317326 public ChangeListener[] getChangeListeners(){
@@ -344,8 +353,10 @@ public class FilterPanel extends JDialog
344353 }
345354
346355 /**
347- * チェックボックスまたはボタン操作時にリスナとして呼ばれる。
348356 * {@inheritDoc}
357+ *
358+ * <p>チェックボックスまたはボタン操作時にリスナとして呼ばれる。
359+ *
349360 * @param event イベント
350361 */
351362 @Override
@@ -407,6 +418,7 @@ public class FilterPanel extends JDialog
407418
408419 /**
409420 * {@inheritDoc}
421+ *
410422 * @param topic {@inheritDoc}
411423 * @return {@inheritDoc}
412424 */
@@ -462,6 +474,7 @@ public class FilterPanel extends JDialog
462474
463475 /**
464476 * {@inheritDoc}
477+ *
465478 * @return {@inheritDoc}
466479 */
467480 @Override
@@ -471,6 +484,7 @@ public class FilterPanel extends JDialog
471484
472485 /**
473486 * {@inheritDoc}
487+ *
474488 * @param context {@inheritDoc}
475489 * @return {@inheritDoc}
476490 */
@@ -520,6 +534,7 @@ public class FilterPanel extends JDialog
520534
521535 /**
522536 * {@inheritDoc}
537+ *
523538 * @return {@inheritDoc}
524539 */
525540 @Override
--- a/src/main/java/jp/sfjp/jindolf/view/FindPanel.java
+++ b/src/main/java/jp/sfjp/jindolf/view/FindPanel.java
@@ -10,7 +10,7 @@ package jp.sfjp.jindolf.view;
1010 import java.awt.BorderLayout;
1111 import java.awt.Component;
1212 import java.awt.Container;
13-import java.awt.Frame;
13+import java.awt.Dialog;
1414 import java.awt.GridBagConstraints;
1515 import java.awt.GridBagLayout;
1616 import java.awt.GridLayout;
@@ -64,7 +64,7 @@ import jp.sourceforge.jovsonz.JsValue;
6464 * 検索パネルGUI。
6565 */
6666 @SuppressWarnings("serial")
67-public class FindPanel extends JDialog
67+public final class FindPanel extends JDialog
6868 implements ActionListener,
6969 ItemListener,
7070 ChangeListener,
@@ -76,6 +76,7 @@ public class FindPanel extends JDialog
7676 private static final String LABEL_REENTER = "再入力";
7777 private static final String LABEL_IGNORE = "無視して検索をキャンセル";
7878
79+
7980 private final JComboBox<Object> findBox = new JComboBox<>();
8081 private final JButton searchButton = new JButton("検索");
8182 private final JButton clearButton = new JButton("クリア");
@@ -99,13 +100,15 @@ public class FindPanel extends JDialog
99100 private boolean canceled = false;
100101 private RegexPattern regexPattern = null;
101102
103+
102104 /**
103105 * 検索パネルを生成する。
104- * @param owner 親フレーム。
105106 */
106107 @SuppressWarnings("LeakingThisInConstructor")
107- public FindPanel(Frame owner){
108- super(owner);
108+ public FindPanel(){
109+ super((Dialog)null);
110+ // We need unowned dialog
111+
109112 setModal(true);
110113
111114 GUIUtils.modifyWindowAttributes(this, true, false, true);
@@ -145,6 +148,7 @@ public class FindPanel extends JDialog
145148 return;
146149 }
147150
151+
148152 /**
149153 * デザイン・レイアウトを行う。
150154 */
@@ -225,7 +229,9 @@ public class FindPanel extends JDialog
225229
226230 /**
227231 * {@inheritDoc}
228- * 検索ダイアログを表示・非表示する。
232+ *
233+ * <p>検索ダイアログを表示・非表示する。
234+ *
229235 * @param show 表示フラグ。真なら表示。{@inheritDoc}
230236 */
231237 @Override
@@ -238,6 +244,7 @@ public class FindPanel extends JDialog
238244
239245 /**
240246 * ダイアログが閉じられた原因を判定する。
247+ *
241248 * @return キャンセルもしくはクローズボタンでダイアログが閉じられたらtrue
242249 */
243250 public boolean isCanceled(){
@@ -246,6 +253,7 @@ public class FindPanel extends JDialog
246253
247254 /**
248255 * 一括検索が指定されたか否か返す。
256+ *
249257 * @return 一括検索が指定されたらtrue
250258 */
251259 public boolean isBulkSearch(){
@@ -254,7 +262,8 @@ public class FindPanel extends JDialog
254262
255263 /**
256264 * キャンセルボタン押下処理。
257- * このモーダルダイアログを閉じる。
265+ *
266+ * <p>このモーダルダイアログを閉じる。
258267 */
259268 private void actionCancel(){
260269 this.canceled = true;
@@ -265,7 +274,8 @@ public class FindPanel extends JDialog
265274
266275 /**
267276 * 検索ボタン押下処理。
268- * このモーダルダイアログを閉じる。
277+ *
278+ * <p>このモーダルダイアログを閉じる。
269279 */
270280 private void actionSubmit(){
271281 Object selected = this.findBox.getSelectedItem();
@@ -306,6 +316,7 @@ public class FindPanel extends JDialog
306316
307317 /**
308318 * 正規表現パターン異常系のダイアログ表示。
319+ *
309320 * @param e 正規表現構文エラー
310321 * @return 再入力が押されたらtrue。それ以外はfalse。
311322 */
@@ -356,6 +367,7 @@ public class FindPanel extends JDialog
356367
357368 /**
358369 * 現時点での検索パターンを得る。
370+ *
359371 * @return 検索パターン
360372 */
361373 public RegexPattern getRegexPattern(){
@@ -364,6 +376,7 @@ public class FindPanel extends JDialog
364376
365377 /**
366378 * 検索パターンを設定する。
379+ *
367380 * @param pattern 検索パターン
368381 */
369382 public final void setRegexPattern(RegexPattern pattern){
@@ -388,7 +401,9 @@ public class FindPanel extends JDialog
388401
389402 /**
390403 * {@inheritDoc}
391- * ボタン操作時にリスナとして呼ばれる。
404+ *
405+ * <p>ボタン操作時にリスナとして呼ばれる。
406+ *
392407 * @param event イベント {@inheritDoc}
393408 */
394409 @Override
@@ -407,7 +422,9 @@ public class FindPanel extends JDialog
407422
408423 /**
409424 * {@inheritDoc}
410- * コンボボックスのアイテム選択リスナ。
425+ *
426+ * <p>コンボボックスのアイテム選択リスナ。
427+ *
411428 * @param event アイテム選択イベント {@inheritDoc}
412429 */
413430 @Override
@@ -426,7 +443,9 @@ public class FindPanel extends JDialog
426443
427444 /**
428445 * {@inheritDoc}
429- * チェックボックス操作のリスナ。
446+ *
447+ * <p>チェックボックス操作のリスナ。
448+ *
430449 * @param event チェックボックス操作イベント {@inheritDoc}
431450 */
432451 @Override
@@ -448,7 +467,9 @@ public class FindPanel extends JDialog
448467
449468 /**
450469 * {@inheritDoc}
451- * コンボボックスのUI変更通知を受け取るリスナ。
470+ *
471+ * <p>コンボボックスのUI変更通知を受け取るリスナ。
472+ *
452473 * @param event UI差し替えイベント {@inheritDoc}
453474 */
454475 @Override
@@ -464,7 +485,9 @@ public class FindPanel extends JDialog
464485
465486 /**
466487 * コンボボックスエディタを修飾する。
467- * マージン修飾と等幅フォントをいじる。
488+ *
489+ * <p>マージン修飾と等幅フォントをいじる。
490+ *
468491 * @param editor エディタ
469492 */
470493 private void modifyComboBoxEditor(ComboBoxEditor editor){
@@ -485,6 +508,7 @@ public class FindPanel extends JDialog
485508
486509 /**
487510 * JSON形式の検索履歴情報を返す。
511+ *
488512 * @return JSON形式の検索履歴情報
489513 */
490514 public JsObject getJson(){
@@ -506,6 +530,7 @@ public class FindPanel extends JDialog
506530
507531 /**
508532 * JSON形式の検索履歴を反映させる。
533+ *
509534 * @param root JSON形式の検索履歴。nullが来たら何もしない
510535 */
511536 public void putJson(JsObject root){
@@ -530,6 +555,7 @@ public class FindPanel extends JDialog
530555
531556 /**
532557 * 起動時の履歴設定と等価か判定する。
558+ *
533559 * @param conf 比較対象
534560 * @return 等価ならtrue
535561 */
@@ -555,6 +581,7 @@ public class FindPanel extends JDialog
555581
556582 /**
557583 * {@inheritDoc}
584+ *
558585 * @param list {@inheritDoc}
559586 * @param value {@inheritDoc}
560587 * @param index {@inheritDoc}
@@ -647,6 +674,7 @@ public class FindPanel extends JDialog
647674
648675 /**
649676 * {@inheritDoc}
677+ *
650678 * @return {@inheritDoc}
651679 */
652680 @Override
@@ -656,6 +684,7 @@ public class FindPanel extends JDialog
656684
657685 /**
658686 * {@inheritDoc}
687+ *
659688 * @param item {@inheritDoc}
660689 */
661690 @Override
@@ -667,6 +696,7 @@ public class FindPanel extends JDialog
667696
668697 /**
669698 * {@inheritDoc}
699+ *
670700 * @param index {@inheritDoc}
671701 * @return {@inheritDoc}
672702 */
@@ -698,6 +728,7 @@ public class FindPanel extends JDialog
698728
699729 /**
700730 * {@inheritDoc}
731+ *
701732 * @return {@inheritDoc}
702733 */
703734 @Override
@@ -712,6 +743,7 @@ public class FindPanel extends JDialog
712743
713744 /**
714745 * {@inheritDoc}
746+ *
715747 * @param listener {@inheritDoc}
716748 */
717749 @Override
@@ -722,6 +754,7 @@ public class FindPanel extends JDialog
722754
723755 /**
724756 * {@inheritDoc}
757+ *
725758 * @param listener {@inheritDoc}
726759 */
727760 @Override
@@ -732,6 +765,7 @@ public class FindPanel extends JDialog
732765
733766 /**
734767 * 検索履歴ヒストリ追加。
768+ *
735769 * @param regexPattern 検索履歴
736770 */
737771 public void addHistory(RegexPattern regexPattern){
@@ -755,6 +789,7 @@ public class FindPanel extends JDialog
755789
756790 /**
757791 * プリセットでない検索ヒストリリストを返す。
792+ *
758793 * @return 検索ヒストリリスト
759794 */
760795 public List<RegexPattern> getOriginalHistoryList(){
--- a/src/main/java/jp/sfjp/jindolf/view/HelpFrame.java
+++ b/src/main/java/jp/sfjp/jindolf/view/HelpFrame.java
@@ -41,7 +41,7 @@ import jp.sfjp.jindolf.util.GUIUtils;
4141 * ヘルプ画面。
4242 */
4343 @SuppressWarnings("serial")
44-public class HelpFrame extends JFrame
44+public final class HelpFrame extends JFrame
4545 implements ActionListener, HyperlinkListener{
4646
4747 private static final String HELP_HTML = "resources/html/help.html";
@@ -172,7 +172,7 @@ public class HelpFrame extends JFrame
172172
173173 if(configStore.useStoreFile()){
174174 info.append("設定格納ディレクトリ : ")
175- .append(configStore.getConfigDir().getPath());
175+ .append(configStore.getConfigDir().toString());
176176 }else{
177177 info.append("※ 設定格納ディレクトリは使っていません。");
178178 }
--- a/src/main/java/jp/sfjp/jindolf/view/LandsTree.java
+++ b/src/main/java/jp/sfjp/jindolf/view/LandsTree.java
@@ -8,10 +8,7 @@
88 package jp.sfjp.jindolf.view;
99
1010 import java.awt.BorderLayout;
11-import java.awt.EventQueue;
1211 import java.awt.Insets;
13-import java.awt.event.ActionEvent;
14-import java.awt.event.ActionListener;
1512 import javax.swing.BorderFactory;
1613 import javax.swing.Icon;
1714 import javax.swing.JButton;
@@ -22,7 +19,6 @@ import javax.swing.JToolBar;
2219 import javax.swing.JTree;
2320 import javax.swing.border.Border;
2421 import javax.swing.event.TreeSelectionEvent;
25-import javax.swing.event.TreeSelectionListener;
2622 import javax.swing.tree.TreeModel;
2723 import javax.swing.tree.TreePath;
2824 import javax.swing.tree.TreeSelectionModel;
@@ -32,56 +28,93 @@ import jp.sfjp.jindolf.data.LandsTreeModel;
3228
3329 /**
3430 * 国一覧Tree周辺コンポーネント群。
31+ *
32+ * <p>昇順/降順トグルボタン、村一覧リロードボタン、
33+ * 人狼BBSサーバ国および村一覧ツリーから構成される。
3534 */
3635 @SuppressWarnings("serial")
37-public class LandsTree
38- extends JPanel
39- implements ActionListener, TreeSelectionListener{
36+public class LandsTree extends JPanel{
37+
38+ private static final String TIP_ASCEND =
39+ "押すと降順に";
40+ private static final String TIP_DESCEND =
41+ "押すと昇順に";
42+ private static final String TIP_ORDER =
43+ "選択中の国の村一覧を読み込み直す";
44+
45+ private static final String RES_DIR = "resources/image/";
46+ private static final String RES_ASCEND = RES_DIR + "tb_ascend.png";
47+ private static final String RES_DESCEND = RES_DIR + "tb_descend.png";
48+ private static final String RES_RELOAD = RES_DIR + "tb_reload.png";
4049
41- private static final String TIP_ASCEND = "押すと降順に";
42- private static final String TIP_DESCEND = "押すと昇順に";
43- private static final String TIP_ORDER = "選択中の国の村一覧を読み込み直す";
4450 private static final Icon ICON_ASCEND;
4551 private static final Icon ICON_DESCEND;
52+ private static final Icon ICON_RELOAD;
4653
4754 static{
48- ICON_ASCEND =
49- ResourceManager.getButtonIcon("resources/image/tb_ascend.png");
50- ICON_DESCEND =
51- ResourceManager.getButtonIcon("resources/image/tb_descend.png");
55+ ICON_ASCEND = ResourceManager.getButtonIcon(RES_ASCEND);
56+ ICON_DESCEND = ResourceManager.getButtonIcon(RES_DESCEND);
57+ ICON_RELOAD = ResourceManager.getButtonIcon(RES_RELOAD);
5258 }
5359
54- private final JButton orderButton = new JButton();
55- private final JButton reloadButton = new JButton();
56- private final JTree treeView = new JTree();
60+
61+ private final JButton orderButton;
62+ private final JButton reloadButton;
63+ private final JTree treeView;
5764
5865 private boolean ascending = false;
5966
67+
6068 /**
6169 * コンストラクタ。
6270 */
63- @SuppressWarnings("LeakingThisInConstructor")
6471 public LandsTree(){
6572 super();
6673
74+ this.orderButton = new JButton();
75+ this.reloadButton = new JButton();
76+ this.treeView = new JTree();
77+
78+ TreeSelectionModel selModel = this.treeView.getSelectionModel();
79+ selModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
80+
81+ setupObserver();
82+
83+ decorateButtons();
6784 design();
6885
86+ return;
87+ }
88+
89+
90+ /**
91+ * 各種監視Observerの登録。
92+ */
93+ private void setupObserver(){
94+ this.orderButton.addActionListener((ev) -> {
95+ toggleTreeOrder();
96+ });
97+
98+ this.treeView.addTreeSelectionListener((ev) -> {
99+ updateReloadButton(ev);
100+ });
101+
102+ this.reloadButton.setActionCommand(ActionManager.CMD_VILLAGELIST);
103+
104+ return;
105+ }
106+
107+ /**
108+ * ボタン群を装飾する。
109+ */
110+ private void decorateButtons(){
69111 this.orderButton.setIcon(ICON_DESCEND);
70112 this.orderButton.setToolTipText(TIP_DESCEND);
71113 this.orderButton.setMargin(new Insets(1, 1, 1, 1));
72- this.orderButton.setActionCommand(ActionManager.CMD_SWITCHORDER);
73- this.orderButton.addActionListener(this);
74114
75- Icon icon = ResourceManager
76- .getButtonIcon("resources/image/tb_reload.png");
77- this.reloadButton.setIcon(icon);
115+ this.reloadButton.setIcon(ICON_RELOAD);
78116 this.reloadButton.setToolTipText(TIP_ORDER);
79117 this.reloadButton.setMargin(new Insets(1, 1, 1, 1));
80- this.reloadButton.setActionCommand(ActionManager.CMD_VILLAGELIST);
81-
82- TreeSelectionModel selModel = this.treeView.getSelectionModel();
83- selModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
84- this.treeView.addTreeSelectionListener(this);
85118
86119 return;
87120 }
@@ -94,6 +127,7 @@ public class LandsTree
94127 setLayout(layout);
95128
96129 JToolBar toolBar = new JToolBar();
130+ toolBar.setFloatable(false);
97131 toolBar.add(this.orderButton);
98132 toolBar.add(this.reloadButton);
99133
@@ -107,6 +141,7 @@ public class LandsTree
107141
108142 /**
109143 * 国村選択ツリーコンポーネントを生成する。
144+ *
110145 * @return 国村選択ツリーコンポーネント
111146 */
112147 private JComponent createLandSelector(){
@@ -123,6 +158,7 @@ public class LandsTree
123158
124159 /**
125160 * リロードボタンを返す。
161+ *
126162 * @return リロードボタン
127163 */
128164 public JButton getReloadVillageListButton(){
@@ -131,6 +167,7 @@ public class LandsTree
131167
132168 /**
133169 * 国村選択ツリービューを返す。
170+ *
134171 * @return 国村選択ツリービュー
135172 */
136173 public JTree getTreeView(){
@@ -138,7 +175,10 @@ public class LandsTree
138175 }
139176
140177 /**
141- * 指定した国を展開する。
178+ * 指定した国をツリー展開する。
179+ *
180+ * <p>セクション一覧が国の下にツリー展開される。
181+ *
142182 * @param land 国
143183 */
144184 public void expandLand(Land land){
@@ -152,6 +192,7 @@ public class LandsTree
152192
153193 /**
154194 * 管理下のLandsTreeModelを返す。
195+ *
155196 * @return LandsTreeModel
156197 */
157198 private LandsTreeModel getLandsModel(){
@@ -164,74 +205,61 @@ public class LandsTree
164205
165206 /**
166207 * Tree表示順を反転させる。
208+ *
209+ * <p>昇順/降順ボタンも表示が切り替わる。
210+ *
211+ * <p>選択中のツリー要素があれば選択は保持される。
212+ *
167213 * @return 反転後が昇順ならtrue
168214 */
169215 private boolean toggleTreeOrder(){
170216 this.ascending = ! this.ascending;
171217
218+ String newTip;
219+ Icon newIcon;
172220 if(this.ascending){
173- this.orderButton.setToolTipText(TIP_ASCEND);
174- this.orderButton.setIcon(ICON_ASCEND);
221+ newTip = TIP_ASCEND;
222+ newIcon = ICON_ASCEND;
175223 }else{
176- this.orderButton.setToolTipText(TIP_DESCEND);
177- this.orderButton.setIcon(ICON_DESCEND);
224+ newTip = TIP_DESCEND;
225+ newIcon = ICON_DESCEND;
178226 }
227+ this.orderButton.setToolTipText(newTip);
228+ this.orderButton.setIcon(newIcon);
179229
180- final TreePath lastPath = this.treeView.getSelectionPath();
230+ TreePath lastPath = this.treeView.getSelectionPath();
181231
182232 LandsTreeModel model = getLandsModel();
183233 if(model != null){
184234 model.setAscending(this.ascending);
185235 }
186236
187- EventQueue.invokeLater(new Runnable(){
188- @Override
189- public void run(){
190- if(lastPath != null){
191- LandsTree.this.treeView.setSelectionPath(lastPath);
192- LandsTree.this.treeView.scrollPathToVisible(lastPath);
193- }
194- return;
195- }
196- });
237+ if(lastPath != null){
238+ this.treeView.setSelectionPath(lastPath);
239+ this.treeView.scrollPathToVisible(lastPath);
240+ }
197241
198242 return this.ascending;
199243 }
200244
201245 /**
202- * {@inheritDoc}
203- * ボタン押下処理。
204- * @param event ボタン押下イベント {@inheritDoc}
246+ * ツリー選択状況によってリロードボタンの状態を変更する。
247+ *
248+ * <p>国がツリー選択された状況でのみリロードボタンは有効になる。
249+ * その他の状況では無効に。
250+ *
251+ * @param event ツリー選択状況
205252 */
206- @Override
207- public void actionPerformed(ActionEvent event){
208- String cmd = event.getActionCommand();
209- if(ActionManager.CMD_SWITCHORDER.equals(cmd)){
210- toggleTreeOrder();
211- }
212- return;
213- }
253+ private void updateReloadButton(TreeSelectionEvent event){
254+ boolean reloadEnable = false;
214255
215- /**
216- * {@inheritDoc}
217- * ツリーリストで何らかの要素(国、村)がクリックされたときの処理。
218- * @param event イベント {@inheritDoc}
219- */
220- @Override
221- public void valueChanged(TreeSelectionEvent event){
222256 TreePath path = event.getNewLeadSelectionPath();
223- if(path == null){
224- this.reloadButton.setEnabled(false);
225- return;
257+ if(path != null){
258+ Object selObj = path.getLastPathComponent();
259+ if(selObj instanceof Land) reloadEnable = true;
226260 }
227261
228- Object selObj = path.getLastPathComponent();
229-
230- if( selObj instanceof Land ){
231- this.reloadButton.setEnabled(true);
232- }else{
233- this.reloadButton.setEnabled(false);
234- }
262+ this.reloadButton.setEnabled(reloadEnable);
235263
236264 return;
237265 }
--- a/src/main/java/jp/sfjp/jindolf/view/LockErrorPane.java
+++ b/src/main/java/jp/sfjp/jindolf/view/LockErrorPane.java
@@ -7,75 +7,74 @@
77
88 package jp.sfjp.jindolf.view;
99
10-import java.awt.Component;
11-import java.awt.HeadlessException;
12-import java.awt.event.ActionEvent;
13-import java.awt.event.ActionListener;
10+import java.nio.file.Path;
11+import java.text.MessageFormat;
1412 import javax.swing.ButtonGroup;
15-import javax.swing.JButton;
16-import javax.swing.JDialog;
1713 import javax.swing.JOptionPane;
1814 import javax.swing.JRadioButton;
1915 import jp.sfjp.jindolf.config.FileUtils;
20-import jp.sfjp.jindolf.config.InterVMLock;
2116
2217 /**
2318 * ロックエラー用ダイアログ。
19+ *
2420 * <ul>
25- * <li>強制解除
21+ * <li>ロックの強制解除を試行
2622 * <li>リトライ
27- * <li>設定ディレクトリを無視
23+ * <li>設定ディレクトリを無視して続行
2824 * <li>起動中止
2925 * </ul>
30- * の選択を利用者に求める。
26+ *
27+ * <p>の選択を利用者に求める。
3128 */
3229 @SuppressWarnings("serial")
33-public class LockErrorPane extends JOptionPane implements ActionListener{
34-
35- private final InterVMLock lock;
30+public final class LockErrorPane extends JOptionPane{
3631
37- private final JRadioButton continueButton =
38- new JRadioButton("設定ディレクトリを使わずに起動を続行");
39- private final JRadioButton retryButton =
40- new JRadioButton("再度ロック取得を試す");
41- private final JRadioButton forceButton =
42- new JRadioButton(
32+ private static final String FORM_MAIN =
4333 "<html>"
44- + "ロックを強制解除<br>"
34+ + "設定ディレクトリのロックファイル<br/>"
35+ + "<center>[&nbsp;{0}&nbsp;]</center>"
36+ + "<br/>"
37+ + "のロックに失敗しました。<br/>"
38+ + "考えられる原因としては、<br/>"
39+ + "<ul>"
40+ + "<li>前回起動したJindolfの終了が正しく行われなかった"
41+ + "<li>今どこかで他のJindolfが動いている"
42+ + "</ul>"
43+ + "などが考えられます。<br/>"
44+ + "<hr/>"
45+ + "</html>";
46+
47+ private static final String LABEL_CONTINUE =
48+ "設定ディレクトリを使わずに起動を続行";
49+ private static final String LABEL_RETRY =
50+ "再度ロック取得を試す";
51+ private static final String LABEL_FORCE =
52+ "<html>"
53+ + "ロックを強制解除<br/>"
4554 + " (※他のJindolfと設定ファイル書き込みが衝突するかも…)"
46- + "</html>");
55+ + "</html>";
56+
57+ private static final String LABEL_OK = "OK";
58+ private static final String LABEL_ABORT = "起動中止";
59+ private static final String[] OPTIONS = {LABEL_OK, LABEL_ABORT};
4760
48- private final JButton okButton = new JButton("OK");
49- private final JButton abortButton = new JButton("起動中止");
5061
51- private boolean aborted = false;
62+ private final JRadioButton continueButton;
63+ private final JRadioButton retryButton;
64+ private final JRadioButton forceButton;
65+
5266
5367 /**
5468 * コンストラクタ。
55- * @param lock 失敗したロック
69+ *
70+ * @param lockFile ロックに失敗したファイル
5671 */
57- @SuppressWarnings("LeakingThisInConstructor")
58- public LockErrorPane(InterVMLock lock){
72+ public LockErrorPane(Path lockFile){
5973 super();
6074
61- this.lock = lock;
62-
63- String htmlMessage =
64- "<html>"
65- + "設定ディレクトリのロックファイル<br>"
66- + "<center>[&nbsp;"
67- + FileUtils.getHtmledFileName(this.lock.getLockFile())
68- + "&nbsp;]</center>"
69- + "<br>"
70- + "のロックに失敗しました。<br>"
71- + "考えられる原因としては、<br>"
72- + "<ul>"
73- + "<li>前回起動したJindolfの終了が正しく行われなかった"
74- + "<li>今どこかで他のJindolfが動いている"
75- + "</ul>"
76- + "などが考えられます。<br>"
77- + "<hr>"
78- + "</html>";
75+ this.continueButton = new JRadioButton(LABEL_CONTINUE);
76+ this.retryButton = new JRadioButton(LABEL_RETRY);
77+ this.forceButton = new JRadioButton(LABEL_FORCE);
7978
8079 ButtonGroup bgrp = new ButtonGroup();
8180 bgrp.add(this.continueButton);
@@ -83,30 +82,40 @@ public class LockErrorPane extends JOptionPane implements ActionListener{
8382 bgrp.add(this.forceButton);
8483 this.continueButton.setSelected(true);
8584
85+ String lockName = FileUtils.getHtmledFileName(lockFile);
86+ String htmlMessage = MessageFormat.format(FORM_MAIN, lockName);
87+
8688 Object[] msg = {
8789 htmlMessage,
8890 this.continueButton,
8991 this.retryButton,
9092 this.forceButton,
9193 };
92- setMessage(msg);
93-
94- Object[] opts = {
95- this.okButton,
96- this.abortButton,
97- };
98- setOptions(opts);
9994
95+ setMessage(msg);
96+ setOptions(OPTIONS);
10097 setMessageType(JOptionPane.ERROR_MESSAGE);
10198
102- this.okButton .addActionListener(this);
103- this.abortButton.addActionListener(this);
104-
10599 return;
106100 }
107101
102+
103+ /**
104+ * 「起動中止」が選択されたか判定する。
105+ *
106+ * @param value ダイアログ結果
107+ * @return 「起動中止」が押されていたならtrue
108+ * @see JOptionPane#getValue()
109+ */
110+ public static boolean isAborted(Object value){
111+ boolean flag = LABEL_ABORT.equals(value);
112+ return flag;
113+ }
114+
115+
108116 /**
109117 * 「設定ディレクトリを無視して続行」が選択されたか判定する。
118+ *
110119 * @return 「無視して続行」が選択されていればtrue
111120 */
112121 public boolean isRadioContinue(){
@@ -115,6 +124,7 @@ public class LockErrorPane extends JOptionPane implements ActionListener{
115124
116125 /**
117126 * 「リトライ」が選択されたか判定する。
127+ *
118128 * @return 「リトライ」が選択されていればtrue
119129 */
120130 public boolean isRadioRetry(){
@@ -123,58 +133,11 @@ public class LockErrorPane extends JOptionPane implements ActionListener{
123133
124134 /**
125135 * 「強制解除」が選択されたか判定する。
136+ *
126137 * @return 「強制解除」が選択されていればtrue
127138 */
128139 public boolean isRadioForce(){
129140 return this.forceButton.isSelected();
130141 }
131142
132- /**
133- * 「起動中止」が選択されたか判定する。
134- * @return 「起動中止」が押されていたならtrue
135- */
136- public boolean isAborted(){
137- return this.aborted;
138- }
139-
140- /**
141- * {@inheritDoc}
142- * @param parentComponent {@inheritDoc}
143- * @param title {@inheritDoc}
144- * @return {@inheritDoc}
145- * @throws HeadlessException {@inheritDoc}
146- */
147- @Override
148- public JDialog createDialog(Component parentComponent,
149- String title)
150- throws HeadlessException{
151- final JDialog dialog =
152- super.createDialog(parentComponent, title);
153-
154- ActionListener listener = new ActionListener(){
155- @Override
156- public void actionPerformed(ActionEvent event){
157- dialog.setVisible(false);
158- return;
159- }
160- };
161-
162- this.okButton .addActionListener(listener);
163- this.abortButton.addActionListener(listener);
164-
165- return dialog;
166- }
167-
168- /**
169- * ボタン押下を受信する。
170- * @param event イベント
171- */
172- @Override
173- public void actionPerformed(ActionEvent event){
174- Object source = event.getSource();
175- if(source == this.okButton) this.aborted = false;
176- else this.aborted = true;
177- return;
178- }
179-
180143 }
--- a/src/main/java/jp/sfjp/jindolf/view/OptionPanel.java
+++ b/src/main/java/jp/sfjp/jindolf/view/OptionPanel.java
@@ -8,7 +8,7 @@
88 package jp.sfjp.jindolf.view;
99
1010 import java.awt.Container;
11-import java.awt.Frame;
11+import java.awt.Dialog;
1212 import java.awt.GridBagConstraints;
1313 import java.awt.GridBagLayout;
1414 import java.awt.Insets;
@@ -29,7 +29,7 @@ import jp.sfjp.jindolf.util.GUIUtils;
2929 * オプション設定パネル。
3030 */
3131 @SuppressWarnings("serial")
32-public class OptionPanel
32+public final class OptionPanel
3333 extends JDialog
3434 implements ActionListener, WindowListener{
3535
@@ -46,11 +46,12 @@ public class OptionPanel
4646
4747 /**
4848 * コンストラクタ。
49- * @param owner フレームオーナ
5049 */
5150 @SuppressWarnings("LeakingThisInConstructor")
52- public OptionPanel(Frame owner){
53- super(owner);
51+ public OptionPanel(){
52+ super((Dialog)null);
53+ // We need unowned dialog
54+
5455 setModal(true);
5556
5657 GUIUtils.modifyWindowAttributes(this, true, false, true);
@@ -76,6 +77,7 @@ public class OptionPanel
7677
7778 /**
7879 * レイアウトを行う。
80+ *
7981 * @param content コンテナ
8082 */
8183 private void design(Container content){
@@ -109,6 +111,7 @@ public class OptionPanel
109111
110112 /**
111113 * FontChooserを返す。
114+ *
112115 * @return FontChooser
113116 */
114117 public FontChooser getFontChooser(){
@@ -117,6 +120,7 @@ public class OptionPanel
117120
118121 /**
119122 * ProxyChooserを返す。
123+ *
120124 * @return ProxyChooser
121125 */
122126 public ProxyChooser getProxyChooser(){
@@ -125,6 +129,7 @@ public class OptionPanel
125129
126130 /**
127131 * DialogPrefPanelを返す。
132+ *
128133 * @return DialogPrefPanel
129134 */
130135 public DialogPrefPanel getDialogPrefPanel(){
@@ -133,7 +138,9 @@ public class OptionPanel
133138
134139 /**
135140 * ダイアログが閉じられた原因が「キャンセル」か否か判定する。
136- * ウィンドウクローズ操作は「キャンセル」扱い。
141+ *
142+ * <p>ウィンドウクローズ操作は「キャンセル」扱い。
143+ *
137144 * @return 「キャンセル」ならtrue
138145 */
139146 public boolean isCanceled(){
@@ -142,7 +149,8 @@ public class OptionPanel
142149
143150 /**
144151 * OKボタン押下処理。
145- * ダイアログを閉じる。
152+ *
153+ * <p>ダイアログを閉じる。
146154 */
147155 private void actionOk(){
148156 this.isCanceled = false;
@@ -153,7 +161,8 @@ public class OptionPanel
153161
154162 /**
155163 * キャンセルボタン押下処理。
156- * ダイアログを閉じる。
164+ *
165+ * <p>ダイアログを閉じる。
157166 */
158167 private void actionCancel(){
159168 this.isCanceled = true;
@@ -164,6 +173,7 @@ public class OptionPanel
164173
165174 /**
166175 * ボタン押下イベント受信。
176+ *
167177 * @param event イベント
168178 */
169179 @Override
@@ -176,6 +186,7 @@ public class OptionPanel
176186
177187 /**
178188 * {@inheritDoc}
189+ *
179190 * @param event {@inheritDoc}
180191 */
181192 @Override
@@ -185,8 +196,11 @@ public class OptionPanel
185196
186197 /**
187198 * {@inheritDoc}
188- * ダイアログを閉じる。
189- * キャンセルボタン押下時と同じ。
199+ *
200+ * <p>ダイアログを閉じる。
201+ *
202+ * <p>キャンセルボタン押下時と同じ。
203+ *
190204 * @param event {@inheritDoc}
191205 */
192206 @Override
@@ -197,6 +211,7 @@ public class OptionPanel
197211
198212 /**
199213 * {@inheritDoc}
214+ *
200215 * @param event {@inheritDoc}
201216 */
202217 @Override
@@ -206,6 +221,7 @@ public class OptionPanel
206221
207222 /**
208223 * {@inheritDoc}
224+ *
209225 * @param event {@inheritDoc}
210226 */
211227 @Override
@@ -215,6 +231,7 @@ public class OptionPanel
215231
216232 /**
217233 * {@inheritDoc}
234+ *
218235 * @param event {@inheritDoc}
219236 */
220237 @Override
@@ -224,6 +241,7 @@ public class OptionPanel
224241
225242 /**
226243 * {@inheritDoc}
244+ *
227245 * @param event {@inheritDoc}
228246 */
229247 @Override
@@ -233,6 +251,7 @@ public class OptionPanel
233251
234252 /**
235253 * {@inheritDoc}
254+ *
236255 * @param event {@inheritDoc}
237256 */
238257 @Override
--- a/src/main/java/jp/sfjp/jindolf/view/TopView.java
+++ b/src/main/java/jp/sfjp/jindolf/view/TopView.java
@@ -46,7 +46,7 @@ import jp.sfjp.jindolf.data.Village;
4646 * プログレスバーとフッタメッセージの管理を行う。
4747 */
4848 @SuppressWarnings("serial")
49-public class TopView extends JPanel{
49+public final class TopView extends JPanel{
5050
5151 private static final String INITCARD = "INITCARD";
5252 private static final String LANDCARD = "LANDINFO";
@@ -80,6 +80,7 @@ public class TopView extends JPanel{
8080 super();
8181
8282 this.cards = createCards();
83+
8384 JComponent split = createSplitPane(this.landsTreeView, this.cards);
8485 JComponent statusBar = createStatusBar();
8586
@@ -129,7 +130,7 @@ public class TopView extends JPanel{
129130
130131 StringBuilder warn = new StringBuilder();
131132 warn.append("※ たまにはWebブラウザでアクセスして、");
132- warn.append("<br></br>");
133+ warn.append("<br/>");
133134 warn.append("運営の動向を確かめようね!");
134135 warn.insert(0, "<font 'size=+1'>").append("</font>");
135136 warn.insert(0, "<center>").append("</center>");
--- a/src/main/java/jp/sfjp/jindolf/view/WindowManager.java
+++ b/src/main/java/jp/sfjp/jindolf/view/WindowManager.java
@@ -8,10 +8,10 @@
88 package jp.sfjp.jindolf.view;
99
1010 import java.awt.EventQueue;
11-import java.awt.Frame;
1211 import java.awt.Window;
1312 import java.util.LinkedList;
1413 import java.util.List;
14+import javax.swing.JOptionPane;
1515 import javax.swing.SwingUtilities;
1616 import javax.swing.UIManager;
1717 import javax.swing.UnsupportedLookAndFeelException;
@@ -22,6 +22,32 @@ import jp.sfjp.jindolf.summary.VillageDigest;
2222
2323 /**
2424 * ウィンドウ群の管理を行う。
25+ *
26+ * <p>原則として閉じても再利用されるウィンドウを管理対象とする。
27+ *
28+ * <p>管理対象ウィンドウは
29+ *
30+ * <ul>
31+ * <li>アプリのトップウィンドウ
32+ * <li>検索ウィンドウ
33+ * <li>フィルタウィンドウ
34+ * <li>発言集計ウィンドウ
35+ * <li>村プレイ記録のダイジェストウィンドウ
36+ * <li>オプション設定ウィンドウ
37+ * <li>ヘルプウィンドウ
38+ * <li>ログウィンドウ
39+ * </ul>
40+ *
41+ * <p>である。
42+ *
43+ * <p>トップウィンドウとヘルプウィンドウは{@link javax.swing.JFrame}、
44+ * その他は{@link javax.swing.JDialog}である。
45+ *
46+ * <p>非モーダルダイアログは、他のウィンドウの下側にも回れるのが望ましい。
47+ *
48+ * <p>各ウィンドウは、他のウィンドウの下に完全に隠れても
49+ * Windowsタスクバーなどを介して前面に引っ張り出す操作手段を
50+ * 提供することが望ましい。
2551 */
2652 public class WindowManager {
2753
@@ -40,8 +66,8 @@ public class WindowManager {
4066 private static final String TITLE_HELP =
4167 getFrameTitle("ヘルプ");
4268
43- private static final Frame NULLPARENT = null;
4469
70+ private final TopFrame topFrame;
4571
4672 private FilterPanel filterPanel;
4773 private LogFrame logFrame;
@@ -50,9 +76,8 @@ public class WindowManager {
5076 private VillageDigest villageDigest;
5177 private DaySummary daySummary;
5278 private HelpFrame helpFrame;
53- private TopFrame topFrame;
5479
55- private final List<Window> windowSet = new LinkedList<>();
80+ private final List<Window> windowSet;
5681
5782
5883 /**
@@ -60,6 +85,15 @@ public class WindowManager {
6085 */
6186 public WindowManager(){
6287 super();
88+
89+ this.topFrame = new TopFrame();
90+ this.topFrame.setVisible(false);
91+
92+ JOptionPane.setRootFrame(this.topFrame);
93+
94+ this.windowSet = new LinkedList<>();
95+ this.windowSet.add(this.topFrame);
96+
6397 return;
6498 }
6599
@@ -84,7 +118,7 @@ public class WindowManager {
84118 protected FilterPanel createFilterPanel(){
85119 FilterPanel result;
86120
87- result = new FilterPanel(NULLPARENT);
121+ result = new FilterPanel();
88122 result.setTitle(TITLE_FILTER);
89123 result.pack();
90124 result.setVisible(false);
@@ -114,7 +148,7 @@ public class WindowManager {
114148 protected LogFrame createLogFrame(){
115149 LogFrame result;
116150
117- result = new LogFrame(NULLPARENT);
151+ result = new LogFrame();
118152 result.setTitle(TITLE_LOGGER);
119153 result.pack();
120154 result.setSize(600, 500);
@@ -146,7 +180,7 @@ public class WindowManager {
146180 protected OptionPanel createOptionPanel(){
147181 OptionPanel result;
148182
149- result = new OptionPanel(NULLPARENT);
183+ result = new OptionPanel();
150184 result.setTitle(TITLE_OPTION);
151185 result.pack();
152186 result.setSize(450, 500);
@@ -177,7 +211,7 @@ public class WindowManager {
177211 protected FindPanel createFindPanel(){
178212 FindPanel result;
179213
180- result = new FindPanel(NULLPARENT);
214+ result = new FindPanel();
181215 result.setTitle(TITLE_FIND);
182216 result.pack();
183217 result.setVisible(false);
@@ -207,7 +241,7 @@ public class WindowManager {
207241 protected VillageDigest createVillageDigest(){
208242 VillageDigest result;
209243
210- result = new VillageDigest(NULLPARENT);
244+ result = new VillageDigest();
211245 result.setTitle(TITLE_DIGEST);
212246 result.pack();
213247 result.setSize(600, 550);
@@ -238,7 +272,7 @@ public class WindowManager {
238272 protected DaySummary createDaySummary(){
239273 DaySummary result;
240274
241- result = new DaySummary(NULLPARENT);
275+ result = new DaySummary();
242276 result.setTitle(TITLE_DAYSUMMARY);
243277 result.pack();
244278 result.setSize(400, 500);
@@ -293,26 +327,11 @@ public class WindowManager {
293327 }
294328
295329 /**
296- * トップフレームを生成する。
297- *
298- * @return トップフレーム
299- */
300- protected TopFrame createTopFrame(){
301- TopFrame result = new TopFrame();
302- result.setVisible(false);
303- this.windowSet.add(result);
304- return result;
305- }
306-
307- /**
308330 * トップフレームを返す。
309331 *
310332 * @return トップフレーム
311333 */
312334 public TopFrame getTopFrame(){
313- if(this.topFrame == null){
314- this.topFrame = createTopFrame();
315- }
316335 return this.topFrame;
317336 }
318337
@@ -332,12 +351,12 @@ public class WindowManager {
332351
333352 UIManager.setLookAndFeel(className);
334353
335- this.windowSet.forEach((window) -> {
354+ this.windowSet.forEach(window -> {
336355 SwingUtilities.updateComponentTreeUI(window);
337356 });
338357
339- if(this.filterPanel != null) this.filterPanel.pack();
340- if(this.findPanel != null) this.findPanel.pack();
358+ if(this.filterPanel != null) this.filterPanel.pack();
359+ if(this.findPanel != null) this.findPanel.pack();
341360
342361 return;
343362 }
--- /dev/null
+++ b/src/main/resources/jp/sfjp/jindolf/resources/README.txt
@@ -0,0 +1,6 @@
1+UTF-8 Japanese
2+このディレクトリは、Jindolfの各種設定が格納されるディレクトリです。
3+Jindolfの詳細は http://jindolf.osdn.jp/ を参照してください。
4+このディレクトリを「Jindolf」の名前で起動元JARファイルと同じ位置に
5+コピーすれば、そちらの設定が優先して使われます。
6+「lock」の名前を持つファイルはロックファイルです。
Show on old repository browser