OSDN Git Service

Merge branch 'develop' into pomconfig
authorOlyutorskii <olyutorskii@users.osdn.me>
Wed, 22 Jul 2020 03:20:00 +0000 (12:20 +0900)
committerOlyutorskii <olyutorskii@users.osdn.me>
Wed, 22 Jul 2020 03:20:00 +0000 (12:20 +0900)
32 files changed:
pom.xml
src/main/java/jp/sfjp/jindolf/BusyStatus.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/Controller.java
src/main/java/jp/sfjp/jindolf/JindolfMain.java
src/main/java/jp/sfjp/jindolf/config/AppSetting.java
src/main/java/jp/sfjp/jindolf/config/CmdOption.java
src/main/java/jp/sfjp/jindolf/config/ConfigDirUi.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/config/ConfigFile.java [deleted file]
src/main/java/jp/sfjp/jindolf/config/ConfigStore.java
src/main/java/jp/sfjp/jindolf/config/EnvInfo.java
src/main/java/jp/sfjp/jindolf/config/FileUtils.java
src/main/java/jp/sfjp/jindolf/config/JsonIo.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/config/OptionInfo.java
src/main/java/jp/sfjp/jindolf/data/Land.java
src/main/java/jp/sfjp/jindolf/data/LandsTreeModel.java
src/main/java/jp/sfjp/jindolf/log/LogFrame.java
src/main/java/jp/sfjp/jindolf/log/LogPanel.java [deleted file]
src/main/java/jp/sfjp/jindolf/log/LogUtils.java
src/main/java/jp/sfjp/jindolf/log/LoggingDispatcher.java
src/main/java/jp/sfjp/jindolf/log/MomentaryHandler.java
src/main/java/jp/sfjp/jindolf/log/SwingDocHandler.java [deleted file]
src/main/java/jp/sfjp/jindolf/summary/DaySummary.java
src/main/java/jp/sfjp/jindolf/summary/VillageDigest.java
src/main/java/jp/sfjp/jindolf/view/FilterPanel.java
src/main/java/jp/sfjp/jindolf/view/FindPanel.java
src/main/java/jp/sfjp/jindolf/view/HelpFrame.java
src/main/java/jp/sfjp/jindolf/view/LandsTree.java
src/main/java/jp/sfjp/jindolf/view/LockErrorPane.java
src/main/java/jp/sfjp/jindolf/view/OptionPanel.java
src/main/java/jp/sfjp/jindolf/view/TopView.java
src/main/java/jp/sfjp/jindolf/view/WindowManager.java
src/main/resources/jp/sfjp/jindolf/resources/README.txt [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index 4ddaec1..3aa3380 100644 (file)
--- a/pom.xml
+++ b/pom.xml
             <version>1.101.104</version>
             <scope>compile</scope>
         </dependency>
-
+        
+        <dependency>
+            <groupId>io.github.olyutorskii</groupId>
+            <artifactId>quetexj</artifactId>
+            <version>1.0.4</version>
+            <scope>compile</scope>
+        </dependency>
+        
     </dependencies>
 
     <repositories/>
diff --git a/src/main/java/jp/sfjp/jindolf/BusyStatus.java b/src/main/java/jp/sfjp/jindolf/BusyStatus.java
new file mode 100644 (file)
index 0000000..c9bd402
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+ * busy status manager
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf;
+
+import java.awt.EventQueue;
+import java.lang.reflect.InvocationTargetException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jp.sfjp.jindolf.view.TopFrame;
+import jp.sfjp.jindolf.view.TopView;
+
+/**
+ * ビジー状態の見た目を管理する。
+ *
+ * <p>ビジー状態を伴うタスクの管理も行う。
+ *
+ * <p>EDTで処理しきれない長時間タスクを実行している状況、
+ * およびその間のGUI操作が抑止される期間をビジーと呼ぶ。
+ */
+public class BusyStatus {
+
+    private static final Logger LOGGER = Logger.getAnonymousLogger();
+
+
+    private final TopFrame topFrame;
+    private final Executor executor = Executors.newCachedThreadPool();
+
+    private boolean isBusy = false;
+
+
+    /**
+     * コンストラクタ。
+     *
+     * @param topFrame TopFrameインスタンス
+     */
+    public BusyStatus(TopFrame topFrame){
+        super();
+        this.topFrame = topFrame;
+        return;
+    }
+
+
+    /**
+     * ビジー状態か否か返す。
+     *
+     * @return ビジーならtrue
+     */
+    public boolean isBusy(){
+        return this.isBusy;
+    }
+
+    /**
+     * ビジー状態を設定する。
+     *
+     * <p>ヘビーなタスク実行をアピールするために、
+     * プログレスバーとカーソルの設定を行う。
+     *
+     * <p>ビジー中のトップフレームのマウス操作、キーボード入力は
+     * 全てグラブされるため無視される。
+     *
+     * @param flag trueならプログレスバーのアニメ開始&amp;WAITカーソル。
+     * falseなら停止&amp;通常カーソル。
+     * @param msg フッタメッセージ。nullなら変更なし。
+     */
+    public void setBusy(boolean flag, String msg){
+        if(EventQueue.isDispatchThread()){
+            setBusyEdt(flag, msg);
+            return;
+        }
+
+        try{
+            EventQueue.invokeAndWait(() -> {
+                setBusyEdt(flag, msg);
+            });
+        }catch(InterruptedException | InvocationTargetException e){
+            LOGGER.log(Level.SEVERE, "ビジー処理で失敗", e);
+        }
+
+        return;
+    }
+
+    /**
+     * ビジー状態を設定する。
+     *
+     * <p>原則EDTから呼ばねばならない。
+     *
+     * @param flag trueならビジー
+     * @param msg メッセージ。nullなら変更なし。
+     */
+    public void setBusyEdt(boolean flag, String msg){
+        assert EventQueue.isDispatchThread();
+
+        this.isBusy = flag;
+
+        this.topFrame.setBusy(this.isBusy);
+
+        if(msg != null){
+            TopView topView = this.topFrame.getTopView();
+            topView.updateSysMessage(msg);
+        }
+
+        return;
+    }
+
+    /**
+     * 軽量タスクをEDTで実行する。
+     *
+     * <p>タスク実行中はビジー状態となる。
+     *
+     * <p>軽量タスク実行中はイベントループが停止するので、
+     * 入出力待ちを伴わなずに早急に終わるタスクでなければならない。
+     *
+     * @param task 軽量タスク
+     * @param beforeMsg ビジー中ステータス文字列
+     * @param afterMsg ビジー復帰時のステータス文字列
+     */
+    public void submitLightBusyTask(Runnable task,
+                                    String beforeMsg,
+                                    String afterMsg ){
+        EventQueue.invokeLater(() -> {
+            setBusy(true, beforeMsg);
+        });
+
+        EventQueue.invokeLater(task);
+
+        EventQueue.invokeLater(() -> {
+            setBusy(false, afterMsg);
+        });
+
+        return;
+    }
+
+    /**
+     * 重量級タスクをEDTとは別のスレッドで実行する。
+     *
+     * <p>タスク実行中はビジー状態となる。
+     *
+     * @param heavyTask 重量級タスク
+     * @param beforeMsg ビジー中ステータス文字列
+     * @param afterMsg ビジー復帰時のステータス文字列
+     */
+    public void submitHeavyBusyTask(final Runnable heavyTask,
+                                    final String beforeMsg,
+                                    final String afterMsg ){
+        setBusy(true, beforeMsg);
+
+        EventQueue.invokeLater(() -> {
+            fork(() -> {
+                try{
+                    heavyTask.run();
+                }finally{
+                    setBusy(false, afterMsg);
+                }
+            });
+        });
+
+        return;
+    }
+
+    /**
+     * スレッドプールを用いて非EDTなタスクを投入する。
+     *
+     * @param task タスク
+     */
+    private void fork(Runnable task){
+        this.executor.execute(task);
+        return;
+    }
+
+}
index e41dcb8..1e7de97 100644 (file)
@@ -16,12 +16,9 @@ import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
 import java.io.File;
 import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
 import java.net.URL;
 import java.text.MessageFormat;
 import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
 import java.util.logging.Handler;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -45,6 +42,7 @@ import javax.swing.filechooser.FileNameExtensionFilter;
 import javax.swing.tree.TreePath;
 import jp.sfjp.jindolf.config.AppSetting;
 import jp.sfjp.jindolf.config.ConfigStore;
+import jp.sfjp.jindolf.config.JsonIo;
 import jp.sfjp.jindolf.config.OptionInfo;
 import jp.sfjp.jindolf.data.Anchor;
 import jp.sfjp.jindolf.data.DialogPref;
@@ -120,8 +118,7 @@ public class Controller
     private final ChangeListener filterWatcher =
             new FilterWatcher();
 
-    private final Executor executor = Executors.newCachedThreadPool();
-    private volatile boolean isBusyNow;
+    private final BusyStatus busyStatus;
 
 
     /**
@@ -184,15 +181,18 @@ public class Controller
                 shutdown();
             }
         });
+        this.busyStatus = new BusyStatus(topFrame);
 
         filterPanel.addChangeListener(this.filterWatcher);
 
         Handler newHandler = logFrame.getHandler();
-        LogUtils.switchHandler(newHandler);
+        EventQueue.invokeLater(() -> {
+            LogUtils.switchHandler(newHandler);
+        });
 
-        ConfigStore config = this.appSetting.getConfigStore();
+        JsonIo jsonIo = this.appSetting.getJsonIo();
 
-        JsObject history = config.loadHistoryConfig();
+        JsObject history = jsonIo.loadHistoryConfig();
         findPanel.putJson(history);
 
         FontInfo fontInfo = this.appSetting.getFontInfo();
@@ -323,34 +323,6 @@ public class Controller
     }
 
     /**
-     * ビジー状態の設定を行う。
-     *
-     * <p>ヘビーなタスク実行をアピールするために、
-     * プログレスバーとカーソルの設定を行う。
-     *
-     * <p>ビジー中のActionコマンド受信は無視される。
-     *
-     * <p>ビジー中のトップフレームのマウス操作、キーボード入力は
-     * 全てグラブされるため無視される。
-     *
-     * @param isBusy trueならプログレスバーのアニメ開始&amp;WAITカーソル。
-     * falseなら停止&amp;通常カーソル。
-     * @param msg フッタメッセージ。nullなら変更なし。
-     */
-    private void setBusy(boolean isBusy, String msg){
-        this.isBusyNow = isBusy;
-
-        TopFrame topFrame = getTopFrame();
-
-        topFrame.setBusy(isBusy);
-        if(msg != null){
-            this.topView.updateSysMessage(msg);
-        }
-
-        return;
-    }
-
-    /**
      * ステータスバーを更新する。
      * @param message メッセージ
      */
@@ -360,91 +332,6 @@ public class Controller
     }
 
     /**
-     * ビジー状態を設定する。
-     *
-     * <p>EDT以外から呼ばれると実際の処理が次回のEDT移行に遅延される。
-     *
-     * @param isBusy ビジーならtrue
-     * @param message ステータスバー表示。nullなら変更なし
-     */
-    public void submitBusyStatus(boolean isBusy, String message){
-        Runnable task = () -> {
-            setBusy(isBusy, message);
-        };
-
-        if(EventQueue.isDispatchThread()){
-            task.run();
-        }else{
-            try{
-                EventQueue.invokeAndWait(task);
-            }catch(InvocationTargetException | InterruptedException e){
-                LOGGER.log(Level.SEVERE, "ビジー処理で失敗", e);
-            }
-        }
-
-        return;
-    }
-
-    /**
-     * 軽量タスクをEDTで実行する。
-     *
-     * <p>タスク実行中はビジー状態となる。
-     *
-     * <p>軽量タスク実行中はイベントループが停止するので、
-     * 入出力待ちを伴わなずに早急に終わるタスクでなければならない。
-     *
-     * @param task 軽量タスク
-     * @param beforeMsg ビジー中ステータス文字列
-     * @param afterMsg ビジー復帰時のステータス文字列
-     */
-    public void submitLightBusyTask(Runnable task,
-                                    String beforeMsg,
-                                    String afterMsg ){
-        submitBusyStatus(true, beforeMsg);
-        EventQueue.invokeLater(task);
-        submitBusyStatus(false, afterMsg);
-
-        return;
-    }
-
-    /**
-     * 重量級タスクをEDTとは別のスレッドで実行する。
-     *
-     * <p>タスク実行中はビジー状態となる。
-     *
-     * @param heavyTask 重量級タスク
-     * @param beforeMsg ビジー中ステータス文字列
-     * @param afterMsg ビジー復帰時のステータス文字列
-     */
-    public void submitHeavyBusyTask(final Runnable heavyTask,
-                                    final String beforeMsg,
-                                    final String afterMsg ){
-        submitBusyStatus(true, beforeMsg);
-
-        EventQueue.invokeLater(() -> {
-            fork(() -> {
-                try{
-                    heavyTask.run();
-                }finally{
-                    submitBusyStatus(false, afterMsg);
-                }
-            });
-        });
-
-        return;
-    }
-
-    /**
-     * スレッドプールを用いて非EDTなタスクを投入する。
-     *
-     * @param task タスク
-     */
-    private void fork(Runnable task){
-        this.executor.execute(task);
-        return;
-    }
-
-    /**
      * About画面を表示する。
      */
     private void actionAbout(){
@@ -454,8 +341,7 @@ public class Controller
                                            JOptionPane.DEFAULT_OPTION,
                                            GUIUtils.getLogoIcon());
 
-        JDialog dialog = pane.createDialog(getTopFrame(),
-                                           VerInfo.TITLE + "について");
+        JDialog dialog = pane.createDialog(VerInfo.TITLE + "について");
 
         dialog.pack();
         dialog.setVisible(true);
@@ -601,7 +487,7 @@ public class Controller
         String className = this.actionManager.getSelectedLookAndFeel();
         if(className == null) return;
 
-        submitLightBusyTask(
+        this.busyStatus.submitLightBusyTask(
             () -> {taskChangeLaF(className);},
             "Look&Feelを更新中…",
             "Look&Feelが更新されました"
@@ -758,7 +644,7 @@ public class Controller
             JOptionPane pane = new JOptionPane(message,
                                                JOptionPane.WARNING_MESSAGE,
                                                JOptionPane.DEFAULT_OPTION );
-            JDialog dialog = pane.createDialog(getTopFrame(), title);
+            JDialog dialog = pane.createDialog(title);
             dialog.pack();
             dialog.setVisible(true);
             dialog.dispose();
@@ -776,7 +662,7 @@ public class Controller
             });
         };
 
-        submitHeavyBusyTask(
+        this.busyStatus.submitHeavyBusyTask(
                 task,
                 "一括読み込み開始",
                 "一括読み込み完了"
@@ -862,7 +748,7 @@ public class Controller
      * 一括検索処理。
      */
     private void bulkSearch(){
-        submitHeavyBusyTask(
+        this.busyStatus.submitHeavyBusyTask(
                 () -> {taskBulkSearch();},
                 null, null
         );
@@ -1016,7 +902,7 @@ public class Controller
      * 全日程の一括ロード。
      */
     private void actionLoadAllPeriod(){
-        submitHeavyBusyTask(
+        this.busyStatus.submitHeavyBusyTask(
                 () -> {taskLoadAllPeriod();},
                 "一括読み込み開始",
                 "一括読み込み完了"
@@ -1182,7 +1068,7 @@ public class Controller
 
         };
 
-        submitHeavyBusyTask(
+        this.busyStatus.submitHeavyBusyTask(
                 task,
                 "ジャンプ先の読み込み中…",
                 null
@@ -1199,7 +1085,7 @@ public class Controller
         if(result != JFileChooser.APPROVE_OPTION) return;
         File selected = this.xmlFileChooser.getSelectedFile();
 
-        submitHeavyBusyTask(() -> {
+        this.busyStatus.submitHeavyBusyTask(() -> {
             Village village;
 
             try{
@@ -1237,7 +1123,7 @@ public class Controller
      * @param land 国
      */
     private void submitReloadVillageList(final Land land){
-        submitHeavyBusyTask(
+        this.busyStatus.submitHeavyBusyTask(
             () -> {taskReloadVillageList(land);},
             "村一覧を読み込み中…",
             "村一覧の読み込み完了"
@@ -1303,7 +1189,7 @@ public class Controller
             });
         };
 
-        submitHeavyBusyTask(
+        this.busyStatus.submitHeavyBusyTask(
                 task,
                 "会話の読み込み中",
                 "会話の表示が完了"
@@ -1362,7 +1248,7 @@ public class Controller
                                            JOptionPane.DEFAULT_OPTION );
 
         String title = VerInfo.getFrameTitle("通信異常発生");
-        JDialog dialog = pane.createDialog(getTopFrame(), title);
+        JDialog dialog = pane.createDialog(title);
 
         dialog.pack();
         dialog.setVisible(true);
@@ -1416,7 +1302,7 @@ public class Controller
             });
         };
 
-        submitHeavyBusyTask(
+        this.busyStatus.submitHeavyBusyTask(
                 task,
                 "村情報を読み込み中…",
                 "村情報の読み込み完了"
@@ -1436,7 +1322,7 @@ public class Controller
      */
     @Override
     public void actionPerformed(ActionEvent ev){
-        if(this.isBusyNow) return;
+        if(this.busyStatus.isBusy()) return;
 
         String cmd = ev.getActionCommand();
         if(cmd == null) return;
@@ -1571,7 +1457,7 @@ public class Controller
             }
         };
 
-        submitHeavyBusyTask(
+        this.busyStatus.submitHeavyBusyTask(
                 task,
                 "アンカーの展開中…",
                 null
@@ -1584,12 +1470,12 @@ public class Controller
      * アプリ正常終了処理。
      */
     private void shutdown(){
-        ConfigStore configStore = this.appSetting.getConfigStore();
+        JsonIo jsonIo = this.appSetting.getJsonIo();
 
         FindPanel findPanel = this.windowManager.getFindPanel();
         JsObject findConf = findPanel.getJson();
         if( ! findPanel.hasConfChanged(findConf) ){
-            configStore.saveHistoryConfig(findConf);
+            jsonIo.saveHistoryConfig(findConf);
         }
 
         this.appSetting.saveConfig();
index e672c2c..ad9682e 100644 (file)
@@ -12,14 +12,17 @@ import java.awt.EventQueue;
 import java.io.PrintStream;
 import java.lang.reflect.InvocationTargetException;
 import java.text.MessageFormat;
+import java.util.Locale;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import javax.swing.JFrame;
 import javax.swing.UIManager;
 import jp.sfjp.jindolf.config.AppSetting;
 import jp.sfjp.jindolf.config.CmdOption;
+import jp.sfjp.jindolf.config.ConfigDirUi;
 import jp.sfjp.jindolf.config.ConfigStore;
 import jp.sfjp.jindolf.config.EnvInfo;
+import jp.sfjp.jindolf.config.FileUtils;
 import jp.sfjp.jindolf.config.OptionInfo;
 import jp.sfjp.jindolf.data.LandsTreeModel;
 import jp.sfjp.jindolf.log.LogUtils;
@@ -103,9 +106,8 @@ public final class JindolfMain {
 
     /**
      * 起動時の諸々の情報をログ出力する。
-     * @param appSetting  アプリ設定
      */
-    private static void dumpBootInfo(AppSetting appSetting){
+    private static void logBootInfo(){
         Object[] logArgs;
 
         logArgs = new Object[]{
@@ -125,28 +127,59 @@ public final class JindolfMain {
         };
         LOGGER.log(Level.INFO, LOG_HEAP, logArgs);
 
-        OptionInfo optinfo = appSetting.getOptionInfo();
+        if(FileUtils.isWindowsOSFs()){
+            LOGGER.info("Windows環境と認識されました。");
+        }
+
+        if(FileUtils.isMacOSXFs()){
+            LOGGER.info("macOS環境と認識されました。");
+        }
+
+        Locale locale = Locale.getDefault();
+        String localeTxt = locale.toString();
+        LOGGER.log(Level.INFO, "ロケールに{0}が用いられます。", localeTxt);
+
+        return;
+    }
+
+    /**
+     * 起動時の諸々の情報をログ出力する。
+     *
+     * @param optinfo コマンドラインオプション
+     */
+    private static void logBootInfo(OptionInfo optinfo){
         StringBuilder bootArgs = new StringBuilder();
+
         bootArgs.append("\n\n").append("起動時引数:\n");
-        for(String arg : optinfo.getInvokeArgList()){
-            bootArgs.append("\u0020\u0020").append(arg).append('\n');
-        }
+        optinfo.getInvokeArgList().forEach(arg ->
+            bootArgs.append("\u0020\u0020").append(arg).append('\n')
+        );
         bootArgs.append('\n');
+
         bootArgs.append(EnvInfo.getVMInfo());
+
         LOGGER.info(bootArgs.toString());
 
-        ConfigStore configStore = appSetting.getConfigStore();
+        return;
+    }
+
+    /**
+     * 起動時の諸々の情報をログ出力する。
+     *
+     * @param configStore  設定ディレクトリ情報
+     */
+    private static void logBootInfo(ConfigStore configStore){
         if(configStore.useStoreFile()){
             LOGGER.log(Level.INFO, LOG_CONF, configStore.getConfigDir());
         }else{
             LOGGER.info(LOG_NOCONF);
         }
-
         return;
     }
 
     /**
      * JindolfMain のスタートアップエントリ。
+     *
      * @param args コマンドライン引数
      * @return 起動に成功すれば0。失敗したら0以外。
      */
@@ -175,6 +208,7 @@ public final class JindolfMain {
 
     /**
      * JindolfMain のスタートアップエントリ。
+     *
      * @param optinfo コマンドライン引数情報
      * @return 起動に成功すれば0。失敗したら0以外。
      */
@@ -229,44 +263,26 @@ public final class JindolfMain {
         LogUtils.initRootLogger(optinfo.hasOption(CmdOption.OPT_CONSOLELOG));
         // ここからロギング解禁
 
-        final AppSetting appSetting = new AppSetting(optinfo);
-        dumpBootInfo(appSetting);
+        logBootInfo();
+        logBootInfo(optinfo);
 
+        final AppSetting appSetting = new AppSetting(optinfo);
         ConfigStore configStore = appSetting.getConfigStore();
-        configStore.prepareConfigDir();
-        configStore.tryLock();
+        logBootInfo(configStore);
+
+        ConfigDirUi.prepareConfigDir(configStore);
+        ConfigDirUi.tryLock(configStore);
         // ここから設定格納ディレクトリ解禁
 
         appSetting.loadConfig();
 
-        final Runtime runtime = Runtime.getRuntime();
-        runtime.addShutdownHook(new Thread(){
-            /** {@inheritDoc} */
-            @Override
-            @SuppressWarnings("CallToThreadYield")
-            public void run(){
-                LOGGER.info("シャットダウン処理に入ります…");
-                flush();
-                runtime.gc();
-                Thread.yield();
-                runtime.runFinalization(); // 危険?
-                Thread.yield();
-                return;
-            }
-        });
-
         LoggingDispatcher.replaceEventQueue();
 
         int exitCode = 0;
         try{
-            EventQueue.invokeAndWait(new Runnable(){
-                /** {@inheritDoc} */
-                @Override
-                public void run(){
-                    startGUI(appSetting);
-                    return;
-                }
-            });
+            EventQueue.invokeAndWait(() ->
+                startGUI(appSetting)
+            );
         }catch(InvocationTargetException | InterruptedException e){
             LOGGER.log(Level.SEVERE, FATALMSG_INITFAIL, e);
             e.printStackTrace(STDERR);
@@ -278,35 +294,38 @@ public final class JindolfMain {
 
     /**
      * AWTイベントディスパッチスレッド版スタートアップエントリ。
+     *
      * @param appSetting アプリ設定
      */
     private static void startGUI(AppSetting appSetting){
         JFrame topFrame = buildMVC(appSetting);
-
         GUIUtils.modifyWindowAttributes(topFrame, true, false, true);
-
         topFrame.pack();
 
-        Dimension initGeometry =
-                new Dimension(appSetting.initialFrameWidth(),
-                              appSetting.initialFrameHeight());
+        int frameWidth  = appSetting.initialFrameWidth();
+        int frameHeight = appSetting.initialFrameHeight();
+        Dimension initGeometry = new Dimension(frameWidth, frameHeight);
         topFrame.setSize(initGeometry);
 
-        if(    appSetting.initialFrameXpos() <= Integer.MIN_VALUE
-            || appSetting.initialFrameYpos() <= Integer.MIN_VALUE ){
+        int frameXpos = appSetting.initialFrameXpos();
+        int frameYpos = appSetting.initialFrameYpos();
+
+        if(    frameXpos <= Integer.MIN_VALUE
+            || frameYpos <= Integer.MIN_VALUE ){
             topFrame.setLocationByPlatform(true);
         }else{
-            topFrame.setLocation(appSetting.initialFrameXpos(),
-                                 appSetting.initialFrameYpos() );
+            topFrame.setLocation(frameXpos, frameYpos);
         }
 
         topFrame.setVisible(true);
 
+        // GOOD BYE BUT EVENT-LOOP WILL CONTINUE
         return;
     }
 
     /**
      * モデル・ビュー・コントローラの結合。
+     *
      * @param appSetting アプリ設定
      * @return アプリケーションのトップフレーム
      */
@@ -315,8 +334,6 @@ public final class JindolfMain {
         WindowManager windowManager = new WindowManager();
         ActionManager actionManager = new ActionManager();
 
-        model.loadLandList();
-
         Controller controller = new Controller(model,
                                                windowManager,
                                                actionManager,
index 8ffab6d..87bff85 100644 (file)
@@ -59,6 +59,7 @@ public class AppSetting{
 
     private final OptionInfo optInfo;
     private final ConfigStore configStore;
+    private final JsonIo jsonIo;
     private final Rectangle frameRect;
 
     private FontInfo fontInfo;
@@ -83,6 +84,7 @@ public class AppSetting{
 
         this.optInfo = info;
         this.configStore = parseConfigStore(this.optInfo);
+        this.jsonIo = new JsonIo(this.configStore);
         this.frameRect = parseGeometrySetting(this.optInfo);
 
         return;
@@ -97,29 +99,27 @@ public class AppSetting{
     private static ConfigStore parseConfigStore(OptionInfo option){
         CmdOption opt = option.getExclusiveOption(CmdOption.OPT_CONFDIR,
                                                   CmdOption.OPT_NOCONF );
-
-        boolean useConfig;
-        boolean isImplicitPath;
-        File configPath;
-
-        if(opt == CmdOption.OPT_NOCONF){
-            useConfig = false;
-            isImplicitPath = true;
-            configPath = null;
-        }else if(opt == CmdOption.OPT_CONFDIR){
-            useConfig = true;
-            isImplicitPath = false;
-            String optPath = option.getStringArg(opt);
-            configPath = FileUtils.supplyFullPath(new File(optPath));
+        ConfigStore result;
+        if(opt == null){
+            result = new ConfigStore(null);
         }else{
-            useConfig = true;
-            isImplicitPath = true;
-            configPath = ConfigFile.getImplicitConfigDirectory();
+            switch(opt){
+            case OPT_NOCONF:
+                result = new ConfigStore();
+                break;
+            case OPT_CONFDIR:
+                String optArg = option.getStringArg(opt);
+                Path configPath = Paths.get(optArg);
+                configPath = configPath.toAbsolutePath();
+                result = new ConfigStore(configPath);
+                break;
+            default:
+                result = null;
+                assert false;
+                break;
+            }
         }
 
-        ConfigStore result =
-                new ConfigStore(useConfig, isImplicitPath, configPath);
-
         return result;
     }
 
@@ -203,6 +203,15 @@ public class AppSetting{
     }
 
     /**
+     * JSON入出力設定を返す。
+     *
+     * @return 入出力設定
+     */
+    public JsonIo getJsonIo(){
+        return this.jsonIo;
+    }
+
+    /**
      * 初期のフレーム幅を返す。
      * @return 初期のフレーム幅
      */
@@ -300,7 +309,7 @@ public class AppSetting{
      * ネットワーク設定をロードする。
      */
     private void loadNetConfig(){
-        JsObject root = this.configStore.loadNetConfig();
+        JsObject root = this.jsonIo.loadNetConfig();
         if(root == null) return;
         this.loadedNetConfig = root;
 
@@ -319,7 +328,7 @@ public class AppSetting{
      * 会話表示設定をロードする。
      */
     private void loadTalkConfig(){
-        JsObject root = this.configStore.loadTalkConfig();
+        JsObject root = this.jsonIo.loadTalkConfig();
         if(root == null) return;
         this.loadedTalkConfig = root;
 
@@ -411,7 +420,7 @@ public class AppSetting{
      * ローカル画像設定をロードする。
      */
     private void loadLocalImageConfig(){
-        JsObject root = this.configStore.loadLocalImgConfig();
+        JsObject root = this.jsonIo.loadLocalImgConfig();
         if(root == null) return;
 
         JsValue faceConfig = root.getValue("avatarFace");
@@ -482,7 +491,7 @@ public class AppSetting{
             if(this.loadedNetConfig.equals(root)) return;
         }
 
-        this.configStore.saveNetConfig(root);
+        this.jsonIo.saveNetConfig(root);
 
         return;
     }
@@ -516,7 +525,7 @@ public class AppSetting{
             if(this.loadedTalkConfig.equals(root)) return;
         }
 
-        this.configStore.saveTalkConfig(root);
+        this.jsonIo.saveTalkConfig(root);
 
         return;
     }
index ddc59c7..1a5fc83 100644 (file)
@@ -15,6 +15,10 @@ import jp.sfjp.jindolf.ResourceManager;
 
 /**
  * コマンドラインオプションの列挙。
+ *
+ * <p>1オプションは複数の別名を持ちうる。
+ *
+ * <p>1引数を持つオプションと持たないオプションは区別される。
  */
 public enum CmdOption {
 
@@ -58,7 +62,8 @@ public enum CmdOption {
             OPT_FRACTIONAL
             );
 
-    private static final String RES_HELPTEXT = "resources/help.txt";
+    private static final String RES_DIR = "resources";
+    private static final String RES_HELPTEXT = RES_DIR + "/help.txt";
 
 
     private final List<String> nameList;
@@ -66,6 +71,7 @@ public enum CmdOption {
 
     /**
      * コンストラクタ。
+     *
      * @param names オプション名の一覧
      */
     private CmdOption(String ... names){
@@ -77,6 +83,7 @@ public enum CmdOption {
 
     /**
      * ヘルプメッセージ(オプションの説明)を返す。
+     *
      * @return ヘルプメッセージ
      */
     public static String getHelpText(){
@@ -86,6 +93,7 @@ public enum CmdOption {
 
     /**
      * オプション名に合致するEnumを返す。
+     *
      * @param arg 個別のコマンドライン引数
      * @return 合致したEnum。どれとも合致しなければnull
      */
@@ -96,40 +104,43 @@ public enum CmdOption {
         return null;
     }
 
+
     /**
      * 任意のオプション文字列がこのオプションに合致するか判定する。
+     *
      * @param option ハイフンの付いたオプション文字列
      * @return 合致すればtrue
      */
     public boolean matches(String option){
-        for(String name : this.nameList){
-            if(option.equals(name)) return true;
-        }
-
-        return false;
+        boolean result = this.nameList.contains(option);
+        return result;
     }
 
     /**
      * 単体で意味をなすオプションか判定する。
+     *
      * @return 単体で意味をなすならtrue
      */
     public boolean isIndepOption(){
-        if(OPTS_INDEPENDENT.contains(this)) return true;
-        return false;
+        boolean result = OPTS_INDEPENDENT.contains(this);
+        return result;
     }
 
     /**
      * 真偽指定を一つ必要とするオプションか判定する。
+     *
      * @return 真偽指定を一つ必要とするオプションならtrue
      */
     public boolean isBooleanOption(){
-        if(OPTS_BOOLEAN.contains(this)) return true;
-        return false;
+        boolean result = OPTS_BOOLEAN.contains(this);
+        return result;
     }
 
     /**
      * オプション名を返す。
-     * オプション別名が複数指定されている場合は最初のオプション名
+     *
+     * <p>オプション別名が複数指定されている場合は最初のオプション名
+     *
      * @return オプション名
      */
     @Override
diff --git a/src/main/java/jp/sfjp/jindolf/config/ConfigDirUi.java b/src/main/java/jp/sfjp/jindolf/config/ConfigDirUi.java
new file mode 100644 (file)
index 0000000..dbf04d8
--- /dev/null
@@ -0,0 +1,526 @@
+/*
+ * configuration file & directory UI
+ *
+ * License : The MIT License
+ * Copyright(c) 2009 olyutorskii
+ */
+
+package jp.sfjp.jindolf.config;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.MessageFormat;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.JDialog;
+import javax.swing.JOptionPane;
+import jp.sfjp.jindolf.ResourceManager;
+import jp.sfjp.jindolf.VerInfo;
+import jp.sfjp.jindolf.view.LockErrorPane;
+
+/**
+ * ユーザとのインタラクションを伴う、
+ * 設定格納ディレクトリに関する各種操作。
+ *
+ * <p>ディレクトリ作成やロック確保に伴う、
+ * 各種異常系に対する判断をユーザーに求める。
+ *
+ * <p>場合によってはそのままVMごと終了し、呼び出し元に制御を返さない。
+ *
+ * <p>ユーザとの各種インタラクションは、
+ * アプリのメインウィンドウが表示される前に行われる。
+ */
+public final class ConfigDirUi{
+
+    private static final Logger LOGGER = Logger.getAnonymousLogger();
+
+    private static final String TITLE_BUILDCONF =
+            VerInfo.TITLE + "設定格納ディレクトリの設定";
+
+    private static final Path FILE_README = Paths.get("README.txt");
+    private static final Path FILE_AVATARJSON = Paths.get("avatarCache.json");
+
+    private static final String RES_DIR = "resources";
+    private static final String RES_README = RES_DIR + "/README.txt";
+    private static final String RES_IMGDIR = RES_DIR + "/image";
+    private static final String RES_AVATARJSON =
+            RES_IMGDIR + "/avatarCache.json";
+
+    private static final String MSG_POST =
+            "<ul>"
+            + "<li><code>" + CmdOption.OPT_CONFDIR + "</code>"
+            + "&nbsp;オプション指定により、<br/>"
+            + "任意の設定格納ディレクトリを指定することができます。<br/>"
+            + "<li><code>" + CmdOption.OPT_NOCONF + "</code>"
+            + "&nbsp;オプション指定により、<br/>"
+            + "設定格納ディレクトリを使わずに起動することができます。<br/>"
+            + "</ul>";
+    private static final String MSG_NOCONF =
+            "<html>"
+            + "設定ディレクトリを使わずに起動を続行します。<br/>"
+            + "今回、各種設定の読み込み・保存はできません。<br/>"
+            + "<code>"
+            + CmdOption.OPT_NOCONF
+            + "</code> オプション"
+            + "を使うとこの警告は出なくなります。"
+            + "</html>";
+    private static final String MSG_ABORT =
+            "<html>"
+            + "設定ディレクトリの作成をせずに起動を中止します。<br/>"
+            + MSG_POST
+            + "</html>";
+    private static final String FORM_FAILRM =
+            "<html>"
+            + "ロックファイルの強制解除に失敗しました。<br/>"
+            + "他に動いているJindolf"
+            + "が見つからないのであれば、<br/>"
+            + "なんとかしてロックファイル<br/>"
+            + "{0}"
+            + "を削除してください。<br/>"
+            + "起動を中止します。"
+            + "</html>";
+    private static final String FORM_ILLLOCK =
+            "<html>"
+            + "ロックファイル<br/>"
+            + "{0}"
+            + "を確保することができません。<br/>"
+            + "起動を中止します。"
+            + "</html>";
+    private static final String FORM_MKDIRFAIL =
+            "<html>"
+            + "ディレクトリ<br/>"
+            + "{0}"
+            + "の作成に失敗しました。"
+            + "起動を中止します。<br/>"
+            + MSG_POST
+            + "</html>";
+    private static final String FORM_ACCERR =
+            "<html>"
+            + "ディレクトリ<br/>"
+            + "{0}"
+            + "へのアクセスができません。"
+            + "起動を中止します。<br/>"
+            + "このディレクトリへのアクセス権を調整し"
+            + "読み書きできるようにしてください。<br/>"
+            + MSG_POST
+            + "</html>";
+    private static final String FORM_WRITEERR =
+            "<html>"
+            + "ファイル<br/>"
+            + "{0}"
+            + "への書き込みができません。"
+            + "起動を中止します。<br/>"
+            + "</html>";
+    private static final String FORM_MKCONF =
+            "<html>"
+            + "設定ファイル格納ディレクトリ<br/>"
+            + "{0}を作成します。<br/>"
+            + "このディレクトリを今から作成して構いませんか?<br/>"
+            + "このディレクトリ名は、後からいつでもヘルプウィンドウで<br/>"
+            + "確認することができます。"
+            + "</html>";
+
+    private static final String LOG_MKDIRERR =
+            "ディレクトリ{0}を生成できません";
+    private static final String LOG_CREATEERR =
+            "ファイル{0}を生成できません";
+    private static final String LOG_RESCPY =
+            "内部リソースから{0}へコピーが行われました。";
+
+    private static final int ERR_ABORT = 1;
+
+
+    /**
+     * 隠れコンストラクタ。
+     */
+    private ConfigDirUi(){
+        assert false;
+    }
+
+
+    /**
+     * VMごとアプリを異常終了させる。
+     *
+     * <p>終了コードは1。
+     */
+    private static void abort(){
+        System.exit(ERR_ABORT);
+        assert false;
+        return;
+    }
+
+    /**
+     * ダイアログを表示し、閉じられるまで待つ。
+     *
+     * @param pane ダイアログの元となるペイン
+     */
+    private static void showDialog(JOptionPane pane){
+        JDialog dialog = pane.createDialog(TITLE_BUILDCONF);
+        dialog.setResizable(true);
+        dialog.pack();
+
+        dialog.setVisible(true);
+        dialog.dispose();
+
+        return;
+    }
+
+    /**
+     * 設定ディレクトリ操作の
+     * 共通エラーメッセージ確認ダイアログを表示する。
+     *
+     * <p>閉じるまで待つ。
+     *
+     * @param txt メッセージ
+     */
+    private static void showErrorMessage(String txt){
+        JOptionPane pane = new JOptionPane(
+                txt, JOptionPane.ERROR_MESSAGE);
+        showDialog(pane);
+        return;
+    }
+
+    /**
+     * センタリングされたファイル名表示のHTML表記を出力する。
+     *
+     * @param path ファイル
+     * @return HTML表記
+     */
+    private static String getCenteredFileName(Path path){
+        String form = "<center>[&nbsp;{0}&nbsp;]</center><br/>";
+        String fileName = FileUtils.getHtmledFileName(path);
+        String result = MessageFormat.format(form, fileName);
+        return result;
+    }
+
+    /**
+     * ディレクトリが生成できないエラーをダイアログで提示し、
+     * VM終了する。
+     *
+     * @param path 生成できなかったディレクトリ
+     */
+    private static void abortCantBuildDir(Path path){
+        String fileName = getCenteredFileName(path);
+        String msg = MessageFormat.format(FORM_MKDIRFAIL, fileName);
+
+        showErrorMessage(msg);
+        abort();
+        assert false;
+
+        return;
+    }
+
+    /**
+     * ディレクトリへアクセスできないエラーをダイアログで提示し、
+     * VM終了する。
+     *
+     * @param path アクセスできないディレクトリ
+     */
+    private static void abortCantAccessDir(Path path){
+        String fileName = getCenteredFileName(path);
+        String msg = MessageFormat.format(FORM_ACCERR, fileName);
+
+        showErrorMessage(msg);
+        abort();
+        assert false;
+
+        return;
+    }
+
+    /**
+     * ディレクトリが生成できない異常系をログ出力する。
+     *
+     * @param dirPath 生成できなかったディレクトリ
+     * @param cause 異常系原因
+     */
+    private static void logMkdirErr(Path dirPath, Throwable cause){
+        String pathTxt = dirPath.toString();
+        String msg = MessageFormat.format(LOG_MKDIRERR, pathTxt);
+        LOGGER.log(Level.SEVERE, msg, cause);
+        return;
+    }
+
+    /**
+     * リソースからローカルファイルへコピーする。
+     *
+     * @param resource リソース名
+     * @param dest ローカルファイル
+     */
+    private static void copyResource(String resource, Path dest){
+        try(InputStream ris =
+                ResourceManager.getResourceAsStream(resource)){
+            InputStream is = new BufferedInputStream(ris);
+            Files.copy(is, dest);
+        }catch(IOException | SecurityException e){
+            String destName = dest.toString();
+            String logMsg = MessageFormat.format(LOG_CREATEERR, destName);
+            LOGGER.log(Level.SEVERE, logMsg, e);
+
+            String destHtml = getCenteredFileName(dest);
+            String diagMsg = MessageFormat.format(FORM_WRITEERR, destHtml);
+            showErrorMessage(diagMsg);
+            abort();
+
+            assert false;
+        }
+
+        String destName = dest.toString();
+        String msg = MessageFormat.format(LOG_RESCPY, destName);
+        LOGGER.info(msg);
+
+        return;
+    }
+
+    /**
+     * 設定ディレクトリがアクセス可能でなければ
+     * エラーダイアログを出してVM終了する。
+     *
+     * @param confDir 設定ディレクトリ
+     */
+    private static void checkDirPerm(Path confDir){
+        if( ! FileUtils.isAccessibleDirectory(confDir) ){
+            abortCantAccessDir(confDir);
+        }
+
+        return;
+    }
+
+    /**
+     * 設定ディレクトリの存在を確認し、なければ作る。
+     *
+     * <p>設定ディレクトリを使わない場合は何もしない。
+     *
+     * @param configStore 設定ディレクトリ情報
+     */
+    public static void prepareConfigDir(ConfigStore configStore){
+        if( ! configStore.useStoreFile() ) return;
+
+        Path conf = configStore.getConfigDir();
+        if(Files.exists(conf)){
+            checkDirPerm(conf);
+        }else{
+            buildConfDirPath(conf);
+        }
+
+        Path imgDir = configStore.getLocalImgDir();
+        if(Files.exists(imgDir)){
+            checkDirPerm(imgDir);
+        }else{
+            buildImageCacheDir(imgDir);
+        }
+
+        return;
+    }
+
+    /**
+     * まだ存在しない設定格納ディレクトリを新規に作成する。
+     *
+     * <p>エラーがあればダイアログ提示とともにVM終了する。
+     *
+     * <p>既に存在すればなにもしない。
+     *
+     * @param confPath 設定格納ディレクトリ
+     * @return 新規に作成した設定格納ディレクトリの絶対パス
+     */
+    private static Path buildConfDirPath(Path confPath){
+        assert confPath.isAbsolute();
+        if(Files.exists(confPath)) return confPath;
+
+        boolean confirmed = confirmBuildConfigDir(confPath);
+        if( ! confirmed ){
+            JOptionPane pane = new JOptionPane(
+                    MSG_ABORT, JOptionPane.WARNING_MESSAGE);
+            showDialog(pane);
+            abort();
+            assert false;
+        }
+
+        try{
+            Files.createDirectories(confPath);
+        }catch(IOException | SecurityException e){
+            logMkdirErr(confPath, e);
+            abortCantBuildDir(confPath);
+            assert false;
+        }
+
+        // FileUtils.setOwnerOnlyAccess(absPath);
+
+        checkDirPerm(confPath);
+
+        Path readme = confPath.resolve(FILE_README);
+        copyResource(RES_README, readme);
+
+        return confPath;
+    }
+
+    /**
+     * 設定ディレクトリを新規に生成してよいかダイアログで問い合わせる。
+     *
+     * @param confDir 設定ディレクトリ
+     * @return 生成してよいと指示があればtrue
+     */
+    private static boolean confirmBuildConfigDir(Path confDir){
+        String confName = getCenteredFileName(confDir);
+        String msg = MessageFormat.format(FORM_MKCONF, confName);
+
+        JOptionPane pane;
+        pane = new JOptionPane(msg,
+                               JOptionPane.QUESTION_MESSAGE,
+                               JOptionPane.YES_NO_OPTION);
+        showDialog(pane);
+
+        Object val = pane.getValue();
+        if( ! (val instanceof Integer) ) return false;
+        int ival = (int) val;
+        boolean result = ival == JOptionPane.YES_OPTION;
+
+        return result;
+    }
+
+    /**
+     * ローカル画像キャッシュディレクトリを作る。
+     *
+     * <p>作られたディレクトリ内に
+     * ファイルavatarCache.jsonが作られる。
+     *
+     * @param imgCacheDir ローカル画像キャッシュディレクトリ
+     */
+    private static void buildImageCacheDir(Path imgCacheDir){
+        assert imgCacheDir.isAbsolute();
+        if(Files.exists(imgCacheDir)) return;
+
+        try{
+            Files.createDirectories(imgCacheDir);
+        }catch(IOException | SecurityException e){
+            logMkdirErr(imgCacheDir, e);
+            abortCantBuildDir(imgCacheDir);
+            assert false;
+        }
+
+        checkDirPerm(imgCacheDir);
+
+        Path jsonPath = imgCacheDir.resolve(FILE_AVATARJSON);
+        copyResource(RES_AVATARJSON, jsonPath);
+
+        return;
+    }
+
+    /**
+     * ロックファイルの取得を試みる。
+     *
+     * <p>ロックに失敗したが処理を続行する場合、
+     * 設定ディレクトリは使わないものとして続行する。
+     *
+     * @param configStore 設定ディレクトリ情報
+     */
+    public static void tryLock(ConfigStore configStore){
+        if( ! configStore.useStoreFile() ) return;
+
+        Path lockPath = configStore.getLockFile();
+        File lockFile = lockPath.toFile();
+        InterVMLock lock = new InterVMLock(lockFile);
+
+        lock.tryLock();
+
+        if( ! lock.isFileOwner() ){
+            confirmLockError(lock);
+            if( ! lock.isFileOwner() ){
+                configStore.setNoConf();
+            }
+        }
+
+        return;
+    }
+
+    /**
+     * ロックエラーダイアログの表示。
+     *
+     * <p>呼び出しから戻ってもまだロックオブジェクトが
+     * ロックファイルのオーナーでない場合、
+     * 今後設定ディレクトリは一切使わずに起動を続行するものとする。
+     *
+     * <p>ロックファイルの強制解除に失敗した場合はVM終了する。
+     *
+     * @param lock エラーを起こしたロック
+     */
+    private static void confirmLockError(InterVMLock lock){
+        File lockFile = lock.getLockFile();
+
+        LockErrorPane lockPane = new LockErrorPane(lockFile.toPath());
+        JDialog lockDialog = lockPane.createDialog(TITLE_BUILDCONF);
+
+        lockDialog.setResizable(true);
+        lockDialog.pack();
+
+        do{
+            lockDialog.setVisible(true);
+
+            Object result = lockPane.getValue();
+            boolean aborted = LockErrorPane.isAborted(result);
+            boolean windowClosed = result == null;
+
+            if(aborted || windowClosed){
+                abort();
+                assert false;
+                break;
+            }
+
+            if(lockPane.isRadioRetry()){
+                lock.tryLock();
+            }else if(lockPane.isRadioContinue()){
+                JOptionPane pane = new JOptionPane(
+                        MSG_NOCONF, JOptionPane.INFORMATION_MESSAGE);
+                showDialog(pane);
+                break;
+            }else if(lockPane.isRadioForce()){
+                forceRemove(lock);
+                break;
+            }
+        }while( ! lock.isFileOwner());
+
+        lockDialog.dispose();
+
+        return;
+    }
+
+    /**
+     * ロックファイルの強制削除を試みる。
+     *
+     * <p>削除とそれに後続する再ロック取得に成功したときだけ制御を戻す。
+     *
+     * <p>削除できないまたは再ロックできない場合は、
+     * 制御を戻さずVMごとアプリを終了する。
+     *
+     * @param lock ロック
+     */
+    private static void forceRemove(InterVMLock lock){
+        File lockFile = lock.getLockFile();
+
+        lock.forceRemove();
+        if(lock.isExistsFile()){
+            String fileName = getCenteredFileName(lockFile.toPath());
+            String msg = MessageFormat.format(FORM_FAILRM, fileName);
+            showErrorMessage(msg);
+            abort();
+            assert false;
+            return;
+        }
+
+        lock.tryLock();
+        if(lock.isFileOwner()) return;
+
+        String fileName = getCenteredFileName(lockFile.toPath());
+        String msg = MessageFormat.format(FORM_ILLLOCK, fileName);
+        showErrorMessage(msg);
+        abort();
+        assert false;
+
+        return;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/config/ConfigFile.java b/src/main/java/jp/sfjp/jindolf/config/ConfigFile.java
deleted file mode 100644 (file)
index 35efc47..0000000
+++ /dev/null
@@ -1,573 +0,0 @@
-/*
- * configuration file & directory
- *
- * License : The MIT License
- * Copyright(c) 2009 olyutorskii
- */
-
-package jp.sfjp.jindolf.config;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.io.Writer;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import javax.swing.JDialog;
-import javax.swing.JOptionPane;
-import jp.sfjp.jindolf.ResourceManager;
-import jp.sfjp.jindolf.VerInfo;
-import jp.sfjp.jindolf.view.LockErrorPane;
-
-/**
- * Jindolf設定格納ディレクトリに関するあれこれ。
- */
-public final class ConfigFile{
-
-    private static final String TITLE_BUILDCONF =
-            VerInfo.TITLE + "設定格納ディレクトリの設定";
-
-    private static final String JINCONF     = "Jindolf";
-    private static final String JINCONF_DOT = ".jindolf";
-    private static final String FILE_README = "README.txt";
-    private static final Charset CHARSET_README = StandardCharsets.UTF_8;
-
-    private static final String MSG_POST =
-            "<ul>"
-            + "<li><code>" + CmdOption.OPT_CONFDIR + "</code>"
-            + "&nbsp;オプション指定により、<br>"
-            + "任意の設定格納ディレクトリを指定することができます。<br>"
-            + "<li><code>" + CmdOption.OPT_NOCONF + "</code>"
-            + "&nbsp;オプション指定により、<br>"
-            + "設定格納ディレクトリを使わずに起動することができます。<br>"
-            + "</ul>";
-
-
-    /**
-     * 隠れコンストラクタ。
-     */
-    private ConfigFile(){
-        assert false;
-        return;
-    }
-
-
-    /**
-     * 暗黙的な設定格納ディレクトリを返す。
-     *
-     * <ul>
-     *
-     * <li>起動元JARファイルと同じディレクトリに、
-     * アクセス可能なディレクトリ"Jindolf"が
-     * すでに存在していればそれを返す。
-     *
-     * <li>起動元JARファイルおよび"Jindolf"が発見できなければ、
-     * MacOSX環境の場合"~/Library/Application Support/Jindolf/"を返す。
-     * Windows環境の場合"%USERPROFILE%\Jindolf\"を返す。
-     *
-     * <li>それ以外の環境(Linux,etc?)の場合"~/.jindolf/"を返す。
-     *
-     * </ul>
-     *
-     * <p>返すディレクトリが存在しているか否か、
-     * アクセス可能か否かは呼び出し元で判断せよ。
-     *
-     * @return 設定格納ディレクトリ
-     */
-    public static File getImplicitConfigDirectory(){
-        File result;
-
-        File jarParent = FileUtils.getJarDirectory();
-        if(jarParent != null && FileUtils.isAccessibleDirectory(jarParent)){
-            result = new File(jarParent, JINCONF);
-            if(FileUtils.isAccessibleDirectory(result)){
-                return result;
-            }
-        }
-
-        File appset = FileUtils.getAppSetDir();
-        if(appset == null) return null;
-
-        if(FileUtils.isMacOSXFs() || FileUtils.isWindowsOSFs()){
-            result = new File(appset, JINCONF);
-        }else{
-            result = new File(appset, JINCONF_DOT);
-        }
-
-        return result;
-    }
-
-    /**
-     * まだ存在しない設定格納ディレクトリを新規に作成する。
-     *
-     * <p>エラーがあればダイアログ提示とともにVM終了する。
-     *
-     * @param confPath 設定格納ディレクトリ
-     * @param isImplicitPath ディレクトリが暗黙的に指定されたものならtrue。
-     * @return 新規に作成した設定格納ディレクトリ
-     * @throws IllegalArgumentException すでにそのディレクトリは存在する。
-     */
-    public static File buildConfigDirectory(File confPath,
-                                            boolean isImplicitPath )
-            throws IllegalArgumentException{
-        if(confPath.exists()) throw new IllegalArgumentException();
-
-        File absPath = FileUtils.supplyFullPath(confPath);
-
-        String preErrMessage =
-                "設定格納ディレクトリ<br>"
-                + getCenteredFileName(absPath)
-                + "の作成に失敗しました。";
-        if( ! isImplicitPath ){
-            preErrMessage =
-                    "<code>"
-                    + CmdOption.OPT_CONFDIR
-                    + "</code>&nbsp;オプション"
-                    + "で指定された、<br>"
-                    + preErrMessage;
-        }
-
-        File existsAncestor = FileUtils.findExistsAncestor(absPath);
-        if(existsAncestor == null){
-            abortNoRoot(absPath, preErrMessage);
-        }else if( ! existsAncestor.canWrite() ){
-            abortCantWriteAncestor(existsAncestor, preErrMessage);
-        }
-
-        String prompt =
-                "設定ファイル格納ディレクトリ<br>"
-                + getCenteredFileName(absPath)
-                + "を作成します。";
-        boolean confirmed = confirmBuildConfigDir(existsAncestor, prompt);
-        if( ! confirmed ){
-            abortQuitBuildConfigDir();
-        }
-
-        boolean success;
-        try{
-            success = absPath.mkdirs();
-        }catch(SecurityException e){
-            success = false;
-        }
-
-        if( ! success || ! absPath.exists() ){
-            abortCantBuildConfigDir(absPath);
-        }
-
-        FileUtils.setOwnerOnlyAccess(absPath);
-
-        checkAccessibility(absPath);
-
-        touchReadme(absPath);
-
-        return absPath;
-    }
-
-    /**
-     * ローカル画像キャッシュディレクトリを作る。
-     *
-     * <p>作られたディレクトリ内に
-     * ファイルavatarCache.jsonが作られる。
-     *
-     * @param imgCacheDir ローカル画像キャッシュディレクトリ
-     */
-    public static void buildImageCacheDir(File imgCacheDir){
-        if(imgCacheDir.exists()) return;
-
-        String jsonRes = "resources/image/avatarCache.json";
-        InputStream is = ResourceManager.getResourceAsStream(jsonRes);
-        if(is == null) return;
-
-        imgCacheDir.mkdirs();
-        ConfigFile.checkAccessibility(imgCacheDir);
-
-        Path cachePath = imgCacheDir.toPath();
-        Path jsonLeaf = Paths.get("avatarCache.json");
-        Path path = cachePath.resolve(jsonLeaf);
-        try{
-            Files.copy(is, path);
-        }catch(IOException e){
-            abortCantAccessConfigDir(path.toFile());
-        }
-
-        return;
-    }
-
-    /**
-     * 設定ディレクトリ操作の
-     * 共通エラーメッセージ確認ダイアログを表示する。
-     *
-     * <p>閉じるまで待つ。
-     *
-     * @param seq メッセージ
-     */
-    private static void showErrorMessage(CharSequence seq){
-        JOptionPane pane =
-                new JOptionPane(seq.toString(),
-                                JOptionPane.ERROR_MESSAGE);
-        showDialog(pane);
-        return;
-    }
-
-    /**
-     * 設定ディレクトリ操作の
-     * 共通エラーメッセージ確認ダイアログを表示する。
-     *
-     * <p>閉じるまで待つ。
-     *
-     * @param seq メッセージ
-     */
-    private static void showWarnMessage(CharSequence seq){
-        JOptionPane pane =
-                new JOptionPane(seq.toString(),
-                                JOptionPane.WARNING_MESSAGE);
-        showDialog(pane);
-        return;
-    }
-
-    /**
-     * 設定ディレクトリ操作の
-     * 情報提示メッセージ確認ダイアログを表示する。
-     *
-     * <p>閉じるまで待つ。
-     *
-     * @param seq メッセージ
-     */
-    private static void showInfoMessage(CharSequence seq){
-        JOptionPane pane =
-                new JOptionPane(seq.toString(),
-                                JOptionPane.INFORMATION_MESSAGE);
-        showDialog(pane);
-        return;
-    }
-
-    /**
-     * ダイアログを表示し、閉じられるまで待つ。
-     *
-     * @param pane ダイアログの元となるペイン
-     */
-    private static void showDialog(JOptionPane pane){
-        JDialog dialog = pane.createDialog(null, TITLE_BUILDCONF);
-        dialog.setResizable(true);
-        dialog.pack();
-
-        dialog.setVisible(true);
-        dialog.dispose();
-
-        return;
-    }
-
-    /**
-     * VMを異常終了させる。
-     */
-    private static void abort(){
-        System.exit(1);
-        assert false;
-        return;
-    }
-
-    /**
-     * 設定ディレクトリのルートファイルシステムもしくはドライブレターに
-     * アクセスできないエラーをダイアログに提示し、VM終了する。
-     *
-     * @param path 設定ディレクトリ
-     * @param preMessage メッセージ前半
-     */
-    private static void abortNoRoot(File path, String preMessage){
-        File root = FileUtils.findRootFile(path);
-        showErrorMessage(
-                "<html>"
-                + preMessage + "<br>"
-                + getCenteredFileName(root)
-                + "を用意する方法が不明です。<br>"
-                + "起動を中止します。<br>"
-                + MSG_POST
-                + "</html>" );
-        abort();
-        return;
-    }
-
-    /**
-     * 設定ディレクトリの祖先に書き込めないエラーをダイアログで提示し、
-     * VM終了する。
-     *
-     * @param existsAncestor 存在するもっとも近い祖先
-     * @param preMessage メッセージ前半
-     */
-    private static void abortCantWriteAncestor(File existsAncestor,
-                                                  String preMessage ){
-        showErrorMessage(
-                "<html>"
-                + preMessage + "<br>"
-                + getCenteredFileName(existsAncestor)
-                + "への書き込みができないため、"
-                + "処理の続行は不可能です。<br>"
-                + "起動を中止します。<br>"
-                + MSG_POST
-                + "</html>" );
-        abort();
-        return;
-    }
-
-    /**
-     * 設定ディレクトリを新規に生成してよいかダイアログで問い合わせる。
-     *
-     * @param existsAncestor 存在するもっとも近い祖先
-     * @param preMessage メッセージ前半
-     * @return 生成してよいと指示があればtrue
-     */
-    private static boolean confirmBuildConfigDir(File existsAncestor,
-                                                    String preMessage){
-        String message =
-                "<html>"
-                + preMessage + "<br>"
-                + "このディレクトリを今から<br>"
-                + getCenteredFileName(existsAncestor)
-                + "に作成して構いませんか?<br>"
-                + "このディレクトリ名は、後からいつでもヘルプウィンドウで<br>"
-                + "確認することができます。"
-                + "</html>";
-
-        JOptionPane pane =
-                new JOptionPane(message,
-                                JOptionPane.QUESTION_MESSAGE,
-                                JOptionPane.YES_NO_OPTION);
-
-        showDialog(pane);
-
-        Object result = pane.getValue();
-        if(result == null) return false;
-        else if( ! (result instanceof Integer) ) return false;
-
-        int ival = (Integer) result;
-        if(ival == JOptionPane.YES_OPTION) return true;
-
-        return false;
-    }
-
-    /**
-     * 設定ディレクトリ生成をやめた操作への警告をダイアログで提示し、
-     * VM終了する。
-     */
-    private static void abortQuitBuildConfigDir(){
-        showWarnMessage(
-                "<html>"
-                + "設定ディレクトリの作成をせずに起動を中止します。<br>"
-                + MSG_POST
-                + "</html>" );
-        abort();
-        return;
-    }
-
-    /**
-     * 設定ディレクトリが生成できないエラーをダイアログで提示し、
-     * VM終了する。
-     *
-     * @param path 生成できなかったディレクトリ
-     */
-    private static void abortCantBuildConfigDir(File path){
-        showErrorMessage(
-                "<html>"
-                + "設定ディレクトリ<br>"
-                + getCenteredFileName(path)
-                + "の作成に失敗しました。"
-                + "起動を中止します。<br>"
-                + MSG_POST
-                + "</html>" );
-        abort();
-        return;
-    }
-
-    /**
-     * 設定ディレクトリへアクセスできないエラーをダイアログで提示し、
-     * VM終了する。
-     *
-     * @param path アクセスできないディレクトリ
-     */
-    private static void abortCantAccessConfigDir(File path){
-        showErrorMessage(
-                "<html>"
-                + "設定ディレクトリ<br>"
-                + getCenteredFileName(path)
-                + "へのアクセスができません。"
-                + "起動を中止します。<br>"
-                + "このディレクトリへのアクセス権を調整し"
-                + "読み書きできるようにしてください。<br>"
-                + MSG_POST
-                + "</html>" );
-        abort();
-        return;
-    }
-
-    /**
-     * ファイルに書き込めないエラーをダイアログで提示し、VM終了する。
-     *
-     * @param file 書き込めなかったファイル
-     */
-    private static void abortCantWrite(File file){
-        showErrorMessage(
-                "<html>"
-                + "ファイル<br>"
-                + getCenteredFileName(file)
-                + "への書き込みができません。"
-                + "起動を中止します。<br>"
-                + "</html>" );
-        abort();
-        return;
-    }
-
-    /**
-     * 指定されたディレクトリにREADMEファイルを生成する。
-     *
-     * <p>生成できなければダイアログ表示とともにVM終了する。
-     *
-     * @param path READMEの格納ディレクトリ
-     */
-    private static void touchReadme(File path){
-        File file = new File(path, FILE_README);
-
-        try{
-            file.createNewFile();
-        }catch(IOException e){
-            abortCantAccessConfigDir(path);
-        }
-
-        PrintWriter writer = null;
-        try{
-            OutputStream ostream = new FileOutputStream(file);
-            Writer owriter = new OutputStreamWriter(ostream, CHARSET_README);
-            writer = new PrintWriter(owriter);
-            writer.println(CHARSET_README.name() + " Japanese");
-            writer.println(
-                    "このディレクトリは、"
-                    + "Jindolfの各種設定が格納されるディレクトリです。");
-            writer.println(
-                    "Jindolfの詳細は "
-                    + "http://jindolf.osdn.jp/"
-                    + " を参照してください。");
-            writer.println(
-                    "このディレクトリを"
-                    + "「" + JINCONF + "」"
-                    + "の名前で起動元JARファイルと"
-                    + "同じ位置に");
-            writer.println(
-                    "コピーすれば、そちらの設定が優先して使われます。");
-            writer.println(
-                    "「lock」の名前を持つファイルはロックファイルです。");
-        }catch(IOException | SecurityException e){
-            abortCantWrite(file);
-        }finally{
-            if(writer != null){
-                writer.close();
-            }
-        }
-
-        return;
-    }
-
-    /**
-     * 設定ディレクトリがアクセス可能でなければ
-     * エラーダイアログを出してVM終了する。
-     *
-     * @param confDir 設定ディレクトリ
-     */
-    public static void checkAccessibility(File confDir){
-        if( ! FileUtils.isAccessibleDirectory(confDir) ){
-            abortCantAccessConfigDir(confDir);
-        }
-
-        return;
-    }
-
-    /**
-     * センタリングされたファイル名表示のHTML表記を出力する。
-     *
-     * @param path ファイル
-     * @return HTML表記
-     */
-    public static String getCenteredFileName(File path){
-        return "<center>[&nbsp;"
-                + FileUtils.getHtmledFileName(path)
-                + "&nbsp;]</center>"
-                + "<br>";
-    }
-
-    /**
-     * ロックエラーダイアログの表示。
-     *
-     * <p>呼び出しから戻ってもまだロックオブジェクトが
-     * ロックファイルのオーナーでない場合、
-     * 今後設定ディレクトリは一切使わずに起動を続行するものとする。
-     *
-     * <p>ロックファイルの強制解除に失敗した場合はVM終了する。
-     *
-     * @param lock エラーを起こしたロック
-     */
-    public static void confirmLockError(InterVMLock lock){
-        LockErrorPane pane = new LockErrorPane(lock);
-        JDialog dialog = pane.createDialog(null, TITLE_BUILDCONF);
-        dialog.setResizable(true);
-        dialog.pack();
-
-        for(;;){
-            dialog.setVisible(true);
-            dialog.dispose();
-
-            if(pane.isAborted() || pane.getValue() == null){
-                abort();
-                break;
-            }else if(pane.isRadioRetry()){
-                lock.tryLock();
-                if(lock.isFileOwner()) break;
-            }else if(pane.isRadioContinue()){
-                showInfoMessage(
-                        "<html>"
-                        + "設定ディレクトリを使わずに起動を続行します。<br>"
-                        + "今回、各種設定の読み込み・保存はできません。<br>"
-                        + "<code>"
-                        + CmdOption.OPT_NOCONF
-                        + "</code> オプション"
-                        + "を使うとこの警告は出なくなります。"
-                        + "</html>");
-                break;
-            }else if(pane.isRadioForce()){
-                lock.forceRemove();
-                if(lock.isExistsFile()){
-                    showErrorMessage(
-                            "<html>"
-                            + "ロックファイルの強制解除に失敗しました。<br>"
-                            + "他に動いているJindolf"
-                            + "が見つからないのであれば、<br>"
-                            + "なんとかしてロックファイル<br>"
-                            + getCenteredFileName(lock.getLockFile())
-                            + "を削除してください。<br>"
-                            + "起動を中止します。"
-                            + "</html>");
-                    abort();
-                    break;
-                }
-                lock.tryLock();
-                if(lock.isFileOwner()) break;
-                showErrorMessage(
-                        "<html>"
-                        + "ロックファイル<br>"
-                        + getCenteredFileName(lock.getLockFile())
-                        + "を確保することができません。<br>"
-                        + "起動を中止します。"
-                        + "</html>");
-                abort();
-                break;
-            }
-        }
-
-        return;
-    }
-
-}
index 720c5e6..108a178 100644 (file)
 
 package jp.sfjp.jindolf.config;
 
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Reader;
-import java.io.Writer;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import jp.sourceforge.jovsonz.JsComposition;
-import jp.sourceforge.jovsonz.JsObject;
-import jp.sourceforge.jovsonz.JsParseException;
-import jp.sourceforge.jovsonz.JsTypes;
-import jp.sourceforge.jovsonz.JsVisitException;
-import jp.sourceforge.jovsonz.Json;
 
 /**
- * 各種設定の永続化関連。
+ * Jindolf設定ディレクトリの管理を行う。
+ *
+ * <p>デフォルトの設定ディレクトリや
+ * アプリのコマンドライン引数から構成された、
+ * 設定ディレクトリに関する情報が保持管理される。
+ *
+ * <p>基本的に1アプリのみが設定ディレクトリへの入出力を許される。
+ *
+ * <p>インスタンス生成後に
+ * 設定ディレクトリを使わない設定に変更することが可能。
+ * (※ロック確保失敗後の続行等を想定)
+ *
+ * <p>設定ディレクトリには
+ *
+ * <ul>
+ * <li>ロックファイル
+ * <li>JSON設定ファイル
+ * <li>Avatar代替イメージ格納ディレクトリ
+ * </ul>
+ *
+ * <p>などが配置される。
+ *
+ * <p>コンストラクタに与えられるディレクトリは
+ * 絶対パスでなければならない。
+ *
+ * <p>ロックファイル取得の失敗、
+ * およびその後のユーザインタラクションによっては、
+ * 設定ディレクトリを使わない設定に上書きされた後
+ * 起動処理が続行される場合がありうる。
  */
 public class ConfigStore {
 
-    /** 検索履歴ファイル。 */
-    public static final File HIST_FILE = new File("searchHistory.json");
-    /** ネットワーク設定ファイル。 */
-    public static final File NETCONFIG_FILE = new File("netconfig.json");
-    /** 台詞表示設定ファイル。 */
-    public static final File TALKCONFIG_FILE = new File("talkconfig.json");
-    /** ローカル画像格納ディレクトリ。 */
-    public static final Path LOCALIMG_DIR = Paths.get("img");
-    /** ローカル画像設定ファイル。 */
-    public static final Path LOCALIMGCONFIG_PATH =
-            Paths.get("avatarCache.json");
+    private static final Path JINCONF      = Paths.get("Jindolf");
+    private static final Path JINCONF_DOT  = Paths.get(".jindolf");
+    private static final Path LOCKFILE     = Paths.get("lock");
+    private static final Path LOCALIMG_DIR = Paths.get("img");
 
-    private static final String LOCKFILE = "lock";
-
-    private static final Charset CHARSET_JSON = StandardCharsets.UTF_8;
-
-    private static final Logger LOGGER = Logger.getAnonymousLogger();
+    private static final Path MAC_LIB     = Paths.get("Library");
+    private static final Path MAC_APPSUPP = Paths.get("Application Support");
 
 
     private boolean useStoreFile;
-    private boolean isImplicitPath;
-    private File configDir;
+    private Path configDir;
 
 
     /**
      * コンストラクタ。
      *
-     * @param useStoreFile 設定ディレクトリ内への
-     *     セーブデータ機能を使うならtrue
-     * @param isImplicitPath 起動コマンドラインから指定された
-     *     設定ディレクトリの場合false
-     * @param configDirPath 設定ディレクトリ。
-     *     設定ディレクトリを使わない場合は無視される。
+     * <p>このインスタンスでは、
+     * 設定ディレクトリへの入出力を行わない。
      */
-    public ConfigStore(boolean useStoreFile,
-                       boolean isImplicitPath,
-                       File configDirPath ){
-        super();
-
-        this.useStoreFile = useStoreFile;
-
-        if(this.useStoreFile){
-            this.isImplicitPath = isImplicitPath;
-            this.configDir = configDirPath;
-        }else{
-            this.isImplicitPath = true;
-            this.configDir = null;
-        }
-
+    public ConfigStore(){
+        this(false, null);
         return;
     }
 
-
     /**
-     * 設定ディレクトリを使うか否か判定する
+     * コンストラクタ
      *
-     * @return 設定ディレクトリを使うならtrue。
-     */
-    public boolean useStoreFile(){
-        return this.useStoreFile;
-    }
-
-    /**
-     * 設定ディレクトリを返す。
+     * <p>このインスタンスでは、
+     * 設定ディレクトリへの入出力を行うが、
+     * デフォルトではない明示されたディレクトリが用いられる。
      *
-     * @return 設定ディレクトリ。設定ディレクトリを使わない場合はnull
+     * @param configDirPath 設定ディレクトリの絶対パス。
+     *     nullの場合はデフォルトの設定ディレクトリが用いられる。
+     * @throws IllegalArgumentException 絶対パスではない。
      */
-    public File getConfigDir(){
-        return this.configDir;
+    public ConfigStore(Path configDirPath) throws IllegalArgumentException{
+        this(true, configDirPath);
+        return;
     }
 
     /**
-     * ã\83­ã\83¼ã\82«ã\83«ç\94»å\83\8fæ ¼ç´\8dã\83\87ã\82£ã\83¬ã\82¯ã\83\88ã\83ªã\82\92è¿\94ã\81\99
+     * ã\82³ã\83³ã\82¹ã\83\88ã\83©ã\82¯ã\82¿
      *
-     * @return 格納ディレクトリ。格納ディレクトリを使わない場合はnull
+     * @param useStoreFile 設定ディレクトリ内への
+     *     入出力機能を使うならtrue
+     * @param configDirPath 設定ディレクトリの絶対パス。
+     *     設定ディレクトリを使わない場合は無視される。
+     *     この時点でのディレクトリ存在の有無は関知しない。
+     *     既存ディレクトリの各種属性チェックは後にチェックするものとする。
+     *     nullの場合デフォルトの設定ディレクトリが用いられる。
+     * @throws IllegalArgumentException 絶対パスではない。
      */
-    public Path getLocalImgDir(){
-        if(this.configDir == null) return null;
-
-        Path configPath = this.configDir.toPath();
-        Path result = configPath.resolve(LOCALIMG_DIR);
+    protected ConfigStore(boolean useStoreFile,
+                          Path configDirPath )
+            throws IllegalArgumentException{
+        super();
 
-        return result;
-    }
+        this.useStoreFile = useStoreFile;
 
-    /**
-     * 設定ディレクトリの存在を確認し、なければ作る。
-     *
-     * <p>設定ディレクトリを使わない場合は何もしない。
-     */
-    public void prepareConfigDir(){
-        if( ! this.useStoreFile ) return;
-
-        if( ! this.configDir.exists() ){
-            File created =
-                ConfigFile.buildConfigDirectory(this.configDir,
-                                                this.isImplicitPath );
-            ConfigFile.checkAccessibility(created);
+        if(this.useStoreFile){
+            if(configDirPath != null){
+                if( ! configDirPath.isAbsolute()){
+                    throw new IllegalArgumentException();
+                }
+                this.configDir = configDirPath;
+            }else{
+                this.configDir = getDefaultConfDirPath();
+            }
         }else{
-            ConfigFile.checkAccessibility(this.configDir);
+            this.configDir = null;
         }
 
-        File imgDir = new File(this.configDir, "img");
-        if( ! imgDir.exists()){
-            ConfigFile.buildImageCacheDir(imgDir);
-        }
+        assert     (     this.useStoreFile  && this.configDir != null)
+                || ( ( ! this.useStoreFile) && this.configDir == null);
 
         return;
     }
 
+
     /**
-     * ロックファイルの取得を試みる。
+     * 暗黙的な設定格納ディレクトリを絶対パスで返す。
+     *
+     * <ul>
+     *
+     * <li>起動元JARファイルと同じディレクトリに、
+     * アクセス可能なディレクトリ"Jindolf"が
+     * すでに存在していればそれを返す。
+     *
+     * <li>起動元JARファイルおよび"Jindolf"が発見できなければ、
+     * MacOSX環境の場合"~/Library/Application Support/Jindolf/"を返す。
+     * Windows環境の場合"%USERPROFILE%\Jindolf\"を返す。
+     *
+     * <li>それ以外の環境(Linux,etc?)の場合"~/.jindolf/"を返す。
+     *
+     * </ul>
      *
-     * <p>ロックに失敗したが処理を続行する場合、
-     * 設定ディレクトリは使わないものとして続行する。
+     * <p>返すディレクトリが存在しているか否か、
+     * アクセス可能か否かは呼び出し元で判断せよ。
+     *
+     * @return 設定格納ディレクトリの絶対パス
      */
-    public void tryLock(){
-        if( ! this.useStoreFile ) return;
-
-        File lockFile = new File(this.configDir, LOCKFILE);
-        InterVMLock lock = new InterVMLock(lockFile);
-
-        lock.tryLock();
-
-        if( ! lock.isFileOwner() ){
-            ConfigFile.confirmLockError(lock);
-            if( ! lock.isFileOwner() ){
-                this.useStoreFile = false;
-                this.isImplicitPath = true;
-                this.configDir = null;
+    public static Path getDefaultConfDirPath(){
+        Path jarParent = FileUtils.getJarDirectory();
+        if(FileUtils.isAccessibleDirectory(jarParent)){
+            Path confPath = jarParent.resolve(JINCONF);
+            if(FileUtils.isAccessibleDirectory(confPath)){
+                assert confPath.isAbsolute();
+                return confPath;
             }
         }
 
-        return;
-    }
+        Path appset = getAppSetDir();
 
-    /**
-     * 設定ディレクトリ上のOBJECT型JSONファイルを読み込む。
-     *
-     * @param file JSONファイルの相対パス。
-     * @return JSON object。
-     *     設定ディレクトリを使わない設定、
-     *     もしくはJSONファイルが存在しない、
-     *     もしくはOBJECT型でなかった、
-     *     もしくは入力エラーがあればnull
-     */
-    public JsObject loadJsObject(File file){
-        JsComposition<?> root = loadJson(file);
-        if(root == null || root.getJsTypes() != JsTypes.OBJECT) return null;
-        JsObject result = (JsObject) root;
-        return result;
-    }
-
-    /**
-     * 設定ディレクトリ上のJSONファイルを読み込む。
-     *
-     * @param file JSONファイルの相対パス
-     * @return JSON objectまたはarray。
-     *     設定ディレクトリを使わない設定、
-     *     もしくはJSONファイルが存在しない、
-     *     もしくは入力エラーがあればnull
-     */
-    public JsComposition<?> loadJson(File file){
-        if( ! this.useStoreFile ) return null;
-
-        File absFile;
-        if(file.isAbsolute()){
-            absFile = file;
+        Path leaf;
+        if(FileUtils.isMacOSXFs() || FileUtils.isWindowsOSFs()){
+            leaf = JINCONF;
         }else{
-            if(this.configDir == null) return null;
-            absFile = new File(this.configDir, file.getPath());
-            if( ! absFile.exists() ) return null;
-            if( ! absFile.isAbsolute() ) return null;
-        }
-        String absPath = absFile.getPath();
-
-        InputStream istream;
-        try{
-            istream = new FileInputStream(absFile);
-        }catch(FileNotFoundException e){
-            assert false;
-            return null;
-        }
-        istream = new BufferedInputStream(istream);
-
-        JsComposition<?> root;
-        try{
-            root = loadJson(istream);
-        }catch(IOException e){
-            LOGGER.log(Level.SEVERE,
-                    "JSONファイル["
-                    + absPath
-                    + "]の読み込み時に支障がありました。", e);
-            return null;
-        }catch(JsParseException e){
-            LOGGER.log(Level.SEVERE,
-                    "JSONファイル["
-                    + absPath
-                    + "]の内容に不備があります。", e);
-            return null;
-        }finally{
-            try{
-                istream.close();
-            }catch(IOException e){
-                LOGGER.log(Level.SEVERE,
-                        "JSONファイル["
-                        + absPath
-                        + "]を閉じることができません。", e);
-                return null;
-            }
+            leaf = JINCONF_DOT;
         }
 
-        return root;
+        Path result = appset.resolve(leaf);
+        assert result.isAbsolute();
+
+        return result;
     }
 
     /**
-     * ã\83\90ã\82¤ã\83\88ã\82¹ã\83\88ã\83ªã\83¼ã\83 ä¸\8aã\81®JSONã\83\87ã\83¼ã\82¿ã\82\92読ã\81¿è¾¼ã\82\80
+     * ã\82¢ã\83\97ã\83ªã\82±ã\83¼ã\82·ã\83§ã\83³è¨­å®\9aã\83\87ã\82£ã\83¬ã\82¯ã\83\88ã\83ªã\82\92絶対ã\83\91ã\82¹ã\81§è¿\94ã\81\99
      *
-     * <p>バイトストリームはUTF-8と解釈される
+     * <p>存在の有無、アクセスの可否は関知しない
      *
-     * @param is バイトストリーム
-     * @return JSON objectまたはarray。
-     * @throws IOException 入力エラー
-     * @throws JsParseException 構文エラー
-     */
-    protected JsComposition<?> loadJson(InputStream is)
-            throws IOException, JsParseException {
-        Reader reader = new InputStreamReader(is, CHARSET_JSON);
-        reader = new BufferedReader(reader);
-        JsComposition<?> root = loadJson(reader);
-        return root;
-    }
-
-    /**
-     * 文字ストリーム上のJSONデータを読み込む。
+     * <p>WindowsやLinuxではホームディレクトリ。
+     * Mac OS X ではさらにホームディレクトリの下の
+     * "Library/Application Support/"
      *
-     * @param reader 文字ストリーム
-     * @return JSON objectまたはarray。
-     * @throws IOException 入力エラー
-     * @throws JsParseException 構文エラー
+     * @return アプリケーション設定ディレクトリの絶対パス
      */
-    protected JsComposition<?> loadJson(Reader reader)
-            throws IOException, JsParseException {
-        JsComposition<?> root = Json.parseJson(reader);
-        return root;
-    }
+    private static Path getAppSetDir(){
+        Path home = getHomeDirectory();
 
-    /**
-     * 設定ディレクトリ上のJSONファイルに書き込む。
-     *
-     * @param file JSONファイルの相対パス
-     * @param root JSON objectまたはarray
-     * @return 正しくセーブが行われればtrue。
-     *     何らかの理由でセーブが完了できなければfalse
-     */
-    public boolean saveJson(File file, JsComposition<?> root){
-        if( ! this.useStoreFile ) return false;
-
-        // TODO テンポラリファイルを用いたより安全なファイル更新
-        File absFile = new File(this.configDir, file.getPath());
-        String absPath = absFile.getPath();
-
-        absFile.delete();
-        try{
-            if(absFile.createNewFile() != true) return false;
-        }catch(IOException e){
-            LOGGER.log(Level.SEVERE,
-                    "JSONファイル["
-                    + absPath
-                    + "]の新規生成ができません。", e);
-            return false;
-        }
+        Path result = home;
 
-        OutputStream ostream;
-        try{
-            ostream = new FileOutputStream(absFile);
-        }catch(FileNotFoundException e){
-            assert false;
-            return false;
-        }
-        ostream = new BufferedOutputStream(ostream);
-
-        try{
-            saveJson(ostream, root);
-        }catch(JsVisitException e){
-            LOGGER.log(Level.SEVERE,
-                    "JSONファイル["
-                    + absPath
-                    + "]の出力処理で支障がありました。", e);
-            return false;
-        }catch(IOException e){
-            LOGGER.log(Level.SEVERE,
-                    "JSONファイル["
-                    + absPath
-                    + "]の書き込み時に支障がありました。", e);
-            return false;
-        }finally{
-            try{
-                ostream.close();
-            }catch(IOException e){
-                LOGGER.log(Level.SEVERE,
-                        "JSONファイル["
-                        + absPath
-                        + "]を閉じることができません。", e);
-                return false;
-            }
+        if(FileUtils.isMacOSXFs()){
+            result = result.resolve(MAC_LIB);
+            result = result.resolve(MAC_APPSUPP);
         }
 
-        return true;
+        return result;
     }
 
     /**
-     * ã\83\90ã\82¤ã\83\88ã\82¹ã\83\88ã\83ªã\83¼ã\83 ã\81«JSONã\83\87ã\83¼ã\82¿ã\82\92æ\9b¸ã\81\8dè¾¼ã\82\80
+     * ã\83\9bã\83¼ã\83 ã\83\87ã\82£ã\83¬ã\82¯ã\83\88ã\83ªã\82\92å¾\97ã\82\8b
      *
-     * <p>バイトストリームはUTF-8と解釈される。
+     * <p>システムプロパティuser.homeで示されたホームディレクトリを
+     * 絶対パスで返す。
      *
-     * @param os バイトストリーム出力
-     * @param root JSON objectまたはarray
-     * @throws IOException 出力エラー
-     * @throws JsVisitException 構造エラー
+     * @return ホームディレクトリの絶対パス。
      */
-    protected void saveJson(OutputStream os, JsComposition<?> root)
-            throws IOException, JsVisitException {
-        Writer writer = new OutputStreamWriter(os, CHARSET_JSON);
-        writer = new BufferedWriter(writer);
-        saveJson(writer, root);
-        return;
+    private static Path getHomeDirectory(){
+        String homeProp = System.getProperty("user.home");
+        Path result = Paths.get(homeProp);
+        result = result.toAbsolutePath();
+        return result;
     }
 
-    /**
-     * 文字ストリームにJSONデータを書き込む。
-     *
-     * @param writer 文字ストリーム出力
-     * @param root JSON objectまたはarray
-     * @throws IOException 出力エラー
-     * @throws JsVisitException 構造エラー
-     */
-    protected void saveJson(Writer writer, JsComposition<?> root)
-            throws IOException, JsVisitException {
-        Json.dumpJson(writer, root);
-        return;
-    }
 
     /**
-     * 検索履歴ファイルを読み込む
+     * 設定ディレクトリを使うか否か判定する
      *
-     * @return 履歴データ。履歴を読まないもしくは読めない場合はnull
+     * @return 設定ディレクトリを使うならtrue。
      */
-    public JsObject loadHistoryConfig(){
-        JsObject result = loadJsObject(HIST_FILE);
-        return result;
+    public boolean useStoreFile(){
+        return this.useStoreFile;
     }
 
     /**
-     * ネットワーク設定ファイルを読み込む
+     * 設定ディレクトリを絶対パスで返す
      *
-     * @return ネットワーク設定データ
-     *     設定を読まないもしくは読めない場合はnull
+     * @return 設定ディレクトリの絶対パス
+     * 設定ディレクトリを使わない場合はnull
      */
-    public JsObject loadNetConfig(){
-        JsObject result = loadJsObject(NETCONFIG_FILE);
-        return result;
+    public Path getConfigDir(){
+        return this.configDir;
     }
 
     /**
-     * 台詞表示設定ファイルを読み込む。
-     *
-     * @return 台詞表示設定データ。
-     *     設定を読まないもしくは読めない場合はnull
+     * 設定ディレクトリを使わない設定に変更する。
      */
-    public JsObject loadTalkConfig(){
-        JsObject result = loadJsObject(TALKCONFIG_FILE);
-        return result;
+    public void setNoConf(){
+        this.useStoreFile = false;
+        this.configDir = null;
+        return;
     }
 
     /**
-     * ローカル画像設定ファイルを読み込む
+     * ローカル画像格納ディレクトリを絶対パスで返す
      *
-     * @return ローカル画像設定データ
-     *     設定を読まないもしくは読めない場合はnull
+     * @return 格納ディレクトリの絶対パス
+     * 格納ディレクトリを使わない場合はnull
      */
-    public JsObject loadLocalImgConfig(){
-        Path path = LOCALIMG_DIR.resolve(LOCALIMGCONFIG_PATH);
-        JsObject result = loadJsObject(path.toFile());
-        return result;
-    }
+    public Path getLocalImgDir(){
+        if( ! this.useStoreFile ) return null;
+        if(this.configDir == null) return null;
 
-    /**
-     * 検索履歴ファイルに書き込む。
-     *
-     * @param root 履歴データ
-     * @return 書き込まなかったもしくは書き込めなかった場合はfalse
-     */
-    public boolean saveHistoryConfig(JsComposition<?> root){
-        boolean result = saveJson(HIST_FILE, root);
-        return result;
-    }
+        Path result = this.configDir.resolve(LOCALIMG_DIR);
+        assert result.isAbsolute();
 
-    /**
-     * ネットワーク設定ファイルに書き込む。
-     *
-     * @param root ネットワーク設定
-     * @return 書き込まなかったもしくは書き込めなかった場合はfalse
-     */
-    public boolean saveNetConfig(JsComposition<?> root){
-        boolean result = saveJson(NETCONFIG_FILE, root);
         return result;
     }
 
     /**
-     * 台詞表示設定ファイルに書き込む
+     * ロックファイルを絶対パスで返す
      *
-     * @param root 台詞表示設定
-     * @return 書き込まなかったもしくは書き込めなかった場合はfalse
+     * @return ロックファイルの絶対パス。
+     * 格納ディレクトリを使わない場合はnull
      */
-    public boolean saveTalkConfig(JsComposition<?> root){
-        boolean result = saveJson(TALKCONFIG_FILE, root);
-        return result;
+    public Path getLockFile(){
+        if( ! this.useStoreFile ) return null;
+        if(this.configDir == null) return null;
+
+        Path lockFile = this.configDir.resolve(LOCKFILE);
+
+        return lockFile;
     }
 
 }
index 4d35025..b00702d 100644 (file)
@@ -8,8 +8,10 @@
 package jp.sfjp.jindolf.config;
 
 import java.io.File;
-import java.text.NumberFormat;
-import java.util.Set;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import java.util.SortedMap;
 import java.util.TreeMap;
 
@@ -32,36 +34,48 @@ public final class EnvInfo{
     /** 最大ヒープメモリ。 */
     public static final long MAX_MEMORY;
 
-    private static final SortedMap<String, String> PROPERTY_MAP =
-            new TreeMap<>();
-
-    private static final SortedMap<String, String> ENVIRONMENT_MAP =
-            new TreeMap<>();
-
-    private static final String[] CLASSPATHS;
+    private static final SortedMap<String, String> PROPERTY_MAP;
+    private static final SortedMap<String, String> ENVIRONMENT_MAP;
+
+    private static final List<String> CLASSPATHS;
+
+    private static final String[] PROPNAMES = {
+        "os.name",
+        "os.version",
+        "os.arch",
+        "java.vendor",
+        "java.version",
+        "java.class.path",
+    };
+
+    private static final String[] ENVNAMES = {
+        "LANG",
+        "DISPLAY",
+        //"PATH",
+        //"TEMP",
+        //"USER",
+    };
+
+    private static final String FORM_MEM =
+            "最大ヒープメモリ量: {0,number} bytes\n";
+    private static final String INDENT = "\u0020\u0020";
+    private static final char NL = '\n';
 
     static{
-        OS_NAME      = getSecureProperty("os.name");
-        OS_VERSION   = getSecureProperty("os.version");
-        OS_ARCH      = getSecureProperty("os.arch");
-        JAVA_VENDOR  = getSecureProperty("java.vendor");
-        JAVA_VERSION = getSecureProperty("java.version");
-
-        getSecureEnvironment("LANG");
-        getSecureEnvironment("DISPLAY");
-
         Runtime runtime = Runtime.getRuntime();
         MAX_MEMORY = runtime.maxMemory();
 
-        String classpath   = getSecureProperty("java.class.path");
-        String[] pathVec;
-        if(classpath != null){
-            pathVec = classpath.split(File.pathSeparator);
-        }else{
-            pathVec = new String[0];
-        }
-        CLASSPATHS = pathVec;
+        ENVIRONMENT_MAP = buildEnvMap();
+
+        PROPERTY_MAP = buildPropMap();
+        OS_NAME      = PROPERTY_MAP.get("os.name");
+        OS_VERSION   = PROPERTY_MAP.get("os.version");
+        OS_ARCH      = PROPERTY_MAP.get("os.arch");
+        JAVA_VENDOR  = PROPERTY_MAP.get("java.vendor");
+        JAVA_VERSION = PROPERTY_MAP.get("java.version");
 
+        String classpath = PROPERTY_MAP.get("java.class.path");
+        CLASSPATHS = buildClassPathList(classpath);
     }
 
 
@@ -69,39 +83,78 @@ public final class EnvInfo{
      * 隠れコンストラクタ。
      */
     private EnvInfo(){
-        throw new AssertionError();
+        assert false;
     }
 
 
     /**
-     * 可能ならシステムプロパティを読み込む
-     * @param key キー
-     * @return プロパティ値。セキュリティ上読み込み禁止の場合はnull。
+     * 主要環境変数マップを作成する
+     *
+     * @return 主要環境変数マップ
      */
-    private static String getSecureProperty(String key){
-        String result;
-        try{
-            result = System.getProperty(key);
-            if(result != null) PROPERTY_MAP.put(key, result);
-        }catch(SecurityException e){
-            result = null;
+    private static SortedMap<String, String> buildEnvMap(){
+        SortedMap<String, String> envmap = new TreeMap<>();
+
+        for(String name : ENVNAMES){
+            String val;
+            try{
+                val = System.getenv(name);
+            }catch(SecurityException e){
+                continue;
+            }
+            if(val == null) continue;
+            envmap.put(name, val);
         }
+
+        SortedMap<String, String> result;
+        result = Collections.unmodifiableSortedMap(envmap);
+
         return result;
     }
 
     /**
-     * 可能なら環境変数を読み込む
-     * @param name 環境変数名
-     * @return 環境変数値。セキュリティ上読み込み禁止の場合はnull。
+     * 主要システムプロパティ値マップを作成する
+     *
+     * @return 主要システムプロパティ値マップ
      */
-    private static String getSecureEnvironment(String name){
-        String result;
-        try{
-            result = System.getenv(name);
-            if(result != null) ENVIRONMENT_MAP.put(name, result);
-        }catch(SecurityException e){
-            result = null;
+    private static SortedMap<String, String> buildPropMap(){
+        SortedMap<String, String> propmap = new TreeMap<>();
+
+        for(String name : PROPNAMES){
+            String val;
+            try{
+                val = System.getProperty(name);
+            }catch(SecurityException e){
+                continue;
+            }
+            if(val == null) continue;
+            propmap.put(name, val);
         }
+
+        SortedMap<String, String> result;
+        result = Collections.unmodifiableSortedMap(propmap);
+
+        return result;
+    }
+
+    /**
+     * クラスパスリストを作成する。
+     *
+     * @param classpath 連結クラスパス値
+     * @return クラスパスリスト
+     */
+    private static List<String> buildClassPathList(String classpath){
+        String[] pathArray;
+        if(classpath != null){
+            pathArray = classpath.split(File.pathSeparator);
+        }else{
+            pathArray = new String[0];
+        }
+
+        List<String> result;
+        result = Arrays.asList(pathArray);
+        result = Collections.unmodifiableList(result);
+
         return result;
     }
 
@@ -111,44 +164,74 @@ public final class EnvInfo{
      */
     public static String getVMInfo(){
         StringBuilder result = new StringBuilder();
-        NumberFormat nform = NumberFormat.getNumberInstance();
 
-        result.append("最大ヒープメモリ量: ")
-              .append(nform.format(MAX_MEMORY))
-              .append(" bytes\n");
+        String memform = MessageFormat.format(FORM_MEM, MAX_MEMORY);
+        result.append(memform).append(NL);
+
+        result.append(getSysPropInfo()).append(NL);
+        result.append(getEnvInfo()).append(NL);
+        result.append(getClassPathInfo()).append(NL);
 
-        result.append("\n");
+        return result.toString();
+    }
 
+    /**
+     * システムプロパティ要覧を返す。
+     *
+     * <p>java.class.pathの値は除く。
+     *
+     * @return システムプロパティ要覧
+     */
+    private static CharSequence getSysPropInfo(){
+        StringBuilder result = new StringBuilder();
         result.append("主要システムプロパティ:\n");
-        Set<String> propKeys = PROPERTY_MAP.keySet();
-        for(String propKey : propKeys){
-            if(propKey.equals("java.class.path")) continue;
-            String value = PROPERTY_MAP.get(propKey);
-            result.append("  ");
-            result.append(propKey).append("=").append(value).append("\n");
-        }
 
-        result.append("\n");
+        PROPERTY_MAP.entrySet().stream()
+                .filter(entry -> ! entry.getKey().equals("java.class.path"))
+                .forEachOrdered(entry -> {
+                    result.append(INDENT);
+                    result.append(entry.getKey());
+                    result.append('=');
+                    result.append(entry.getValue());
+                    result.append(NL);
+                });
 
-        result.append("主要環境変数:\n");
-        Set<String> envKeys = ENVIRONMENT_MAP.keySet();
-        for(String envKey : envKeys){
-            String value = ENVIRONMENT_MAP.get(envKey);
-            result.append("  ");
-            result.append(envKey).append("=").append(value).append("\n");
-        }
+        return result;
+    }
 
-        result.append("\n");
+    /**
+     * 環境変数要覧を返す。
+     *
+     * @return 環境変数要覧
+     */
+    private static CharSequence getEnvInfo(){
+        StringBuilder result = new StringBuilder("主要環境変数:\n");
+
+        ENVIRONMENT_MAP.entrySet().stream()
+                .forEachOrdered(entry -> {
+                    result.append(INDENT);
+                    result.append(entry.getKey());
+                    result.append('=');
+                    result.append(entry.getValue());
+                    result.append(NL);
+                });
 
-        result.append("クラスパス:\n");
-        for(String path : CLASSPATHS){
-            result.append("  ");
-            result.append(path).append("\n");
-        }
+        return result;
+    }
 
-        result.append("\n");
+    /**
+     * クラスパス情報要覧を返す。
+     *
+     * @return クラスパス情報要覧
+     */
+    private static CharSequence getClassPathInfo(){
+        StringBuilder result = new StringBuilder("クラスパス:\n");
 
-        return result.toString();
+        CLASSPATHS.stream().forEachOrdered(path -> {
+            result.append(INDENT).append(path).append(NL);
+        });
+
+        return result;
     }
 
 }
index 1912568..2bf135c 100644 (file)
@@ -11,6 +11,9 @@ import java.io.File;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.security.CodeSource;
 import java.security.ProtectionDomain;
 import java.util.Locale;
@@ -44,79 +47,6 @@ public final class FileUtils{
 
 
     /**
-     * なるべく自分にだけ読み書き許可を与え
-     * 自分以外には読み書き許可を与えないように
-     * ファイル属性を操作する。
-     *
-     * @param file 操作対象ファイル
-     * @return 成功すればtrue
-     * @throws SecurityException セキュリティ上の許可が無い場合
-     */
-    public static boolean setOwnerOnlyAccess(File file)
-            throws SecurityException{
-        boolean result = true;
-
-        result &= file.setReadable(false, false);
-        result &= file.setReadable(true,  true);
-
-        result &= file.setWritable(false, false);
-        result &= file.setWritable(true, true);
-
-        return result;
-    }
-
-    /**
-     * 任意の絶対パスの祖先の内、存在するもっとも近い祖先を返す。
-     *
-     * @param file 任意の絶対パス
-     * @return 存在するもっとも近い祖先。一つも存在しなければnull。
-     * @throws IllegalArgumentException 引数が絶対パスでない
-     */
-    public static File findExistsAncestor(File file)
-            throws IllegalArgumentException{
-        if(file == null) return null;
-        if( ! file.isAbsolute() ) throw new IllegalArgumentException();
-        if(file.exists()) return file;
-        File parent = file.getParentFile();
-        return findExistsAncestor(parent);
-    }
-
-    /**
-     * 任意の絶対パスのルートファイルシステムもしくはドライブレターを返す。
-     *
-     * @param file 任意の絶対パス
-     * @return ルートファイルシステムもしくはドライブレター
-     * @throws IllegalArgumentException 引数が絶対パスでない
-     */
-    public static File findRootFile(File file)
-            throws IllegalArgumentException{
-        if( ! file.isAbsolute() ) throw new IllegalArgumentException();
-        File parent = file.getParentFile();
-        if(parent == null) return file;
-        return findRootFile(parent);
-    }
-
-    /**
-     * 相対パスの絶対パス化を試みる。
-     *
-     * @param file 対象パス
-     * @return 絶対パス。絶対化に失敗した場合は元の引数。
-     */
-    public static File supplyFullPath(File file){
-        if(file.isAbsolute()) return file;
-
-        File absFile;
-
-        try{
-            absFile = file.getAbsoluteFile();
-        }catch(SecurityException e){
-            return file;
-        }
-
-        return absFile;
-    }
-
-    /**
      * 任意のディレクトリがアクセス可能な状態にあるか判定する。
      *
      * <p>アクセス可能の条件を満たすためには、与えられたパスが
@@ -131,39 +61,49 @@ public final class FileUtils{
      * @param path 任意のディレクトリ
      * @return アクセス可能ならtrue
      */
-    public static boolean isAccessibleDirectory(File path){
+    public static boolean isAccessibleDirectory(Path path){
         if(path == null) return false;
 
-        boolean result = true;
-
-        if     ( ! path.exists() )      result = false;
-        else if( ! path.isDirectory() ) result = false;
-        else if( ! path.canRead() )     result = false;
-        else if( ! path.canWrite() )    result = false;
+        boolean result =
+                   Files.exists(path)
+                && Files.isDirectory(path)
+                && Files.isReadable(path)
+                && Files.isWritable(path);
 
         return result;
     }
 
     /**
-     * クラスがローカルファイルからロードされたのであれば
-     * そのファイルを返す。
+     * クラスのロード元のURLを返す。
      *
-     * @param klass 任意のクラス
-     * @return ロード元ファイル。見つからなければnull。
+     * @param klass クラス
+     * @return ロード元URL。不明ならnull
      */
-    public static File getClassSourceFile(Class<?> klass){
-        ProtectionDomain domain;
-        try{
-            domain = klass.getProtectionDomain();
-        }catch(SecurityException e){
-            return null;
-        }
+    public static URL getClassSourceUrl(Class<?> klass){
+        ProtectionDomain domain = klass.getProtectionDomain();
+        if(domain == null) return null;
 
         CodeSource src = domain.getCodeSource();
+        if(src == null) return null;
 
         URL location = src.getLocation();
+
+        return location;
+    }
+
+    /**
+     * クラスがローカルファイルからロードされたのであれば
+     * その絶対パスを返す。
+     *
+     * @param klass 任意のクラス
+     * @return ロード元ファイルの絶対パス。見つからなければnull。
+     */
+    public static Path getClassSourcePath(Class<?> klass){
+        URL location = getClassSourceUrl(klass);
+        if(location == null) return null;
+
         String scheme = location.getProtocol();
-        if( ! scheme.equals(SCHEME_FILE) ) return null;
+        if( ! SCHEME_FILE.equalsIgnoreCase(scheme) ) return null;
 
         URI uri;
         try{
@@ -173,81 +113,68 @@ public final class FileUtils{
             return null;
         }
 
-        File file = new File(uri);
+        Path result = Paths.get(uri);
+        result = result.toAbsolutePath();
 
-        return file;
+        return result;
     }
 
     /**
      * すでに存在するJARファイルか判定する。
      *
-     * @param file 任意のファイル
+     * <p>ファイルがすでに通常ファイルとしてローカルに存在し、
+     * ファイル名の拡張子が「.jar」であれば真と判定される。
+     *
+     * @param path 任意のファイル
      * @return すでに存在するJARファイルであればtrue
      */
-    public static boolean isExistsJarFile(File file){
-        if(file == null) return false;
-        if( ! file.exists() ) return false;
-        if( ! file.isFile() ) return false;
-
-        String name = file.getName();
-        if( ! name.matches("^.+\\.[jJ][aA][rR]$") ) return false;
+    public static boolean isExistsJarFile(Path path){
+        if(path == null) return false;
+        if( ! Files.exists(path) ) return false;
+        if( ! Files.isRegularFile(path) ) return false;
 
-        // TODO ファイル先頭マジックナンバーのテストも必要?
+        Path leaf = path.getFileName();
+        assert leaf != null;
+        String leafName = leaf.toString();
+        boolean result = leafName.matches("^.+\\.[jJ][aA][rR]$");
 
-        return true;
+        return result;
     }
 
     /**
      * クラスがローカルJARファイルからロードされたのであれば
-     * その格納ディレクトリを返す。
+     * ã\81\9dã\81®æ ¼ç´\8dã\83\87ã\82£ã\83¬ã\82¯ã\83\88ã\83ªã\81®çµ¶å¯¾ã\83\91ã\82¹ã\82\92è¿\94ã\81\99ã\80\82
      *
      * @param klass 任意のクラス
-     * @return ロード元JARファイルの格納ディレクトリ。
+     * @return ã\83­ã\83¼ã\83\89å\85\83JARã\83\95ã\82¡ã\82¤ã\83«ã\81®æ ¼ç´\8dã\83\87ã\82£ã\83¬ã\82¯ã\83\88ã\83ªã\81®çµ¶å¯¾ã\83\91ã\82¹ã\80\82
      *     JARが見つからない、もしくはロード元がJARファイルでなければnull。
      */
-    public static File getJarDirectory(Class<?> klass){
-        File jarFile = getClassSourceFile(klass);
+    public static Path getJarDirectory(Class<?> klass){
+        Path jarFile = getClassSourcePath(klass);
         if(jarFile == null) return null;
 
         if( ! isExistsJarFile(jarFile) ){
             return null;
         }
 
-        return jarFile.getParentFile();
+        Path result = jarFile.getParent();
+        assert result.isAbsolute();
+
+        return result;
     }
 
     /**
      * このクラスがローカルJARファイルからロードされたのであれば
-     * その格納ディレクトリを返す。
+     * ã\81\9dã\81®æ ¼ç´\8dã\83\87ã\82£ã\83¬ã\82¯ã\83\88ã\83ªã\81®çµ¶å¯¾ã\83\91ã\82¹ã\82\92è¿\94ã\81\99ã\80\82
      *
-     * @return ロード元JARファイルの格納ディレクトリ。
+     * @return ã\83­ã\83¼ã\83\89å\85\83JARã\83\95ã\82¡ã\82¤ã\83«ã\81®æ ¼ç´\8dã\83\87ã\82£ã\83¬ã\82¯ã\83\88ã\83ªã\81®çµ¶å¯¾ã\83\91ã\82¹ã\80\82
      *     JARが見つからない、もしくはロード元がJARファイルでなければnull。
      */
-    public static File getJarDirectory(){
+    public static Path getJarDirectory(){
         return getJarDirectory(THISKLASS);
     }
 
     /**
-     * ホームディレクトリを得る。
-     *
-     * <p>システムプロパティuser.homeで示されたホームディレクトリを返す。
-     *
-     * @return ホームディレクトリ。何らかの事情でnullを返す場合もあり。
-     */
-    public static File getHomeDirectory(){
-        String homeProp;
-        try{
-            homeProp = System.getProperty("user.home");
-        }catch(SecurityException e){
-            return null;
-        }
-
-        File homeFile = new File(homeProp);
-
-        return homeFile;
-    }
-
-    /**
      * MacOSX環境か否か判定する。
      *
      * @return MacOSX環境ならtrue
@@ -255,22 +182,13 @@ public final class FileUtils{
     public static boolean isMacOSXFs(){
         if(File.separatorChar != '/') return false;
 
-        String osName;
-        try{
-            osName = System.getProperty(SYSPROP_OSNAME);
-        }catch(SecurityException e){
-            return false;
-        }
-
+        String osName = System.getProperty(SYSPROP_OSNAME);
         if(osName == null) return false;
 
         osName = osName.toLowerCase(Locale.ROOT);
 
-        if(osName.startsWith("mac os x")){
-            return true;
-        }
-
-        return false;
+        boolean result = osName.startsWith("mac os x");
+        return result;
     }
 
     /**
@@ -281,48 +199,12 @@ public final class FileUtils{
     public static boolean isWindowsOSFs(){
         if(File.separatorChar != '\\') return false;
 
-        String osName;
-        try{
-            osName = System.getProperty(SYSPROP_OSNAME);
-        }catch(SecurityException e){
-            return false;
-        }
-
+        String osName = System.getProperty(SYSPROP_OSNAME);
         if(osName == null) return false;
 
         osName = osName.toLowerCase(Locale.ROOT);
 
-        if(osName.startsWith("windows")){
-            return true;
-        }
-
-        return false;
-    }
-
-    /**
-     * アプリケーション設定ディレクトリを返す。
-     *
-     * <p>存在の有無、アクセスの可否は関知しない。
-     *
-     * <p>WindowsやLinuxではホームディレクトリ。
-     * Mac OS X ではさらにホームディレクトリの下の
-     * "Library/Application Support/"
-     *
-     * @return アプリケーション設定ディレクトリ
-     */
-    public static File getAppSetDir(){
-        File home = getHomeDirectory();
-        if(home == null) return null;
-
-        File result = home;
-
-        if(isMacOSXFs()){
-            result = new File(result, "Library");
-            result = new File(result, "Application Support");
-        }
-
-        // TODO Win環境での%APPDATA%サポート
-
+        boolean result = osName.startsWith("windows");
         return result;
     }
 
@@ -331,11 +213,11 @@ public final class FileUtils{
      *
      * <p>Windows日本語環境では、バックスラッシュ記号が円通貨記号に置換される。
      *
-     * @param file 対象ファイル
+     * @param path 対象ファイル
      * @return HTML文字列断片
      */
-    public static String getHtmledFileName(File file){
-        String pathName = file.getPath();
+    public static String getHtmledFileName(Path path){
+        String pathName = path.toString();
 
         Locale locale = Locale.getDefault();
         String lang = locale.getLanguage();
diff --git a/src/main/java/jp/sfjp/jindolf/config/JsonIo.java b/src/main/java/jp/sfjp/jindolf/config/JsonIo.java
new file mode 100644 (file)
index 0000000..26eff4a
--- /dev/null
@@ -0,0 +1,339 @@
+/*
+ * JSON I/O
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.config;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jp.sourceforge.jovsonz.JsComposition;
+import jp.sourceforge.jovsonz.JsObject;
+import jp.sourceforge.jovsonz.JsParseException;
+import jp.sourceforge.jovsonz.JsTypes;
+import jp.sourceforge.jovsonz.JsVisitException;
+import jp.sourceforge.jovsonz.Json;
+
+/**
+ * JSONファイルの入出力。
+ */
+public class JsonIo {
+
+    /** 検索履歴ファイル。 */
+    public static final Path HIST_FILE = Paths.get("searchHistory.json");
+    /** ネットワーク設定ファイル。 */
+    public static final Path NETCONFIG_FILE = Paths.get("netconfig.json");
+    /** 台詞表示設定ファイル。 */
+    public static final Path TALKCONFIG_FILE = Paths.get("talkconfig.json");
+
+    /** ローカル画像設定ファイル。 */
+    public static final Path LOCALIMGCONFIG_PATH =
+            Paths.get("avatarCache.json");
+
+
+    private static final Charset CHARSET_JSON = StandardCharsets.UTF_8;
+    private static final Logger LOGGER = Logger.getAnonymousLogger();
+
+
+    private final ConfigStore configStore;
+
+
+    /**
+     * Constructor.
+     *
+     * @param configStore 設定ディレクトリ
+     */
+    public JsonIo(ConfigStore configStore){
+        super();
+        Objects.nonNull(configStore);
+        this.configStore = configStore;
+        return;
+    }
+
+
+    /**
+     * 設定ディレクトリ上のOBJECT型JSONファイルを読み込む。
+     *
+     * @param file JSONファイルの相対パス。
+     * @return JSON object。
+     *     設定ディレクトリを使わない設定、
+     *     もしくはJSONファイルが存在しない、
+     *     もしくはOBJECT型でなかった、
+     *     もしくは入力エラーがあればnull
+     */
+    public JsObject loadJsObject(Path file){
+        if( ! this.configStore.useStoreFile()){
+            return null;
+        }
+
+        JsComposition<?> root = loadJson(file);
+        if(root == null || root.getJsTypes() != JsTypes.OBJECT) return null;
+        JsObject result = (JsObject) root;
+        return result;
+    }
+
+    /**
+     * 設定ディレクトリ上のJSONファイルを読み込む。
+     *
+     * @param file JSONファイルの相対パス
+     * @return JSON objectまたはarray。
+     *     設定ディレクトリを使わない設定、
+     *     もしくはJSONファイルが存在しない、
+     *     もしくは入力エラーがあればnull
+     */
+    protected JsComposition<?> loadJson(Path file){
+        Path absFile;
+        if(file.isAbsolute()){
+            absFile = file;
+        }else{
+            Path configDir = this.configStore.getConfigDir();
+            if(configDir == null) return null;
+            absFile = configDir.resolve(file);
+            if( ! Files.exists(absFile) ) return null;
+            if( ! absFile.isAbsolute() ) return null;
+        }
+        String absPath = absFile.toString();
+
+        JsComposition<?> root;
+        try(InputStream is = Files.newInputStream(absFile)){
+            InputStream bis = new BufferedInputStream(is);
+            root = loadJson(bis);
+        }catch(IOException e){
+            LOGGER.log(Level.SEVERE,
+                    "JSONファイル["
+                    + absPath
+                    + "]の読み込み時に支障がありました。", e);
+            return null;
+        }catch(JsParseException e){
+            LOGGER.log(Level.SEVERE,
+                    "JSONファイル["
+                    + absPath
+                    + "]の内容に不備があります。", e);
+            return null;
+        }
+
+        return root;
+    }
+
+    /**
+     * バイトストリーム上のJSONデータを読み込む。
+     *
+     * <p>バイトストリームはUTF-8と解釈される。
+     *
+     * @param is バイトストリーム
+     * @return JSON objectまたはarray。
+     * @throws IOException 入力エラー
+     * @throws JsParseException 構文エラー
+     */
+    protected JsComposition<?> loadJson(InputStream is)
+            throws IOException, JsParseException {
+        Reader reader = new InputStreamReader(is, CHARSET_JSON);
+        reader = new BufferedReader(reader);
+        JsComposition<?> root = loadJson(reader);
+        return root;
+    }
+
+    /**
+     * 文字ストリーム上のJSONデータを読み込む。
+     *
+     * @param reader 文字ストリーム
+     * @return JSON objectまたはarray。
+     * @throws IOException 入力エラー
+     * @throws JsParseException 構文エラー
+     */
+    protected JsComposition<?> loadJson(Reader reader)
+            throws IOException, JsParseException {
+        JsComposition<?> root = Json.parseJson(reader);
+        return root;
+    }
+
+    /**
+     * 設定ディレクトリ上のJSONファイルに書き込む。
+     *
+     * @param file JSONファイルの相対パス
+     * @param root JSON objectまたはarray
+     * @return 正しくセーブが行われればtrue。
+     *     何らかの理由でセーブが完了できなければfalse
+     */
+    public boolean saveJson(Path file, JsComposition<?> root){
+        if( ! this.configStore.useStoreFile()){
+            return false;
+        }
+
+        // TODO テンポラリファイルを用いたより安全なファイル更新
+        Path configDir = this.configStore.getConfigDir();
+        Path absFile = configDir.resolve(file);
+        String absPath = absFile.toString();
+
+        try{
+            Files.deleteIfExists(absFile);
+        }catch(IOException e){
+            // NOTHING
+            assert true;
+        }
+
+        try{
+            Files.createFile(absFile);
+        }catch(IOException e){
+            LOGGER.log(Level.SEVERE,
+                    "JSONファイル["
+                    + absPath
+                    + "]の新規生成ができません。", e);
+            return false;
+        }
+
+        try(OutputStream os = Files.newOutputStream(absFile)){
+            OutputStream bos = new BufferedOutputStream(os);
+            saveJson(bos, root);
+        }catch(IOException e){
+            LOGGER.log(Level.SEVERE,
+                    "JSONファイル["
+                    + absPath
+                    + "]の書き込み時に支障がありました。", e);
+            return false;
+        }catch(JsVisitException e){
+            LOGGER.log(Level.SEVERE,
+                    "JSONファイル["
+                    + absPath
+                    + "]の出力処理で支障がありました。", e);
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * バイトストリームにJSONデータを書き込む。
+     *
+     * <p>バイトストリームはUTF-8と解釈される。
+     *
+     * @param os バイトストリーム出力
+     * @param root JSON objectまたはarray
+     * @throws IOException 出力エラー
+     * @throws JsVisitException 構造エラー
+     */
+    protected void saveJson(OutputStream os, JsComposition<?> root)
+            throws IOException, JsVisitException {
+        Writer writer = new OutputStreamWriter(os, CHARSET_JSON);
+        writer = new BufferedWriter(writer);
+        saveJson(writer, root);
+        return;
+    }
+
+    /**
+     * 文字ストリームにJSONデータを書き込む。
+     *
+     * @param writer 文字ストリーム出力
+     * @param root JSON objectまたはarray
+     * @throws IOException 出力エラー
+     * @throws JsVisitException 構造エラー
+     */
+    protected void saveJson(Writer writer, JsComposition<?> root)
+            throws IOException, JsVisitException {
+        Json.dumpJson(writer, root);
+        return;
+    }
+
+    /**
+     * 検索履歴ファイルを読み込む。
+     *
+     * @return 履歴データ。履歴を読まないもしくは読めない場合はnull
+     */
+    public JsObject loadHistoryConfig(){
+        JsObject result = loadJsObject(HIST_FILE);
+        return result;
+    }
+
+    /**
+     * ネットワーク設定ファイルを読み込む。
+     *
+     * @return ネットワーク設定データ。
+     *     設定を読まないもしくは読めない場合はnull
+     */
+    public JsObject loadNetConfig(){
+        JsObject result = loadJsObject(NETCONFIG_FILE);
+        return result;
+    }
+
+    /**
+     * 台詞表示設定ファイルを読み込む。
+     *
+     * @return 台詞表示設定データ。
+     *     設定を読まないもしくは読めない場合はnull
+     */
+    public JsObject loadTalkConfig(){
+        JsObject result = loadJsObject(TALKCONFIG_FILE);
+        return result;
+    }
+
+    /**
+     * ローカル画像設定ファイルを読み込む。
+     *
+     * @return ローカル画像設定データ。
+     *     設定を読まないもしくは読めない場合はnull
+     */
+    public JsObject loadLocalImgConfig(){
+        if( ! this.configStore.useStoreFile()){
+            return null;
+        }
+
+        Path imgDir = this.configStore.getLocalImgDir();
+        Path path = imgDir.resolve(LOCALIMGCONFIG_PATH);
+        JsObject result = loadJsObject(path);
+
+        return result;
+    }
+
+    /**
+     * 検索履歴ファイルに書き込む。
+     *
+     * @param root 履歴データ
+     * @return 書き込まなかったもしくは書き込めなかった場合はfalse
+     */
+    public boolean saveHistoryConfig(JsComposition<?> root){
+        boolean result = saveJson(HIST_FILE, root);
+        return result;
+    }
+
+    /**
+     * ネットワーク設定ファイルに書き込む。
+     *
+     * @param root ネットワーク設定
+     * @return 書き込まなかったもしくは書き込めなかった場合はfalse
+     */
+    public boolean saveNetConfig(JsComposition<?> root){
+        boolean result = saveJson(NETCONFIG_FILE, root);
+        return result;
+    }
+
+    /**
+     * 台詞表示設定ファイルに書き込む。
+     *
+     * @param root 台詞表示設定
+     * @return 書き込まなかったもしくは書き込めなかった場合はfalse
+     */
+    public boolean saveTalkConfig(JsComposition<?> root){
+        boolean result = saveJson(TALKCONFIG_FILE, root);
+        return result;
+    }
+
+}
index bef7def..b93fb13 100644 (file)
@@ -24,6 +24,10 @@ import java.util.regex.Pattern;
  */
 public class OptionInfo{
 
+    /*
+        ex) 1000x800
+        ex) 1290x1024-256+128
+    */
     private static final String REGEX_DIMNO =
             "([1-9][0-9]{0,5})";
     private static final String REGEX_SIGN =
@@ -96,9 +100,9 @@ public class OptionInfo{
      * @throws IllegalArgumentException 引数の構文エラー
      */
     private static void parseBooleanSwitch(OptionInfo info,
-                                              CmdOption option,
-                                              String optTxt,
-                                              String onoff )
+                                           CmdOption option,
+                                           String optTxt,
+                                           String onoff )
             throws IllegalArgumentException{
         Boolean flag;
 
@@ -128,8 +132,8 @@ public class OptionInfo{
      * @throws IllegalArgumentException 引数の構文エラー
      */
     private static void parseGeometry(OptionInfo info,
-                                        String optTxt,
-                                        String geometry )
+                                      String optTxt,
+                                      String geometry )
             throws IllegalArgumentException{
         Matcher matcher = PATTERN_GEOMETRY.matcher(geometry);
         if( ! matcher.matches() ){
@@ -176,18 +180,17 @@ public class OptionInfo{
      * @throws IllegalArgumentException オプションの引数がない
      */
     private static void parseOptionArg(OptionInfo info,
-                                         String optTxt,
-                                         CmdOption option,
-                                         Iterator<String> iterator )
+                                       String optTxt,
+                                       CmdOption option,
+                                       Iterator<String> iterator )
             throws IllegalArgumentException {
-        String nextArg;
-        if(iterator.hasNext()){
-            nextArg = iterator.next();
-        }else{
+        if( ! iterator.hasNext()){
             String errMsg = MessageFormat.format(ERRFORM_NOARG, optTxt);
             throw new IllegalArgumentException(errMsg);
         }
 
+        String nextArg = iterator.next();
+
         if(option == CmdOption.OPT_GEOMETRY){
             parseGeometry(info, optTxt, nextArg);
         }else if(option.isBooleanOption()){
@@ -254,8 +257,8 @@ public class OptionInfo{
      * @return 指定されていたらtrue
      */
     public boolean hasOption(CmdOption option){
-        if(this.optionList.contains(option)) return true;
-        return false;
+        boolean result = this.optionList.contains(option);
+        return result;
     }
 
     /**
index e74b936..cb0b1c0 100644 (file)
@@ -12,8 +12,8 @@ import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URL;
+import java.util.ArrayList;
 import java.util.Collections;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -22,6 +22,8 @@ import jp.sourceforge.jindolf.corelib.LandDef;
 
 /**
  * いわゆる「国」。
+ *
+ * 人狼BBSのサーバと1:1の概念。
  */
 public class Land {
 
@@ -31,11 +33,12 @@ public class Land {
     private final LandDef landDef;
     private final ServerAccess serverAccess;
 
-    private final List<Village> villageList = new LinkedList<>();
+    private final List<Village> villageList = new ArrayList<>(1000);
 
 
     /**
      * コンストラクタ。
+     *
      * @param landDef 国定義
      * @throws java.lang.IllegalArgumentException 不正な国定義
      */
@@ -58,6 +61,7 @@ public class Land {
 
     /**
      * 国定義を得る。
+     *
      * @return 国定義
      */
     public LandDef getLandDef(){
@@ -66,6 +70,7 @@ public class Land {
 
     /**
      * サーバ接続を返す。
+     *
      * @return ServerAccessインスタンス
      */
     public ServerAccess getServerAccess(){
@@ -74,6 +79,7 @@ public class Land {
 
     /**
      * 指定されたインデックス位置の村を返す。
+     *
      * @param index 0から始まるインデックス値
      * @return 村
      */
@@ -87,6 +93,7 @@ public class Land {
 
     /**
      * 村の総数を返す。
+     *
      * @return 村の総数
      */
     public int getVillageCount(){
@@ -96,6 +103,7 @@ public class Land {
 
     /**
      * 村のリストを返す。
+     *
      * @return 村のリスト
      */
     // TODO インスタンス変数でいいはず。
@@ -105,7 +113,9 @@ public class Land {
 
     /**
      * 絶対または相対URLの指すパーマネントなイメージ画像をダウンロードする。
-     * ※ A,B,D 国の顔アイコンは絶対パスらしい…。
+     *
+     * <p>※ A,B,D 国の顔アイコンは絶対パスらしい…。
+     *
      * @param imageURL 画像URL文字列
      * @return 画像イメージ
      */
@@ -126,6 +136,7 @@ public class Land {
 
     /**
      * 墓アイコンイメージを取得する。
+     *
      * @return 墓アイコンイメージ
      */
     public BufferedImage getGraveIconImage(){
@@ -136,6 +147,7 @@ public class Land {
 
     /**
      * 墓アイコンイメージ(大)を取得する。
+     *
      * @return 墓アイコンイメージ(大)
      */
     public BufferedImage getGraveBodyImage(){
@@ -146,6 +158,7 @@ public class Land {
 
     /**
      * 村リストを更新する。
+     *
      * @param vset ソート済みの村一覧
      */
     public void updateVillageList(List<Village> vset){
@@ -157,6 +170,7 @@ public class Land {
 
     /**
      * 国の文字列表現を返す。
+     *
      * @return 文字列表現
      */
     @Override
index 1e33b81..f979032 100644 (file)
@@ -7,12 +7,13 @@
 
 package jp.sfjp.jindolf.data;
 
+import java.text.MessageFormat;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.logging.Logger;
+import java.util.Objects;
 import javax.swing.event.EventListenerList;
 import javax.swing.event.TreeModelEvent;
 import javax.swing.event.TreeModelListener;
@@ -21,101 +22,161 @@ import javax.swing.tree.TreePath;
 import jp.sourceforge.jindolf.corelib.LandDef;
 
 /**
- * 国の集合。あらゆるデータモデルの大元。
- * 国一覧と村一覧を管理。
- * JTreeのモデルも兼用。
+ * {@link javax.swing.JTree}のモデルとして国一覧と村一覧を管理。
+ *
+ * <p>ツリー階層は ROOT - 国 - 範囲セクション - 村 の4階層。
+ *
+ * <p>昇順/降順の切り替えをサポート。
  */
-public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付けるか?
+public class LandsTreeModel implements TreeModel{
 
-    private static final String ROOT = "ROOT";
+    private static final Object ROOT = new Object();
     private static final int SECTION_INTERVAL = 100;
 
-    private static final Logger LOGGER = Logger.getAnonymousLogger();
-
 
-    private final List<Land> landList = new LinkedList<>();
-    private final List<Land> unmodList =
-            Collections.unmodifiableList(this.landList);
-    private final Map<Land, List<VillageSection>> sectionMap =
-            new HashMap<>();
-    private boolean isLandListLoaded = false;
+    private final EventListenerList listeners;
 
-    private final EventListenerList listeners = new EventListenerList();
+    private final List<Land> landList;
+    private final Map<Land, List<VillageSection> > sectionMap;
 
     private boolean ascending = false;
 
+
     /**
      * コンストラクタ。
-     * この時点ではまだ国一覧が読み込まれない。
      */
     public LandsTreeModel(){
         super();
+
+        this.listeners = new EventListenerList();
+
+        this.landList = buildLandList();
+        this.sectionMap = new HashMap<>();
+
         return;
     }
 
+
     /**
-     * 指定した国の村一覧を更新しイベントを投げる。
-     * @param land 国
+     * ツリーのルートオブジェクトか否か判定する。
+     *
+     * @param obj オブジェクト
+     * @return ルートならtrue
      */
-    public void updateVillageList(Land land){
-        List<VillageSection> sectionList =
-                VillageSection.getSectionList(land, SECTION_INTERVAL);
-        this.sectionMap.put(land, sectionList);
+    private static boolean isRoot(Object obj){
+        boolean result = Objects.equals(ROOT, obj);
+        return result;
+    }
 
-        int[] childIndices = new int[sectionList.size()];
-        for(int ct = 0; ct < childIndices.length; ct++){
-            childIndices[ct] = ct;
-        }
-        Object[] children = sectionList.toArray();
+    /**
+     * 国一覧を読み込む。
+     *
+     * <p>村一覧はまだ読み込まれない。
+     *
+     * @return 国リスト
+     */
+    private static List<Land> buildLandList(){
+        List<LandDef> landDefList = CoreData.getLandDefList();
+        List<Land> newList = new ArrayList<>(landDefList.size());
 
-        Object[] path = {ROOT, land};
-        TreePath treePath = new TreePath(path);
-        TreeModelEvent event = new TreeModelEvent(this,
-                                                  treePath,
-                                                  childIndices,
-                                                  children     );
-        fireTreeStructureChanged(event);
+        landDefList.stream().map(landDef ->
+            new Land(landDef)
+        ).forEachOrdered(land -> {
+            newList.add(land);
+        });
 
-        return;
+        return Collections.unmodifiableList(newList);
     }
 
     /**
-     * 国一覧を読み込む。
+     * 与えられた国の全ての村を指定されたinterval間隔で格納するために、
+     * 範囲セクションのリストを生成する。
+     *
+     * @param land 国
+     * @param interval 範囲セクション間の村ID間隔
+     * @return 範囲セクションのリスト
+     * @throws java.lang.IllegalArgumentException intervalが正でない
      */
-    // TODO static にできない?
-    public void loadLandList(){
-        if(this.isLandListLoaded) return;
+    private static List<VillageSection> getSectionList(Land land,
+                                                       int interval )
+            throws IllegalArgumentException{
+        if(interval <= 0){
+            throw new IllegalArgumentException();
+        }
 
-        this.landList.clear();
+        String pfx = land.getLandDef().getLandPrefix();
+        List<Village> span = new ArrayList<>(interval);
 
-        List<LandDef> landDefList = CoreData.getLandDefList();
-        landDefList.stream().map((landDef) ->
-            new Land(landDef)
-        ).forEachOrdered((land) -> {
-            this.landList.add(land);
-        });
+        List<VillageSection> result = new ArrayList<>(2500 / interval);
 
-        this.isLandListLoaded = true;
+        boolean loop1st = true;
+        int rangeStart = -1;
+        int rangeEnd = -1;
 
-        fireLandListChanged();
+        for(Village village : land.getVillageList()){
+            int vid = village.getVillageIDNum();
 
-        return;
+            if(loop1st){
+                rangeStart = vid / interval * interval;
+                rangeEnd = rangeStart + interval - 1;
+                loop1st = false;
+            }
+
+            if(rangeEnd < vid){
+                VillageSection section = new VillageSection(
+                        pfx, rangeStart, rangeEnd, span);
+                span.clear();
+                result.add(section);
+
+                rangeStart = vid / interval * interval;
+                rangeEnd = rangeStart + interval - 1;
+            }
+
+            span.add(village);
+        }
+
+        if( ! span.isEmpty()){
+            VillageSection section = new VillageSection(
+                    pfx, rangeStart, rangeEnd, span);
+            span.clear();
+            result.add(section);
+        }
+
+        return result;
     }
 
+
     /**
-     * ツリー内容が更新された事をリスナーに通知する。
+     * 国リストを得る。
+     *
+     * @return 国のリスト
      */
-    private void fireLandListChanged(){
-        int size = this.landList.size();
-        int[] childIndices = new int[size];
-        for(int ct = 0; ct < size; ct++){
-            int index = ct;
-            childIndices[ct] = index;
-        }
+    public List<Land> getLandList(){
+        return this.landList;
+    }
 
-        Object[] children = this.landList.toArray();
+    /**
+     * 指定した国の村一覧でツリーリストを更新し、
+     * 更新イベントをリスナに投げる。
+     *
+     * <p>2020-04現在、もはや村一覧が増減することはない。
+     *
+     * @param land 国
+     */
+    public void updateVillageList(Land land){
+        List<VillageSection> sectionList =
+                getSectionList(land, SECTION_INTERVAL);
+        this.sectionMap.put(land, sectionList);
+
+        int[] childIndices = new int[sectionList.size()];
+        for(int ct = 0; ct < childIndices.length; ct++){
+            childIndices[ct] = ct;
+        }
+        Object[] children = sectionList.toArray();
 
         TreePath treePath = new TreePath(ROOT);
+        treePath = treePath.pathByAddingChild(land);
+
         TreeModelEvent event = new TreeModelEvent(this,
                                                   treePath,
                                                   childIndices,
@@ -127,7 +188,9 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
 
     /**
      * ツリーの並び順を設定する。
-     * 場合によってはTreeModelEventが発生する。
+     *
+     * <p>場合によってはTreeModelEventが発生する。
+     *
      * @param ascending trueなら昇順
      */
     public void setAscending(boolean ascending){
@@ -141,26 +204,29 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
 
     /**
      * {@inheritDoc}
-     * @param l {@inheritDoc}
+     *
+     * @param lst {@inheritDoc}
      */
     @Override
-    public void addTreeModelListener(TreeModelListener l){
-        this.listeners.add(TreeModelListener.class, l);
+    public void addTreeModelListener(TreeModelListener lst){
+        this.listeners.add(TreeModelListener.class, lst);
         return;
     }
 
     /**
      * {@inheritDoc}
-     * @param l {@inheritDoc}
+     *
+     * @param lst {@inheritDoc}
      */
     @Override
-    public void removeTreeModelListener(TreeModelListener l){
-        this.listeners.remove(TreeModelListener.class, l);
+    public void removeTreeModelListener(TreeModelListener lst){
+        this.listeners.remove(TreeModelListener.class, lst);
         return;
     }
 
     /**
      * 登録中のリスナーのリストを得る。
+     *
      * @return リスナーのリスト
      */
     private TreeModelListener[] getTreeModelListeners(){
@@ -169,6 +235,7 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
 
     /**
      * 全リスナーにイベントを送出する。
+     *
      * @param event ツリーイベント
      */
     protected void fireTreeStructureChanged(TreeModelEvent event){
@@ -179,15 +246,31 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
     }
 
     /**
-     * 国リストを得る。
-     * @return 国のリスト
+     * ツリー内容の国一覧が更新された事をリスナーに通知する。
      */
-    public List<Land> getLandList(){
-        return this.unmodList;
+    private void fireLandListChanged(){
+        int size = getLandList().size();
+        int[] childIndices = new int[size];
+        for(int ct = 0; ct < size; ct++){
+            int index = ct;
+            childIndices[ct] = index;
+        }
+
+        Object[] children = getLandList().toArray();
+
+        TreePath treePath = new TreePath(ROOT);
+        TreeModelEvent event = new TreeModelEvent(this,
+                                                  treePath,
+                                                  childIndices,
+                                                  children     );
+        fireTreeStructureChanged(event);
+
+        return;
     }
 
     /**
      * {@inheritDoc}
+     *
      * @param parent {@inheritDoc}
      * @param index {@inheritDoc}
      * @return {@inheritDoc}
@@ -197,58 +280,63 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
         if(index < 0)                      return null;
         if(index >= getChildCount(parent)) return null;
 
-        if(parent == ROOT){
+        Object result = null;
+
+        if(isRoot(parent)){
             List<Land> list = getLandList();
             int landIndex = index;
             if( ! this.ascending) landIndex = list.size() - index - 1;
             Land land = list.get(landIndex);
-            return land;
-        }
-        if(parent instanceof Land){
+            result = land;
+        }else if(parent instanceof Land){
             Land land = (Land) parent;
             List<VillageSection> sectionList = this.sectionMap.get(land);
             int sectIndex = index;
             if( ! this.ascending) sectIndex = sectionList.size() - index - 1;
             VillageSection section = sectionList.get(sectIndex);
-            return section;
-        }
-        if(parent instanceof VillageSection){
+            result = section;
+        }else if(parent instanceof VillageSection){
             VillageSection section = (VillageSection) parent;
             int vilIndex = index;
             if( ! this.ascending){
                 vilIndex = section.getVillageCount() - index - 1;
             }
             Village village = section.getVillage(vilIndex);
-            return village;
+            result = village;
         }
-        return null;
+
+        return result;
     }
 
     /**
      * {@inheritDoc}
+     *
      * @param parent {@inheritDoc}
      * @return {@inheritDoc}
      */
     @Override
     public int getChildCount(Object parent){
-        if(parent == ROOT){
-            return getLandList().size();
-        }
-        if(parent instanceof Land){
+        int result = 0;
+
+        if(isRoot(parent)){
+            result = getLandList().size();
+        }else if(parent instanceof Land){
             Land land = (Land) parent;
             List<VillageSection> sectionList = this.sectionMap.get(land);
-            if(sectionList == null) return 0;
-            return sectionList.size();
-        }
-        if(parent instanceof VillageSection){
+            if(sectionList != null){
+                result = sectionList.size();
+            }
+        }else if(parent instanceof VillageSection){
             VillageSection section = (VillageSection) parent;
-            return section.getVillageCount();
+            result = section.getVillageCount();
         }
-        return 0;
+
+        return result;
     }
 
     /**
      * {@inheritDoc}
+     *
      * @param parent {@inheritDoc}
      * @param child {@inheritDoc}
      * @return {@inheritDoc}
@@ -256,32 +344,35 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
     @Override
     public int getIndexOfChild(Object parent, Object child){
         if(child == null) return -1;
-        if(parent == ROOT){
+
+        int result = -1;
+
+        if(isRoot(parent)){
             List<Land> list = getLandList();
             int index = list.indexOf(child);
             if( ! this.ascending) index = list.size() - index - 1;
-            return index;
-        }
-        if(parent instanceof Land){
+            result = index;
+        }else if(parent instanceof Land){
             Land land = (Land) parent;
             List<VillageSection> sectionList = this.sectionMap.get(land);
             int index = sectionList.indexOf(child);
             if( ! this.ascending) index = sectionList.size() - index - 1;
-            return index;
-        }
-        if(parent instanceof VillageSection){
+            result = index;
+        }else if(parent instanceof VillageSection){
             VillageSection section = (VillageSection) parent;
             int index = section.getIndexOfVillage(child);
             if( ! this.ascending){
                 index = section.getVillageCount() - index - 1;
             }
-            return index;
+            result = index;
         }
-        return -1;
+
+        return result;
     }
 
     /**
      * {@inheritDoc}
+     *
      * @return {@inheritDoc}
      */
     @Override
@@ -291,21 +382,24 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
 
     /**
      * {@inheritDoc}
+     *
      * @param node {@inheritDoc}
      * @return {@inheritDoc}
      */
     @Override
     public boolean isLeaf(Object node){
-        if(node == ROOT)                   return false;
-        if(node instanceof Land)           return false;
-        if(node instanceof VillageSection) return false;
         if(node instanceof Village)        return true;
+        if(node instanceof VillageSection) return false;
+        if(node instanceof Land)           return false;
+        if(isRoot(node))                   return false;
         return true;
     }
 
     /**
      * {@inheritDoc}
-     * ※ たぶん使わないので必ず失敗させている。
+     *
+     * <p>※ たぶん使わないので必ず失敗させている。
+     *
      * @param path {@inheritDoc}
      * @param newValue {@inheritDoc}
      */
@@ -314,121 +408,109 @@ public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付ける
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
+
     /**
      * 村IDで範囲指定した、村のセクション集合。国-村間の中間ツリー。
+     *
      * @see javax.swing.tree.TreeModel
      */
     private static final class VillageSection{
 
-        private final int startID;
-        private final int endID;
-        private final String prefix;
+        private static final String FORM_NODE =
+                "{0}{1,number,#} ~ {0}{2,number,#}";
+        private static final String FORM_NODE_G =
+                "{0}{1,number,#000} ~ {0}{2,number,#000}";
+
+
+        private final int startId;
+        private final int endId;
+
+        private final String text;
 
-        private final List<Village> villageList = new LinkedList<>();
+        private final List<Village> villageList;
 
 
         /**
          * セクション集合を生成する。
-         * @param land 国
-         * @param startID 開始村ID
-         * @param endID 終了村ID
+         *
+         * @param prefix 国名プレフィクス
+         * @param startId 区間開始村ID
+         * @param endId 区間終了村ID
+         * @param spanList 村の区間リスト
          * @throws java.lang.IndexOutOfBoundsException IDの範囲指定が変
          */
-        private VillageSection(Land land, int startID, int endID)
+        VillageSection(
+                String prefix, int startId, int endId, List<Village> spanList)
                 throws IndexOutOfBoundsException{
             super();
 
-            if(startID < 0 || startID > endID){
+            if(startId < 0 || startId > endId){
                 throw new IndexOutOfBoundsException();
             }
 
-            this.startID = startID;
-            this.endID = endID;
-            this.prefix = land.getLandDef().getLandPrefix();
+            this.startId = startId;
+            this.endId = endId;
 
-            for(Village village : land.getVillageList()){
-                int id = village.getVillageIDNum();
-                if(startID <= id && id <= endID){
-                    this.villageList.add(village);
-                }
-            }
-
-            return;
-        }
-
-
-        /**
-         * 与えられた国の全ての村を、指定されたinterval間隔でセクション化する。
-         * @param land 国
-         * @param interval セクションの間隔
-         * @return セクションのリスト
-         * @throws java.lang.IllegalArgumentException intervalが正でない
-         */
-        private static List<VillageSection> getSectionList(Land land,
-                                                             int interval )
-                throws IllegalArgumentException{
-            if(interval <= 0){
-                throw new IllegalArgumentException();
-            }
-
-            List<Village> villageList = land.getVillageList();
-            Village village1st = villageList.get(0);
-            Village villageLast = villageList.get(villageList.size() - 1);
+            String format;
+            if("G".equals(prefix)) format = FORM_NODE_G;
+            else                   format = FORM_NODE;
+            this.text = MessageFormat.format(
+                    format, prefix, this.startId, this.endId);
 
-            int startID = village1st.getVillageIDNum();
-            int endID = villageLast.getVillageIDNum();
+            List<Village> newList = new ArrayList<>(spanList);
+            this.villageList = Collections.unmodifiableList(newList);
 
-            List<VillageSection> result = new LinkedList<>();
-
-            int fixedStart = startID / interval * interval;
-            for(int ct = fixedStart; ct <= endID; ct += interval){
-                VillageSection section =
-                        new VillageSection(land, ct, ct + interval - 1);
-                result.add(section);
-            }
+            assert this.endId - this.startId + 1 >= this.villageList.size();
 
-            return Collections.unmodifiableList(result);
+            return;
         }
 
+
         /**
-         * セクションに含まれる村の総数を返す。
+         * セクション内に含まれる村の総数を返す。
+         *
+         * <p>ほとんどの場合はintervalと同じ数。
+         *
          * @return 村の総数
          */
-        private int getVillageCount(){
+        int getVillageCount(){
             return this.villageList.size();
         }
 
         /**
-         * セクションに含まれるindex番目の村を返す。
+         * セクション内に含まれるindex番目の村を返す。
+         *
          * @param index インデックス
          * @return index番目の村
          */
-        private Village getVillage(int index){
+        Village getVillage(int index){
             return this.villageList.get(index);
         }
 
         /**
-         * セクションにおける、指定された子(村)のインデックス位置を返す。
+         * セクション内における、指定された子(村)のインデックス位置を返す。
+         *
          * @param child 子
          * @return インデックス位置
          */
-        private int getIndexOfVillage(Object child){
+        int getIndexOfVillage(Object child){
             return this.villageList.indexOf(child);
         }
 
         /**
          * セクションの文字列表記。
-         * JTree描画に反映される。
+         *
+         * <p>JTree描画に反映される。
+         *
+         * <p>例:「G800 ~ G899」
+         *
          * @return 文字列表記
          */
         @Override
         public String toString(){
-            StringBuilder result = new StringBuilder();
-            result.append(this.prefix).append(this.startID);
-            result.append(" ~ ");
-            result.append(this.prefix).append(this.endID);
-            return result.toString();
+            return this.text;
         }
+
     }
 
 }
index f755ed9..666a550 100644 (file)
 
 package jp.sfjp.jindolf.log;
 
+import io.github.olyutorskii.quetexj.HeightKeeper;
+import io.github.olyutorskii.quetexj.MaxTracker;
+import io.github.olyutorskii.quetexj.MvcFacade;
+import io.github.olyutorskii.quetexj.SwingLogHandler;
 import java.awt.Container;
-import java.awt.Frame;
-import java.awt.GridBagConstraints;
-import java.awt.GridBagLayout;
-import java.awt.Insets;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
+import java.awt.Dialog;
 import java.util.logging.Handler;
+import javax.swing.Action;
+import javax.swing.BorderFactory;
+import javax.swing.BoundedRangeModel;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
 import javax.swing.JButton;
+import javax.swing.JCheckBox;
 import javax.swing.JDialog;
-import javax.swing.JSeparator;
+import javax.swing.JPopupMenu;
+import javax.swing.JScrollBar;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.JToggleButton;
+import javax.swing.border.Border;
+import javax.swing.text.Document;
+import jp.sfjp.jindolf.dxchg.TextPopup;
+import jp.sfjp.jindolf.util.Monodizer;
 
 /**
  * ログ表示ウィンドウ。
  */
 @SuppressWarnings("serial")
-public class LogFrame extends JDialog {
+public final class LogFrame extends JDialog {
 
-    private static final String CMD_CLOSELOG = "CMD_CLOSE_LOG";
-    private static final String CMD_CLEARLOG = "CMD_CLEAR_LOG";
+    private static final int HEIGHT_LIMIT = 5000;
+    private static final int HEIGHT_NEW   = 4000;
 
+    private static final int AROUND_TEXT   = 3;
+    private static final int AROUND_BUTTON = 5;
 
-    private final LogPanel logPanel = new LogPanel();
-    private final JButton clearButton = new JButton("クリア");
-    private final JButton closeButton = new JButton("閉じる");
+
+    private final MvcFacade facade;
+
+    private final JScrollPane scrollPane;
+    private final JButton clearButton;
+    private final JButton closeButton;
+    private final JCheckBox trackButton;
+
+    private final Handler handler;
 
 
     /**
      * コンストラクタ。
-     * @param owner フレームオーナー
      */
-    public LogFrame(Frame owner){
-        super(owner);
+    public LogFrame(){
+        super((Dialog)null);
+        // We need unowned dialog
 
-        design();
+        this.facade = new MvcFacade();
+
+        this.scrollPane = buildScrollPane(this.facade);
 
-        this.clearButton.setActionCommand(CMD_CLEARLOG);
-        this.closeButton.setActionCommand(CMD_CLOSELOG);
+        this.clearButton = new JButton();
+        this.closeButton = new JButton();
+        this.trackButton = new JCheckBox();
 
-        ActionListener actionListener = new ActionWatcher();
-        this.clearButton.addActionListener(actionListener);
-        this.closeButton.addActionListener(actionListener);
+        setupButtons();
+
+        MaxTracker tracker = this.facade.getMaxTracker();
+        HeightKeeper keeper = this.facade.getHeightKeeper();
+
+        tracker.setTrackingMode(true);
+        keeper.setConditions(HEIGHT_LIMIT, HEIGHT_NEW);
+
+        Handler logHandler = null;
+        if(LogUtils.hasLoggingPermission()){
+            Document document = this.facade.getDocument();
+            logHandler = new SwingLogHandler(document);
+        }
+        this.handler = logHandler;
 
         setResizable(true);
         setLocationByPlatform(true);
         setModal(false);
 
+        design();
+
         return;
     }
 
+
     /**
-     * デザインを行う。
+     * ログ用スクロール領域を生成する。
+     *
+     * @param facadeArg ファサード
+     * @return スクロール領域
      */
-    private void design(){
-        Container content = getContentPane();
+    private static JScrollPane buildScrollPane(MvcFacade facadeArg){
+        JScrollPane scrollPane = new JScrollPane();
 
-        GridBagLayout layout = new GridBagLayout();
-        GridBagConstraints constraints = new GridBagConstraints();
-        content.setLayout(layout);
+        JScrollBar vbar = scrollPane.getVerticalScrollBar();
+        BoundedRangeModel rangeModel =
+                facadeArg.getVerticalBoundedRangeModel();
+        vbar.setModel(rangeModel);
 
-        constraints.weightx = 1.0;
-        constraints.weighty = 1.0;
-        constraints.fill = GridBagConstraints.BOTH;
-        constraints.gridwidth = GridBagConstraints.REMAINDER;
+        JTextArea textArea = buildTextArea(facadeArg);
+        scrollPane.setViewportView(textArea);
 
-        content.add(this.logPanel, constraints);
+        return scrollPane;
+    }
 
-        constraints.weighty = 0.0;
-        constraints.fill = GridBagConstraints.HORIZONTAL;
-        constraints.insets = new Insets(5, 5, 5, 5);
+    /**
+     * ログ用テキストエリアを生成する。
+     *
+     * @param facadeArg ファサード
+     * @return テキストエリア
+     */
+    private static JTextArea buildTextArea(MvcFacade facadeArg){
+        JTextArea textArea = facadeArg.getTextArea();
 
-        content.add(new JSeparator(), constraints);
+        textArea.setEditable(false);
+        textArea.setLineWrap(true);
+        Monodizer.monodize(textArea);
 
-        constraints.anchor = GridBagConstraints.WEST;
-        constraints.fill = GridBagConstraints.NONE;
-        constraints.gridwidth = 1;
-        content.add(this.clearButton, constraints);
+        Border border = BorderFactory.createEmptyBorder(
+                AROUND_TEXT,
+                AROUND_TEXT,
+                AROUND_TEXT,
+                AROUND_TEXT
+        );
+        textArea.setBorder(border);
 
-        constraints.weightx = 0.0;
-        constraints.anchor = GridBagConstraints.EAST;
-        content.add(this.closeButton, constraints);
+        JPopupMenu popup = new TextPopup();
+        textArea.setComponentPopupMenu(popup);
 
-        return;
+        return textArea;
     }
 
-    /**
-     * ロギングハンドラを返す。
-     * @return ロギングハンドラ
-     */
-    public Handler getHandler(){
-        return this.logPanel.getHandler();
-    }
 
     /**
-     * ã\83­ã\82°å\86\85容ã\82\92ã\82¯ã\83ªã\82¢ã\81\99ã\82\8b
+     * ã\83\9cã\82¿ã\83³ã\81®å\90\84種設å®\9a
      */
-    public void clearLog(){
-        this.logPanel.clearLog();
+    private void setupButtons(){
+        Action clearAction = this.facade.getClearAction();
+        this.clearButton.setAction(clearAction);
+        this.clearButton.setText("クリア");
+
+        this.closeButton.addActionListener(event -> {
+            if(event.getSource() == this.closeButton){
+                setVisible(false);
+            }
+        });
+        this.closeButton.setText("閉じる");
+
+        JToggleButton.ToggleButtonModel toggleModel;
+        toggleModel = this.facade.getTrackSwitchButtonModel();
+        this.trackButton.setModel(toggleModel);
+        this.trackButton.setText("末尾に追従");
+
         return;
     }
 
-
     /**
-     * ã\83\9cã\82¿ã\83³æ\93\8dä½\9cã\82\92ç\9b£è¦\96ã\81\99ã\82\8b
+     * ã\83¬ã\82¤ã\82¢ã\82¦ã\83\88ã\83\87ã\82¶ã\82¤ã\83³ã\82\92è¡\8cã\81\86
      */
-    private final class ActionWatcher implements ActionListener{
-
-        /**
-         * コンストラクタ。
-         */
-        ActionWatcher(){
-            super();
-            return;
-        }
+    private void design(){
+        Box buttonPanel = Box.createHorizontalBox();
+
+        buttonPanel.add(this.clearButton);
+        buttonPanel.add(Box.createHorizontalStrut(AROUND_BUTTON));
+        buttonPanel.add(this.trackButton);
+        buttonPanel.add(Box.createHorizontalGlue());
+        buttonPanel.add(this.closeButton);
+
+        Border border = BorderFactory.createEmptyBorder(
+                AROUND_BUTTON,
+                AROUND_BUTTON,
+                AROUND_BUTTON,
+                AROUND_BUTTON
+        );
+        buttonPanel.setBorder(border);
 
-        /**
-         * {@inheritDoc}
-         * ボタン押下イベント処理。
-         * @param event {@inheritDoc}
-         */
-        @Override
-        public void actionPerformed(ActionEvent event){
-            String cmd = event.getActionCommand();
+        Container content = getContentPane();
+        BoxLayout layout = new BoxLayout(content, BoxLayout.Y_AXIS);
+        content.setLayout(layout);
 
-            if     (CMD_CLEARLOG.equals(cmd)) clearLog();
-            else if(CMD_CLOSELOG.equals(cmd)) setVisible(false);
+        content.add(this.scrollPane);
+        content.add(buttonPanel);
 
-            return;
-        }
+        return;
+    }
 
+    /**
+     * ロギングハンドラを返す。
+     *
+     * @return ロギングハンドラ
+     */
+    public Handler getHandler(){
+        return this.handler;
     }
 
 }
diff --git a/src/main/java/jp/sfjp/jindolf/log/LogPanel.java b/src/main/java/jp/sfjp/jindolf/log/LogPanel.java
deleted file mode 100644 (file)
index 1a3c415..0000000
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Log panel
- *
- * License : The MIT License
- * Copyright(c) 2012 olyutorskii
- */
-
-package jp.sfjp.jindolf.log;
-
-import java.awt.Adjustable;
-import java.awt.EventQueue;
-import java.util.logging.Handler;
-import javax.swing.BorderFactory;
-import javax.swing.JPopupMenu;
-import javax.swing.JScrollPane;
-import javax.swing.JTextArea;
-import javax.swing.border.Border;
-import javax.swing.event.AncestorEvent;
-import javax.swing.event.AncestorListener;
-import javax.swing.event.DocumentEvent;
-import javax.swing.event.DocumentListener;
-import javax.swing.text.BadLocationException;
-import javax.swing.text.Document;
-import javax.swing.text.PlainDocument;
-import jp.sfjp.jindolf.dxchg.TextPopup;
-import jp.sfjp.jindolf.util.Monodizer;
-
-/**
- * スクロールバー付きログ表示パネル。
- * 垂直スクロールバーは自動的に最下部へトラックする。
- */
-@SuppressWarnings("serial")
-public class LogPanel extends JScrollPane {
-
-    private static final Document DOC_EMPTY = new PlainDocument();
-
-
-    private final Document document = new PlainDocument();
-    private final Handler handler;
-
-    private final JTextArea textarea = new JTextArea();
-
-
-    /**
-     * コンストラクタ。
-     */
-    public LogPanel(){
-        super();
-
-        if(LogUtils.hasLoggingPermission()){
-            this.handler = new SwingDocHandler(this.document);
-        }else{
-            this.handler = null;
-        }
-
-        this.textarea.setDocument(DOC_EMPTY);
-        this.textarea.setEditable(false);
-        this.textarea.setLineWrap(true);
-        Monodizer.monodize(this.textarea);
-
-        Border border = BorderFactory.createEmptyBorder(3, 3, 3, 3);
-        this.textarea.setBorder(border);
-
-        JPopupMenu popup = new TextPopup();
-        this.textarea.setComponentPopupMenu(popup);
-
-        setViewportView(this.textarea);
-
-        DocumentListener docListener = new DocWatcher();
-        this.document.addDocumentListener(docListener);
-
-        AncestorListener ancestorListener = new AncestorWatcher();
-        addAncestorListener(ancestorListener);
-
-        return;
-    }
-
-    /**
-     * ロギングハンドラを返す。
-     * @return ロギングハンドラ
-     */
-    public Handler getHandler(){
-        return this.handler;
-    }
-
-    /**
-     * 垂直スクロールバーをドキュメント下端に設定し、
-     * ログの最新部を表示する。
-     * 不可視状態なら何もしない。
-     */
-    private void showLastPos(){
-        if(this.textarea.getDocument() != this.document) return;
-
-        final Adjustable yPos = getVerticalScrollBar();
-        EventQueue.invokeLater(new Runnable(){
-            @Override
-            public void run(){
-                yPos.setValue(Integer.MAX_VALUE);
-                return;
-            }
-        });
-
-        return;
-    }
-
-    /**
-     * モデルとビューを連携させる。
-     * スクロール位置は末端に。
-     */
-    private void attachModel(){
-        if(this.textarea.getDocument() != this.document){
-            this.textarea.setDocument(this.document);
-        }
-        showLastPos();
-        return;
-    }
-
-    /**
-     * モデルとビューを切り離す。
-     */
-    private void detachModel(){
-        if(this.textarea.getDocument() == DOC_EMPTY) return;
-        this.textarea.setDocument(DOC_EMPTY);
-        return;
-    }
-
-    /**
-     * ログ内容をクリアする。
-     */
-    public void clearLog(){
-        try{
-            int docLength = this.document.getLength();
-            this.document.remove(0, docLength);
-        }catch(BadLocationException e){
-            assert false;
-        }
-        return;
-    }
-
-
-    /**
-     * 画面更新が必要な状態か監視し、必要に応じてモデルとビューを切り離す。
-     */
-    private final class AncestorWatcher implements AncestorListener{
-
-        /**
-         * コンストラクタ。
-         */
-        AncestorWatcher(){
-            super();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param event {@inheritDoc}
-         */
-        @Override
-        public void ancestorAdded(AncestorEvent event){
-            attachModel();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param event {@inheritDoc}
-         */
-        @Override
-        public void ancestorRemoved(AncestorEvent event){
-            detachModel();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param event {@inheritDoc}
-         */
-        @Override
-        public void ancestorMoved(AncestorEvent event){
-            return;
-        }
-
-    }
-
-
-    /**
-     * ドキュメント操作を監視し、スクロールバーを更新する。
-     */
-    private final class DocWatcher implements DocumentListener{
-
-        /**
-         * コンストラクタ。
-         */
-        DocWatcher(){
-            super();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param event {@inheritDoc}
-         */
-        @Override
-        public void changedUpdate(DocumentEvent event){
-            showLastPos();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param event {@inheritDoc}
-         */
-        @Override
-        public void insertUpdate(DocumentEvent event){
-            showLastPos();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param event {@inheritDoc}
-         */
-        @Override
-        public void removeUpdate(DocumentEvent event){
-            showLastPos();
-            return;
-        }
-
-    }
-
-}
index 9be7f69..7bf62a7 100644 (file)
@@ -24,7 +24,7 @@ public final class LogUtils {
             new LoggingPermission("control", null);
 
     private static final PrintStream STDERR = System.err;
-    private static final String ERRMSG_LOGSECURITY =
+    private static final String ERRMSG_LOGPERM =
             "セキュリティ設定により、ログ設定を変更できませんでした";
 
 
@@ -38,6 +38,7 @@ public final class LogUtils {
 
     /**
      * ログ操作のアクセス権があるか否か判定する。
+     *
      * @return アクセス権があればtrue
      */
     public static boolean hasLoggingPermission(){
@@ -48,6 +49,7 @@ public final class LogUtils {
 
     /**
      * ログ操作のアクセス権があるか否か判定する。
+     *
      * @param manager セキュリティマネージャ
      * @return アクセス権があればtrue
      */
@@ -65,6 +67,7 @@ public final class LogUtils {
 
     /**
      * ルートロガーを返す。
+     *
      * @return ルートロガー
      */
     public static Logger getRootLogger(){
@@ -74,14 +77,16 @@ public final class LogUtils {
 
     /**
      * ルートロガーの初期化を行う。
-     * ルートロガーの既存ハンドラを全解除し、
+     *
+     * <p>ルートロガーの既存ハンドラを全解除し、
      * {@link MomentaryHandler}ハンドラを登録する。
+     *
      * @param useConsoleLog trueなら
      * {@link java.util.logging.ConsoleHandler}も追加する。
      */
     public static void initRootLogger(boolean useConsoleLog){
         if( ! hasLoggingPermission() ){
-            STDERR.println(ERRMSG_LOGSECURITY);
+            STDERR.println(ERRMSG_LOGPERM);
             return;
         }
 
@@ -105,10 +110,14 @@ public final class LogUtils {
 
     /**
      * ルートロガーに新ハンドラを追加する。
-     * ルートロガー中の全{@link MomentaryHandler}型ハンドラに
+     *
+     * <p>ルートロガー中の全{@link MomentaryHandler}型ハンドラに
      * 蓄積されていたログは、新ハンドラに一気に転送される。
-     * {@link MomentaryHandler}型ハンドラはルートロガーから削除される。
-     * ログ操作のパーミッションがない場合、何もしない。
+     *
+     * <p>{@link MomentaryHandler}型ハンドラはルートロガーから削除される。
+     *
+     * <p>ログ操作のパーミッションがない場合、何もしない。
+     *
      * @param newHandler 新ハンドラ
      */
     public static void switchHandler(Handler newHandler){
@@ -122,10 +131,10 @@ public final class LogUtils {
 
         logger.addHandler(newHandler);
 
-        for(MomentaryHandler momentaryHandler : momentaryHandlers){
+        momentaryHandlers.forEach(momentaryHandler -> {
             momentaryHandler.transfer(newHandler);
             momentaryHandler.close();
-        }
+        });
 
         return;
     }
index 6c73628..a7d2a8a 100644 (file)
@@ -18,11 +18,11 @@ import java.util.logging.Logger;
  */
 public class LoggingDispatcher extends EventQueue{
 
+    private static final Logger LOGGER = Logger.getAnonymousLogger();
+
     private static final String FATALMSG =
             "イベントディスパッチ中に異常が起きました。";
 
-    private static final Logger LOGGER = Logger.getAnonymousLogger();
-
 
     /**
      * コンストラクタ。
@@ -47,17 +47,20 @@ public class LoggingDispatcher extends EventQueue{
 
     /**
      * 異常系を匿名ロガーに出力する。
-     * @param e 例外
+     *
+     * @param throwable 例外
      */
-    private static void logThrowable(Throwable e){
-        LOGGER.log(Level.SEVERE, FATALMSG, e);
+    private static void logThrowable(Throwable throwable){
+        LOGGER.log(Level.SEVERE, FATALMSG, throwable);
         return;
     }
 
 
     /**
      * {@inheritDoc}
-     * イベントディスパッチにより発生した例外を匿名ログ出力する。
+     *
+     * <p>イベントディスパッチにより発生した例外を匿名ログ出力する。
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -70,11 +73,11 @@ public class LoggingDispatcher extends EventQueue{
         }catch(Exception e){
             logThrowable(e);
         }
-        // TODO Toolkit#beep()もするべきか
-        // TODO モーダルダイアログを出すべきか
-        // TODO 標準エラー出力抑止オプションを用意すべきか
-        // TODO セキュリティバイパス
         return;
     }
 
+    // TODO モーダルダイアログを出すべきか
+    // TODO 標準エラー出力抑止オプションを用意すべきか
+    // TODO セキュリティバイパス
+
 }
index ff232a1..437a7bb 100644 (file)
@@ -7,24 +7,28 @@
 
 package jp.sfjp.jindolf.log;
 
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Objects;
 import java.util.logging.Handler;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.util.logging.Logger;
+import java.util.stream.Collectors;
 
 /**
  * なにもしない一時的なロギングハンドラ。
- * なにがロギングされたのかあとから一括して取得することができる。
+ *
+ * <p>なにがロギングされたのかあとから一括して取得することができる。
  *
  * <p>知らないうちにメモリを圧迫しないよう注意。
  */
 public class MomentaryHandler extends Handler{
 
     private final List<LogRecord> logList =
-            Collections.synchronizedList(new LinkedList<LogRecord>());
+            Collections.synchronizedList(new LinkedList<>());
     private final List<LogRecord> unmodList =
             Collections.unmodifiableList(this.logList);
 
@@ -40,36 +44,39 @@ public class MomentaryHandler extends Handler{
 
     /**
      * ロガーに含まれる{@link MomentaryHandler}型ハンドラのリストを返す。
+     *
      * @param logger ロガー
      * @return {@link MomentaryHandler}型ハンドラのリスト
      */
     public static List<MomentaryHandler>
             getMomentaryHandlers(Logger logger){
-        List<MomentaryHandler> result = new LinkedList<>();
+        List<MomentaryHandler> result;
 
-        for(Handler handler : logger.getHandlers()){
-            if( ! (handler instanceof MomentaryHandler) ) continue;
-            MomentaryHandler momentaryHandler = (MomentaryHandler) handler;
-            result.add(momentaryHandler);
-        }
+        result = Arrays.stream(logger.getHandlers())
+                .filter(handler -> handler instanceof MomentaryHandler)
+                .map(handler -> (MomentaryHandler) handler)
+                .collect(Collectors.toList());
 
         return result;
     }
 
     /**
      * ロガーに含まれる{@link MomentaryHandler}型ハンドラを全て削除する。
+     *
      * @param logger ロガー
      */
     public static void removeMomentaryHandlers(Logger logger){
-        for(MomentaryHandler handler : getMomentaryHandlers(logger)){
+        getMomentaryHandlers(logger).forEach(handler -> {
             logger.removeHandler(handler);
-        }
+        });
         return;
     }
 
     /**
      * 蓄積されたログレコードのリストを返す。
-     * 古いログが先頭に来る。
+     *
+     * <p>古いログが先頭に来る。
+     *
      * @return 刻一刻と成長するログレコードのリスト。変更不可。
      */
     public List<LogRecord> getRecordList(){
@@ -78,7 +85,9 @@ public class MomentaryHandler extends Handler{
 
     /**
      * {@inheritDoc}
-     * ログを内部に溜め込む。
+     *
+     * <p>ログを内部に溜め込む。
+     *
      * @param record {@inheritDoc}
      */
     @Override
@@ -89,6 +98,7 @@ public class MomentaryHandler extends Handler{
             return;
         }
 
+        // recording caller method
         record.getSourceMethodName();
 
         this.logList.add(record);
@@ -98,7 +108,8 @@ public class MomentaryHandler extends Handler{
 
     /**
      * {@inheritDoc}
-     * (何もしない)。
+     *
+     * <p>(何もしない)。
      */
     @Override
     public void flush(){
@@ -107,7 +118,8 @@ public class MomentaryHandler extends Handler{
 
     /**
      * {@inheritDoc}
-     * 以降のログ出力を無視する。
+     *
+     * <p>以降のログ出力を無視する。
      */
     @Override
     public void close(){
@@ -119,19 +131,21 @@ public class MomentaryHandler extends Handler{
     /**
      * 自分自身をクローズし、
      * 蓄積したログを他のハンドラへまとめて出力する。
-     * 最後に蓄積されたログを解放する。
+     *
+     * <p>最後に蓄積されたログを解放する。
+     *
      * @param handler 他のハンドラ
      * @throws NullPointerException 引数がnull
      */
     public void transfer(Handler handler) throws NullPointerException {
-        if(handler == null) throw new NullPointerException();
+        Objects.nonNull(handler);
         if(handler == this) return;
 
         close();
 
-        for(LogRecord record : this.logList){
+        this.logList.forEach(record -> {
             handler.publish(record);
-        }
+        });
 
         handler.flush();
         this.logList.clear();
diff --git a/src/main/java/jp/sfjp/jindolf/log/SwingDocHandler.java b/src/main/java/jp/sfjp/jindolf/log/SwingDocHandler.java
deleted file mode 100644 (file)
index 8be09fd..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Logging handler for Swing text component
- *
- * License : The MIT License
- * Copyright(c) 2011 olyutorskii
- */
-
-package jp.sfjp.jindolf.log;
-
-import java.awt.EventQueue;
-import java.util.logging.Formatter;
-import java.util.logging.Handler;
-import java.util.logging.Level;
-import java.util.logging.LogRecord;
-import java.util.logging.SimpleFormatter;
-import javax.swing.text.AttributeSet;
-import javax.swing.text.BadLocationException;
-import javax.swing.text.Document;
-
-/**
- * Swingテキストコンポーネント用データモデル
- * {@link javax.swing.text.Document}
- * に出力する{@link java.util.logging.Handler}。
- *
- * <p>スレッド間競合はEDTで解決される。
- *
- * <p>一定の文字数を超えないよう、古い記録は消去される。
- */
-public class SwingDocHandler extends Handler{
-
-    private static final int DOCLIMIT = 100 * 1000; // 単位は文字
-    private static final float CHOPRATIO = 0.9f;
-    private static final int CHOPPEDLEN = (int) (DOCLIMIT * CHOPRATIO);
-
-    static{
-        assert DOCLIMIT > CHOPPEDLEN;
-    }
-
-
-    private final Document document;
-
-
-    /**
-     * ログハンドラの生成。
-     * @param document ドキュメントモデル
-     */
-    public SwingDocHandler(Document document){
-        super();
-
-        this.document = document;
-
-        Formatter formatter = new SimpleFormatter();
-        setFormatter(formatter);
-
-        return;
-    }
-
-    /**
-     * ドキュメント末尾に文字列を追加する。
-     *
-     * <p>EDTから呼ばなければならない。
-     *
-     * @param logMessage 文字列
-     */
-    private void appendLog(String logMessage){
-        try{
-            this.document.insertString(this.document.getLength(),
-                                       logMessage,
-                                       (AttributeSet) null );
-        }catch(BadLocationException e){
-            assert false;
-        }
-        return;
-    }
-
-    /**
-     * ドキュメント先頭部をチョップして最大長に納める。
-     *
-     * <p>EDTから呼ばなければならない。
-     */
-    private void chopHead(){
-        int docLength = this.document.getLength();
-        if(docLength <= DOCLIMIT) return;
-
-        int offset = docLength - CHOPPEDLEN;
-        try{
-            this.document.remove(0, offset);
-        }catch(BadLocationException e){
-            assert false;
-        }
-
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @param record {@inheritDoc}
-     */
-    @Override
-    public void publish(LogRecord record){
-        if( ! isLoggable(record) ){
-            return;
-        }
-
-        Formatter formatter = getFormatter();
-        final String message = formatter.format(record);
-
-        EventQueue.invokeLater(new Runnable(){
-            @Override
-            public void run(){
-                appendLog(message);
-                chopHead();
-                return;
-            }
-        });
-
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * (何もしない)。
-     */
-    @Override
-    public void flush(){
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * ログ受け入れを締め切る。
-     */
-    @Override
-    public void close(){
-        setLevel(Level.OFF);
-        flush();
-        return;
-    }
-
-}
index 35155b4..286249c 100644 (file)
@@ -10,8 +10,8 @@ package jp.sfjp.jindolf.summary;
 import java.awt.Color;
 import java.awt.Component;
 import java.awt.Container;
+import java.awt.Dialog;
 import java.awt.Dimension;
-import java.awt.Frame;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.Image;
@@ -57,7 +57,7 @@ import jp.sourceforge.jindolf.corelib.TalkType;
  * その日ごとの集計。
  */
 @SuppressWarnings("serial")
-public class DaySummary extends JDialog
+public final class DaySummary extends JDialog
         implements WindowListener, ActionListener, ItemListener{
 
     private static final NumberFormat AVERAGE_FORM;
@@ -92,11 +92,13 @@ public class DaySummary extends JDialog
 
     /**
      * コンストラクタ。
-     * 集計結果を表示するモーダルダイアログを生成する。
-     * @param owner オーナー
+     *
+     * <p>集計結果を表示するモーダルダイアログを生成する。
      */
-    public DaySummary(Frame owner){
-        super(owner);
+    public DaySummary(){
+        super((Dialog)null);
+        // We need unowned dialog
+
         setModal(true);
 
         GUIUtils.modifyWindowAttributes(this, true, false, true);
@@ -154,6 +156,7 @@ public class DaySummary extends JDialog
 
     /**
      * 初期のデータモデルを生成する。
+     *
      * @return データモデル
      */
     private static DefaultTableModel createInitModel(){
@@ -180,6 +183,7 @@ public class DaySummary extends JDialog
 
     /**
      * 行を追加する。
+     *
      * @param avatar アバター
      * @param talkCount 発言回数
      * @param totalChars 発言文字総数
@@ -256,6 +260,7 @@ public class DaySummary extends JDialog
 
     /**
      * 与えられたPeriodで集計を更新する。
+     *
      * @param newPeriod 日
      */
     public void summaryPeriod(Period newPeriod){
@@ -322,6 +327,7 @@ public class DaySummary extends JDialog
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -331,6 +337,7 @@ public class DaySummary extends JDialog
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -340,6 +347,7 @@ public class DaySummary extends JDialog
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -349,6 +357,7 @@ public class DaySummary extends JDialog
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -358,6 +367,7 @@ public class DaySummary extends JDialog
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -367,7 +377,9 @@ public class DaySummary extends JDialog
 
     /**
      * {@inheritDoc}
-     * ダイアログのクローズボタン押下処理を行う。
+     *
+     * <p>ダイアログのクローズボタン押下処理を行う。
+     *
      * @param event ウィンドウ変化イベント {@inheritDoc}
      */
     @Override
@@ -378,6 +390,7 @@ public class DaySummary extends JDialog
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -387,7 +400,9 @@ public class DaySummary extends JDialog
 
     /**
      * {@inheritDoc}
-     * クローズボタン押下処理。
+     *
+     * <p>クローズボタン押下処理。
+     *
      * @param event イベント {@inheritDoc}
      */
     @Override
@@ -399,7 +414,9 @@ public class DaySummary extends JDialog
 
     /**
      * {@inheritDoc}
-     * コンボボックス操作処理。
+     *
+     * <p>コンボボックス操作処理。
+     *
      * @param event イベント {@inheritDoc}
      */
     @Override
@@ -443,7 +460,9 @@ public class DaySummary extends JDialog
 
         /**
          * {@inheritDoc}
-         * セルに{@link Avatar}がきたら顔アイコンと名前を表示する。
+         *
+         * <p>セルに{@link Avatar}がきたら顔アイコンと名前を表示する。
+         *
          * @param value {@inheritDoc}
          */
         @Override
@@ -484,8 +503,10 @@ public class DaySummary extends JDialog
         }
 
         /**
-         *  {@inheritDoc}
-         * 統計種別によってセル色を変える。
+         * {@inheritDoc}
+         *
+         * <p>統計種別によってセル色を変える。
+         *
          * @param table {@inheritDoc}
          * @param value {@inheritDoc}
          * @param isSelected {@inheritDoc}
@@ -530,6 +551,7 @@ public class DaySummary extends JDialog
 
             return result;
         }
+
     }
 
 }
index 3c58acc..bc8f05e 100644 (file)
@@ -8,9 +8,9 @@
 package jp.sfjp.jindolf.summary;
 
 import java.awt.Container;
+import java.awt.Dialog;
 import java.awt.Dimension;
 import java.awt.EventQueue;
-import java.awt.Frame;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.Image;
@@ -60,7 +60,7 @@ import jp.sourceforge.jindolf.corelib.Team;
  * 決着のついた村のダイジェストを表示する。
  */
 @SuppressWarnings("serial")
-public class VillageDigest
+public final class VillageDigest
         extends JDialog
         implements ActionListener,
                    ItemListener {
@@ -109,10 +109,12 @@ public class VillageDigest
 
     /**
      * コンストラクタ。
-     * @param owner 親フレーム
      */
-    public VillageDigest(Frame owner){
-        super(owner);
+    @SuppressWarnings("LeakingThisInConstructor")
+    public VillageDigest(){
+        super((Dialog)null);
+        // We need unowned dialog
+
         setModal(true);
 
         GUIUtils.modifyWindowAttributes(this, true, false, true);
@@ -177,6 +179,7 @@ public class VillageDigest
 
     /**
      * キャプション付き項目をコンテナに追加。
+     *
      * @param container コンテナ
      * @param caption 項目キャプション名
      * @param delimiter デリミタ文字
@@ -222,6 +225,7 @@ public class VillageDigest
 
     /**
      * キャプション付き項目をコンテナに追加。
+     *
      * @param container コンテナ
      * @param caption 項目キャプション名
      * @param item 項目アイテム
@@ -235,6 +239,7 @@ public class VillageDigest
 
     /**
      * レイアウトの最後に詰め物をする。
+     *
      * @param container コンテナ
      */
     private static void addFatPad(Container container){
@@ -257,6 +262,7 @@ public class VillageDigest
 
     /**
      * GridBagLayoutでレイアウトする空コンポーネントを生成する。
+     *
      * @return 空コンポーネント
      */
     private static JComponent createGridBagComponent(){
@@ -268,6 +274,7 @@ public class VillageDigest
 
     /**
      * 村サマリ画面の生成。
+     *
      * @return 村サマリ画面
      */
     private JComponent buildSummaryPanel(){
@@ -281,6 +288,7 @@ public class VillageDigest
 
     /**
      * プレイヤーサマリ画面の生成。
+     *
      * @return プレイヤーサマリ画面
      */
     private JComponent buildPlayerPanel(){
@@ -323,6 +331,7 @@ public class VillageDigest
 
     /**
      * キャスト表生成画面を生成する。
+     *
      * @return キャスト表生成画面
      */
     private JComponent buildCastPanel(){
@@ -354,6 +363,7 @@ public class VillageDigest
 
     /**
      * 投票Box生成画面を生成する。
+     *
      * @return 投票Box生成画面
      */
     private JComponent buildVotePanel(){
@@ -376,6 +386,7 @@ public class VillageDigest
 
     /**
      * 村詳細Wiki生成画面を生成する。
+     *
      * @return 村詳細Wiki生成画面
      */
     private JComponent buildVillageWikiPanel(){
@@ -398,6 +409,7 @@ public class VillageDigest
 
     /**
      * Wikiテキスト領域GUIの生成。
+     *
      * @return Wikiテキスト領域GUI
      */
     private JComponent buildClipText(){
@@ -439,6 +451,7 @@ public class VillageDigest
 
     /**
      * テンプレート生成画面を生成する。
+     *
      * @return テンプレート生成画面
      */
     private JComponent buildClipboardPanel(){
@@ -480,6 +493,7 @@ public class VillageDigest
 
     /**
      * 画面レイアウトを行う。
+     *
      * @param container コンテナ
      */
     private void design(Container container){
@@ -533,6 +547,7 @@ public class VillageDigest
 
     /**
      * 村を設定する。
+     *
      * @param village 村
      */
     public void setVillage(Village village){
@@ -646,6 +661,7 @@ public class VillageDigest
 
     /**
      * アクションイベントの振り分け。
+     *
      * @param event アクションイベント
      */
     @Override
@@ -713,7 +729,9 @@ public class VillageDigest
 
     /**
      * Wikiテキストをテキストボックスに出力する。
-     * スクロール位置は一番上に。
+     *
+     * <p>スクロール位置は一番上に。
+     *
      * @param wikiText Wikiテキスト
      */
     private void putWikiText(CharSequence wikiText){
@@ -741,6 +759,7 @@ public class VillageDigest
 
     /**
      * プレイヤーの選択操作。
+     *
      * @param avatar 選択されたAvatar
      */
     private void selectPlayer(Avatar avatar){
@@ -805,6 +824,7 @@ public class VillageDigest
 
     /**
      * 顔アイコンセットの選択操作。
+     *
      * @param iconSet 顔アイコンセット
      */
     private void selectIconSet(FaceIconSet iconSet){
@@ -817,6 +837,7 @@ public class VillageDigest
 
     /**
      * コンボボックス操作の受信。
+     *
      * @param event コンボボックス操作イベント
      */
     @Override
index ee09578..a15703a 100644 (file)
@@ -8,7 +8,7 @@
 package jp.sfjp.jindolf.view;
 
 import java.awt.Container;
-import java.awt.Frame;
+import java.awt.Dialog;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.Insets;
@@ -43,7 +43,7 @@ import jp.sourceforge.jindolf.corelib.TalkType;
  * 発言フィルタ GUI。
  */
 @SuppressWarnings("serial")
-public class FilterPanel extends JDialog
+public final class FilterPanel extends JDialog
         implements ActionListener, TopicFilter{
 
     private static final int COLS = 4;
@@ -71,11 +71,12 @@ public class FilterPanel extends JDialog
 
     /**
      * 発言フィルタを生成する。
-     * @param owner フレームオーナー
      */
     @SuppressWarnings("LeakingThisInConstructor")
-    public FilterPanel(Frame owner){
-        super(owner);
+    public FilterPanel(){
+        super((Dialog)null);
+        // We need unowned dialog
+
         setModal(false);
 
         GUIUtils.modifyWindowAttributes(this, true, false, true);
@@ -109,6 +110,7 @@ public class FilterPanel extends JDialog
 
     /**
      * レイアウトデザインを行う。
+     *
      * @param topicPanel システムイベント選択
      * @param avatarPanel キャラ一覧
      * @param buttonPanel ボタン群
@@ -167,6 +169,7 @@ public class FilterPanel extends JDialog
 
     /**
      * システムイベントチェックボックス群パネルを作成。
+     *
      * @return システムイベントチェックボックス群パネル
      */
     private JComponent createTopicPanel(){
@@ -210,6 +213,7 @@ public class FilterPanel extends JDialog
 
     /**
      * キャラ一覧チェックボックス群パネルを作成。
+     *
      * @return キャラ一覧チェックボックス群パネル
      */
     private JComponent createAvatarPanel(){
@@ -248,6 +252,7 @@ public class FilterPanel extends JDialog
 
     /**
      * ボタン群パネルを生成。
+     *
      * @return ボタン群パネル
      */
     private JComponent createButtonPanel(){
@@ -276,6 +281,7 @@ public class FilterPanel extends JDialog
 
     /**
      * 下段パネルを生成する。
+     *
      * @return 下段パネル
      */
     private JComponent createBottomPanel(){
@@ -296,6 +302,7 @@ public class FilterPanel extends JDialog
 
     /**
      * リスナを登録する。
+     *
      * @param listener リスナ
      */
     public void addChangeListener(ChangeListener listener){
@@ -304,6 +311,7 @@ public class FilterPanel extends JDialog
 
     /**
      * リスナを削除する。
+     *
      * @param listener リスナ
      */
     public void removeChangeListener(ChangeListener listener){
@@ -312,6 +320,7 @@ public class FilterPanel extends JDialog
 
     /**
      * 全リスナを取得する。
+     *
      * @return リスナの配列
      */
     public ChangeListener[] getChangeListeners(){
@@ -344,8 +353,10 @@ public class FilterPanel extends JDialog
     }
 
     /**
-     * チェックボックスまたはボタン操作時にリスナとして呼ばれる。
      * {@inheritDoc}
+     *
+     * <p>チェックボックスまたはボタン操作時にリスナとして呼ばれる。
+     *
      * @param event イベント
      */
     @Override
@@ -407,6 +418,7 @@ public class FilterPanel extends JDialog
 
     /**
      * {@inheritDoc}
+     *
      * @param topic {@inheritDoc}
      * @return {@inheritDoc}
      */
@@ -462,6 +474,7 @@ public class FilterPanel extends JDialog
 
     /**
      * {@inheritDoc}
+     *
      * @return {@inheritDoc}
      */
     @Override
@@ -471,6 +484,7 @@ public class FilterPanel extends JDialog
 
     /**
      * {@inheritDoc}
+     *
      * @param context {@inheritDoc}
      * @return {@inheritDoc}
      */
@@ -520,6 +534,7 @@ public class FilterPanel extends JDialog
 
         /**
          * {@inheritDoc}
+         *
          * @return {@inheritDoc}
          */
         @Override
index fbabb65..0d1d8f0 100644 (file)
@@ -10,7 +10,7 @@ package jp.sfjp.jindolf.view;
 import java.awt.BorderLayout;
 import java.awt.Component;
 import java.awt.Container;
-import java.awt.Frame;
+import java.awt.Dialog;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.GridLayout;
@@ -64,7 +64,7 @@ import jp.sourceforge.jovsonz.JsValue;
  * 検索パネルGUI。
  */
 @SuppressWarnings("serial")
-public class FindPanel extends JDialog
+public final class FindPanel extends JDialog
         implements ActionListener,
                    ItemListener,
                    ChangeListener,
@@ -76,6 +76,7 @@ public class FindPanel extends JDialog
     private static final String LABEL_REENTER = "再入力";
     private static final String LABEL_IGNORE = "無視して検索をキャンセル";
 
+
     private final JComboBox<Object> findBox = new JComboBox<>();
     private final JButton searchButton = new JButton("検索");
     private final JButton clearButton = new JButton("クリア");
@@ -99,13 +100,15 @@ public class FindPanel extends JDialog
     private boolean canceled = false;
     private RegexPattern regexPattern = null;
 
+
     /**
      * 検索パネルを生成する。
-     * @param owner 親フレーム。
      */
     @SuppressWarnings("LeakingThisInConstructor")
-    public FindPanel(Frame owner){
-        super(owner);
+    public FindPanel(){
+        super((Dialog)null);
+        // We need unowned dialog
+
         setModal(true);
 
         GUIUtils.modifyWindowAttributes(this, true, false, true);
@@ -145,6 +148,7 @@ public class FindPanel extends JDialog
         return;
     }
 
+
     /**
      * デザイン・レイアウトを行う。
      */
@@ -225,7 +229,9 @@ public class FindPanel extends JDialog
 
     /**
      * {@inheritDoc}
-     * 検索ダイアログを表示・非表示する。
+     *
+     * <p>検索ダイアログを表示・非表示する。
+     *
      * @param show 表示フラグ。真なら表示。{@inheritDoc}
      */
     @Override
@@ -238,6 +244,7 @@ public class FindPanel extends JDialog
 
     /**
      * ダイアログが閉じられた原因を判定する。
+     *
      * @return キャンセルもしくはクローズボタンでダイアログが閉じられたらtrue
      */
     public boolean isCanceled(){
@@ -246,6 +253,7 @@ public class FindPanel extends JDialog
 
     /**
      * 一括検索が指定されたか否か返す。
+     *
      * @return 一括検索が指定されたらtrue
      */
     public boolean isBulkSearch(){
@@ -254,7 +262,8 @@ public class FindPanel extends JDialog
 
     /**
      * キャンセルボタン押下処理。
-     * このモーダルダイアログを閉じる。
+     *
+     * <p>このモーダルダイアログを閉じる。
      */
     private void actionCancel(){
         this.canceled = true;
@@ -265,7 +274,8 @@ public class FindPanel extends JDialog
 
     /**
      * 検索ボタン押下処理。
-     * このモーダルダイアログを閉じる。
+     *
+     * <p>このモーダルダイアログを閉じる。
      */
     private void actionSubmit(){
         Object selected = this.findBox.getSelectedItem();
@@ -306,6 +316,7 @@ public class FindPanel extends JDialog
 
     /**
      * 正規表現パターン異常系のダイアログ表示。
+     *
      * @param e 正規表現構文エラー
      * @return 再入力が押されたらtrue。それ以外はfalse。
      */
@@ -356,6 +367,7 @@ public class FindPanel extends JDialog
 
     /**
      * 現時点での検索パターンを得る。
+     *
      * @return 検索パターン
      */
     public RegexPattern getRegexPattern(){
@@ -364,6 +376,7 @@ public class FindPanel extends JDialog
 
     /**
      * 検索パターンを設定する。
+     *
      * @param pattern 検索パターン
      */
     public final void setRegexPattern(RegexPattern pattern){
@@ -388,7 +401,9 @@ public class FindPanel extends JDialog
 
     /**
      * {@inheritDoc}
-     * ボタン操作時にリスナとして呼ばれる。
+     *
+     * <p>ボタン操作時にリスナとして呼ばれる。
+     *
      * @param event イベント {@inheritDoc}
      */
     @Override
@@ -407,7 +422,9 @@ public class FindPanel extends JDialog
 
     /**
      * {@inheritDoc}
-     * コンボボックスのアイテム選択リスナ。
+     *
+     * <p>コンボボックスのアイテム選択リスナ。
+     *
      * @param event アイテム選択イベント {@inheritDoc}
      */
     @Override
@@ -426,7 +443,9 @@ public class FindPanel extends JDialog
 
     /**
      * {@inheritDoc}
-     * チェックボックス操作のリスナ。
+     *
+     * <p>チェックボックス操作のリスナ。
+     *
      * @param event チェックボックス操作イベント {@inheritDoc}
      */
     @Override
@@ -448,7 +467,9 @@ public class FindPanel extends JDialog
 
     /**
      * {@inheritDoc}
-     * コンボボックスのUI変更通知を受け取るリスナ。
+     *
+     * <p>コンボボックスのUI変更通知を受け取るリスナ。
+     *
      * @param event UI差し替えイベント {@inheritDoc}
      */
     @Override
@@ -464,7 +485,9 @@ public class FindPanel extends JDialog
 
     /**
      * コンボボックスエディタを修飾する。
-     * マージン修飾と等幅フォントをいじる。
+     *
+     * <p>マージン修飾と等幅フォントをいじる。
+     *
      * @param editor エディタ
      */
     private void modifyComboBoxEditor(ComboBoxEditor editor){
@@ -485,6 +508,7 @@ public class FindPanel extends JDialog
 
     /**
      * JSON形式の検索履歴情報を返す。
+     *
      * @return JSON形式の検索履歴情報
      */
     public JsObject getJson(){
@@ -506,6 +530,7 @@ public class FindPanel extends JDialog
 
     /**
      * JSON形式の検索履歴を反映させる。
+     *
      * @param root JSON形式の検索履歴。nullが来たら何もしない
      */
     public void putJson(JsObject root){
@@ -530,6 +555,7 @@ public class FindPanel extends JDialog
 
     /**
      * 起動時の履歴設定と等価か判定する。
+     *
      * @param conf 比較対象
      * @return 等価ならtrue
      */
@@ -555,6 +581,7 @@ public class FindPanel extends JDialog
 
         /**
          * {@inheritDoc}
+         *
          * @param list {@inheritDoc}
          * @param value {@inheritDoc}
          * @param index {@inheritDoc}
@@ -647,6 +674,7 @@ public class FindPanel extends JDialog
 
         /**
          * {@inheritDoc}
+         *
          * @return {@inheritDoc}
          */
         @Override
@@ -656,6 +684,7 @@ public class FindPanel extends JDialog
 
         /**
          * {@inheritDoc}
+         *
          * @param item {@inheritDoc}
          */
         @Override
@@ -667,6 +696,7 @@ public class FindPanel extends JDialog
 
         /**
          * {@inheritDoc}
+         *
          * @param index {@inheritDoc}
          * @return {@inheritDoc}
          */
@@ -698,6 +728,7 @@ public class FindPanel extends JDialog
 
         /**
          * {@inheritDoc}
+         *
          * @return {@inheritDoc}
          */
         @Override
@@ -712,6 +743,7 @@ public class FindPanel extends JDialog
 
         /**
          * {@inheritDoc}
+         *
          * @param listener {@inheritDoc}
          */
         @Override
@@ -722,6 +754,7 @@ public class FindPanel extends JDialog
 
         /**
          * {@inheritDoc}
+         *
          * @param listener {@inheritDoc}
          */
         @Override
@@ -732,6 +765,7 @@ public class FindPanel extends JDialog
 
         /**
          * 検索履歴ヒストリ追加。
+         *
          * @param regexPattern 検索履歴
          */
         public void addHistory(RegexPattern regexPattern){
@@ -755,6 +789,7 @@ public class FindPanel extends JDialog
 
         /**
          * プリセットでない検索ヒストリリストを返す。
+         *
          * @return 検索ヒストリリスト
          */
         public List<RegexPattern> getOriginalHistoryList(){
index 2100db2..7cc8e40 100644 (file)
@@ -41,7 +41,7 @@ import jp.sfjp.jindolf.util.GUIUtils;
  * ヘルプ画面。
  */
 @SuppressWarnings("serial")
-public class HelpFrame extends JFrame
+public final class HelpFrame extends JFrame
         implements ActionListener, HyperlinkListener{
 
     private static final String HELP_HTML = "resources/html/help.html";
@@ -172,7 +172,7 @@ public class HelpFrame extends JFrame
 
         if(configStore.useStoreFile()){
             info.append("設定格納ディレクトリ : ")
-                .append(configStore.getConfigDir().getPath());
+                .append(configStore.getConfigDir().toString());
         }else{
             info.append("※ 設定格納ディレクトリは使っていません。");
         }
index 4f91e78..c5ad1e8 100644 (file)
@@ -8,10 +8,7 @@
 package jp.sfjp.jindolf.view;
 
 import java.awt.BorderLayout;
-import java.awt.EventQueue;
 import java.awt.Insets;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
 import javax.swing.BorderFactory;
 import javax.swing.Icon;
 import javax.swing.JButton;
@@ -22,7 +19,6 @@ import javax.swing.JToolBar;
 import javax.swing.JTree;
 import javax.swing.border.Border;
 import javax.swing.event.TreeSelectionEvent;
-import javax.swing.event.TreeSelectionListener;
 import javax.swing.tree.TreeModel;
 import javax.swing.tree.TreePath;
 import javax.swing.tree.TreeSelectionModel;
@@ -32,56 +28,93 @@ import jp.sfjp.jindolf.data.LandsTreeModel;
 
 /**
  * 国一覧Tree周辺コンポーネント群。
+ *
+ * <p>昇順/降順トグルボタン、村一覧リロードボタン、
+ * 人狼BBSサーバ国および村一覧ツリーから構成される。
  */
 @SuppressWarnings("serial")
-public class LandsTree
-        extends JPanel
-        implements ActionListener, TreeSelectionListener{
+public class LandsTree extends JPanel{
+
+    private static final String TIP_ASCEND =
+            "押すと降順に";
+    private static final String TIP_DESCEND =
+            "押すと昇順に";
+    private static final String TIP_ORDER =
+            "選択中の国の村一覧を読み込み直す";
+
+    private static final String RES_DIR = "resources/image/";
+    private static final String RES_ASCEND  = RES_DIR + "tb_ascend.png";
+    private static final String RES_DESCEND = RES_DIR + "tb_descend.png";
+    private static final String RES_RELOAD  = RES_DIR + "tb_reload.png";
 
-    private static final String TIP_ASCEND = "押すと降順に";
-    private static final String TIP_DESCEND = "押すと昇順に";
-    private static final String TIP_ORDER = "選択中の国の村一覧を読み込み直す";
     private static final Icon ICON_ASCEND;
     private static final Icon ICON_DESCEND;
+    private static final Icon ICON_RELOAD;
 
     static{
-        ICON_ASCEND =
-            ResourceManager.getButtonIcon("resources/image/tb_ascend.png");
-        ICON_DESCEND =
-            ResourceManager.getButtonIcon("resources/image/tb_descend.png");
+        ICON_ASCEND  = ResourceManager.getButtonIcon(RES_ASCEND);
+        ICON_DESCEND = ResourceManager.getButtonIcon(RES_DESCEND);
+        ICON_RELOAD  = ResourceManager.getButtonIcon(RES_RELOAD);
     }
 
-    private final JButton orderButton = new JButton();
-    private final JButton reloadButton = new JButton();
-    private final JTree treeView = new JTree();
+
+    private final JButton orderButton;
+    private final JButton reloadButton;
+    private final JTree treeView;
 
     private boolean ascending = false;
 
+
     /**
      * コンストラクタ。
      */
-    @SuppressWarnings("LeakingThisInConstructor")
     public LandsTree(){
         super();
 
+        this.orderButton = new JButton();
+        this.reloadButton = new JButton();
+        this.treeView = new JTree();
+
+        TreeSelectionModel selModel = this.treeView.getSelectionModel();
+        selModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+
+        setupObserver();
+
+        decorateButtons();
         design();
 
+        return;
+    }
+
+
+    /**
+     * 各種監視Observerの登録。
+     */
+    private void setupObserver(){
+        this.orderButton.addActionListener((ev) -> {
+            toggleTreeOrder();
+        });
+
+        this.treeView.addTreeSelectionListener((ev) -> {
+            updateReloadButton(ev);
+        });
+
+        this.reloadButton.setActionCommand(ActionManager.CMD_VILLAGELIST);
+
+        return;
+    }
+
+    /**
+     * ボタン群を装飾する。
+     */
+    private void decorateButtons(){
         this.orderButton.setIcon(ICON_DESCEND);
         this.orderButton.setToolTipText(TIP_DESCEND);
         this.orderButton.setMargin(new Insets(1, 1, 1, 1));
-        this.orderButton.setActionCommand(ActionManager.CMD_SWITCHORDER);
-        this.orderButton.addActionListener(this);
 
-        Icon icon = ResourceManager
-                .getButtonIcon("resources/image/tb_reload.png");
-        this.reloadButton.setIcon(icon);
+        this.reloadButton.setIcon(ICON_RELOAD);
         this.reloadButton.setToolTipText(TIP_ORDER);
         this.reloadButton.setMargin(new Insets(1, 1, 1, 1));
-        this.reloadButton.setActionCommand(ActionManager.CMD_VILLAGELIST);
-
-        TreeSelectionModel selModel = this.treeView.getSelectionModel();
-        selModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
-        this.treeView.addTreeSelectionListener(this);
 
         return;
     }
@@ -94,6 +127,7 @@ public class LandsTree
         setLayout(layout);
 
         JToolBar toolBar = new JToolBar();
+        toolBar.setFloatable(false);
         toolBar.add(this.orderButton);
         toolBar.add(this.reloadButton);
 
@@ -107,6 +141,7 @@ public class LandsTree
 
     /**
      * 国村選択ツリーコンポーネントを生成する。
+     *
      * @return 国村選択ツリーコンポーネント
      */
     private JComponent createLandSelector(){
@@ -123,6 +158,7 @@ public class LandsTree
 
     /**
      * リロードボタンを返す。
+     *
      * @return リロードボタン
      */
     public JButton getReloadVillageListButton(){
@@ -131,6 +167,7 @@ public class LandsTree
 
     /**
      * 国村選択ツリービューを返す。
+     *
      * @return 国村選択ツリービュー
      */
     public JTree getTreeView(){
@@ -138,7 +175,10 @@ public class LandsTree
     }
 
     /**
-     * 指定した国を展開する。
+     * 指定した国をツリー展開する。
+     *
+     * <p>セクション一覧が国の下にツリー展開される。
+     *
      * @param land 国
      */
     public void expandLand(Land land){
@@ -152,6 +192,7 @@ public class LandsTree
 
     /**
      * 管理下のLandsTreeModelを返す。
+     *
      * @return LandsTreeModel
      */
     private LandsTreeModel getLandsModel(){
@@ -164,74 +205,61 @@ public class LandsTree
 
     /**
      * Tree表示順を反転させる。
+     *
+     * <p>昇順/降順ボタンも表示が切り替わる。
+     *
+     * <p>選択中のツリー要素があれば選択は保持される。
+     *
      * @return 反転後が昇順ならtrue
      */
     private boolean toggleTreeOrder(){
         this.ascending = ! this.ascending;
 
+        String newTip;
+        Icon newIcon;
         if(this.ascending){
-            this.orderButton.setToolTipText(TIP_ASCEND);
-            this.orderButton.setIcon(ICON_ASCEND);
+            newTip = TIP_ASCEND;
+            newIcon = ICON_ASCEND;
         }else{
-            this.orderButton.setToolTipText(TIP_DESCEND);
-            this.orderButton.setIcon(ICON_DESCEND);
+            newTip = TIP_DESCEND;
+            newIcon = ICON_DESCEND;
         }
+        this.orderButton.setToolTipText(newTip);
+        this.orderButton.setIcon(newIcon);
 
-        final TreePath lastPath = this.treeView.getSelectionPath();
+        TreePath lastPath = this.treeView.getSelectionPath();
 
         LandsTreeModel model = getLandsModel();
         if(model != null){
             model.setAscending(this.ascending);
         }
 
-        EventQueue.invokeLater(new Runnable(){
-            @Override
-            public void run(){
-                if(lastPath != null){
-                    LandsTree.this.treeView.setSelectionPath(lastPath);
-                    LandsTree.this.treeView.scrollPathToVisible(lastPath);
-                }
-                return;
-            }
-        });
+        if(lastPath != null){
+            this.treeView.setSelectionPath(lastPath);
+            this.treeView.scrollPathToVisible(lastPath);
+        }
 
         return this.ascending;
     }
 
     /**
-     * {@inheritDoc}
-     * ボタン押下処理。
-     * @param event ボタン押下イベント {@inheritDoc}
+     * ツリー選択状況によってリロードボタンの状態を変更する。
+     *
+     * <p>国がツリー選択された状況でのみリロードボタンは有効になる。
+     * その他の状況では無効に。
+     *
+     * @param event ツリー選択状況
      */
-    @Override
-    public void actionPerformed(ActionEvent event){
-        String cmd = event.getActionCommand();
-        if(ActionManager.CMD_SWITCHORDER.equals(cmd)){
-            toggleTreeOrder();
-        }
-        return;
-    }
+    private void updateReloadButton(TreeSelectionEvent event){
+        boolean reloadEnable = false;
 
-    /**
-     * {@inheritDoc}
-     * ツリーリストで何らかの要素(国、村)がクリックされたときの処理。
-     * @param event イベント {@inheritDoc}
-     */
-    @Override
-    public void valueChanged(TreeSelectionEvent event){
         TreePath path = event.getNewLeadSelectionPath();
-        if(path == null){
-            this.reloadButton.setEnabled(false);
-            return;
+        if(path != null){
+            Object selObj = path.getLastPathComponent();
+            if(selObj instanceof Land) reloadEnable = true;
         }
 
-        Object selObj = path.getLastPathComponent();
-
-        if( selObj instanceof Land ){
-            this.reloadButton.setEnabled(true);
-        }else{
-            this.reloadButton.setEnabled(false);
-        }
+        this.reloadButton.setEnabled(reloadEnable);
 
         return;
     }
index f1ff5cf..833cda4 100644 (file)
@@ -7,75 +7,74 @@
 
 package jp.sfjp.jindolf.view;
 
-import java.awt.Component;
-import java.awt.HeadlessException;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
+import java.nio.file.Path;
+import java.text.MessageFormat;
 import javax.swing.ButtonGroup;
-import javax.swing.JButton;
-import javax.swing.JDialog;
 import javax.swing.JOptionPane;
 import javax.swing.JRadioButton;
 import jp.sfjp.jindolf.config.FileUtils;
-import jp.sfjp.jindolf.config.InterVMLock;
 
 /**
  * ロックエラー用ダイアログ。
+ *
  * <ul>
- * <li>強制解除
+ * <li>ロックの強制解除を試行
  * <li>リトライ
- * <li>設定ディレクトリを無視
+ * <li>設定ディレクトリを無視して続行
  * <li>起動中止
  * </ul>
- * の選択を利用者に求める。
+ *
+ * <p>の選択を利用者に求める。
  */
 @SuppressWarnings("serial")
-public class LockErrorPane extends JOptionPane implements ActionListener{
-
-    private final InterVMLock lock;
+public final class LockErrorPane extends JOptionPane{
 
-    private final JRadioButton continueButton =
-            new JRadioButton("設定ディレクトリを使わずに起動を続行");
-    private final JRadioButton retryButton =
-            new JRadioButton("再度ロック取得を試す");
-    private final JRadioButton forceButton =
-            new JRadioButton(
+    private static final String FORM_MAIN =
             "<html>"
-            + "ロックを強制解除<br>"
+            + "設定ディレクトリのロックファイル<br/>"
+            + "<center>[&nbsp;{0}&nbsp;]</center>"
+            + "<br/>"
+            + "のロックに失敗しました。<br/>"
+            + "考えられる原因としては、<br/>"
+            + "<ul>"
+            + "<li>前回起動したJindolfの終了が正しく行われなかった"
+            + "<li>今どこかで他のJindolfが動いている"
+            + "</ul>"
+            + "などが考えられます。<br/>"
+            + "<hr/>"
+            + "</html>";
+
+    private static final String LABEL_CONTINUE =
+            "設定ディレクトリを使わずに起動を続行";
+    private static final String LABEL_RETRY =
+            "再度ロック取得を試す";
+    private static final String LABEL_FORCE =
+            "<html>"
+            + "ロックを強制解除<br/>"
             + " (※他のJindolfと設定ファイル書き込みが衝突するかも…)"
-            + "</html>");
+            + "</html>";
+
+    private static final String LABEL_OK    = "OK";
+    private static final String LABEL_ABORT = "起動中止";
+    private static final String[] OPTIONS = {LABEL_OK, LABEL_ABORT};
 
-    private final JButton okButton = new JButton("OK");
-    private final JButton abortButton = new JButton("起動中止");
 
-    private boolean aborted = false;
+    private final JRadioButton continueButton;
+    private final JRadioButton retryButton;
+    private final JRadioButton forceButton;
+
 
     /**
      * コンストラクタ。
-     * @param lock 失敗したロック
+     *
+     * @param lockFile ロックに失敗したファイル
      */
-    @SuppressWarnings("LeakingThisInConstructor")
-    public LockErrorPane(InterVMLock lock){
+    public LockErrorPane(Path lockFile){
         super();
 
-        this.lock = lock;
-
-        String htmlMessage =
-                "<html>"
-                + "設定ディレクトリのロックファイル<br>"
-                + "<center>[&nbsp;"
-                + FileUtils.getHtmledFileName(this.lock.getLockFile())
-                + "&nbsp;]</center>"
-                + "<br>"
-                + "のロックに失敗しました。<br>"
-                + "考えられる原因としては、<br>"
-                + "<ul>"
-                + "<li>前回起動したJindolfの終了が正しく行われなかった"
-                + "<li>今どこかで他のJindolfが動いている"
-                + "</ul>"
-                + "などが考えられます。<br>"
-                + "<hr>"
-                + "</html>";
+        this.continueButton = new JRadioButton(LABEL_CONTINUE);
+        this.retryButton    = new JRadioButton(LABEL_RETRY);
+        this.forceButton    = new JRadioButton(LABEL_FORCE);
 
         ButtonGroup bgrp = new ButtonGroup();
         bgrp.add(this.continueButton);
@@ -83,30 +82,40 @@ public class LockErrorPane extends JOptionPane implements ActionListener{
         bgrp.add(this.forceButton);
         this.continueButton.setSelected(true);
 
+        String lockName = FileUtils.getHtmledFileName(lockFile);
+        String htmlMessage = MessageFormat.format(FORM_MAIN, lockName);
+
         Object[] msg = {
             htmlMessage,
             this.continueButton,
             this.retryButton,
             this.forceButton,
         };
-        setMessage(msg);
-
-        Object[] opts = {
-            this.okButton,
-            this.abortButton,
-        };
-        setOptions(opts);
 
+        setMessage(msg);
+        setOptions(OPTIONS);
         setMessageType(JOptionPane.ERROR_MESSAGE);
 
-        this.okButton   .addActionListener(this);
-        this.abortButton.addActionListener(this);
-
         return;
     }
 
+
+    /**
+     * 「起動中止」が選択されたか判定する。
+     *
+     * @param value ダイアログ結果
+     * @return 「起動中止」が押されていたならtrue
+     * @see JOptionPane#getValue()
+     */
+    public static boolean isAborted(Object value){
+        boolean flag = LABEL_ABORT.equals(value);
+        return flag;
+    }
+
+
     /**
      * 「設定ディレクトリを無視して続行」が選択されたか判定する。
+     *
      * @return 「無視して続行」が選択されていればtrue
      */
     public boolean isRadioContinue(){
@@ -115,6 +124,7 @@ public class LockErrorPane extends JOptionPane implements ActionListener{
 
     /**
      * 「リトライ」が選択されたか判定する。
+     *
      * @return 「リトライ」が選択されていればtrue
      */
     public boolean isRadioRetry(){
@@ -123,58 +133,11 @@ public class LockErrorPane extends JOptionPane implements ActionListener{
 
     /**
      * 「強制解除」が選択されたか判定する。
+     *
      * @return 「強制解除」が選択されていればtrue
      */
     public boolean isRadioForce(){
         return this.forceButton.isSelected();
     }
 
-    /**
-     * 「起動中止」が選択されたか判定する。
-     * @return 「起動中止」が押されていたならtrue
-     */
-    public boolean isAborted(){
-        return this.aborted;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @param parentComponent {@inheritDoc}
-     * @param title {@inheritDoc}
-     * @return {@inheritDoc}
-     * @throws HeadlessException {@inheritDoc}
-     */
-    @Override
-    public JDialog createDialog(Component parentComponent,
-                                    String title)
-            throws HeadlessException{
-        final JDialog dialog =
-                super.createDialog(parentComponent, title);
-
-        ActionListener listener = new ActionListener(){
-            @Override
-            public void actionPerformed(ActionEvent event){
-                dialog.setVisible(false);
-                return;
-            }
-        };
-
-        this.okButton   .addActionListener(listener);
-        this.abortButton.addActionListener(listener);
-
-        return dialog;
-    }
-
-    /**
-     * ボタン押下を受信する。
-     * @param event イベント
-     */
-    @Override
-    public void actionPerformed(ActionEvent event){
-        Object source = event.getSource();
-        if(source == this.okButton) this.aborted = false;
-        else                        this.aborted = true;
-        return;
-    }
-
 }
index d15fde3..3f4528b 100644 (file)
@@ -8,7 +8,7 @@
 package jp.sfjp.jindolf.view;
 
 import java.awt.Container;
-import java.awt.Frame;
+import java.awt.Dialog;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.Insets;
@@ -29,7 +29,7 @@ import jp.sfjp.jindolf.util.GUIUtils;
  * オプション設定パネル。
  */
 @SuppressWarnings("serial")
-public class OptionPanel
+public final class OptionPanel
         extends JDialog
         implements ActionListener, WindowListener{
 
@@ -46,11 +46,12 @@ public class OptionPanel
 
     /**
      * コンストラクタ。
-     * @param owner フレームオーナ
      */
     @SuppressWarnings("LeakingThisInConstructor")
-    public OptionPanel(Frame owner){
-        super(owner);
+    public OptionPanel(){
+        super((Dialog)null);
+        // We need unowned dialog
+
         setModal(true);
 
         GUIUtils.modifyWindowAttributes(this, true, false, true);
@@ -76,6 +77,7 @@ public class OptionPanel
 
     /**
      * レイアウトを行う。
+     *
      * @param content コンテナ
      */
     private void design(Container content){
@@ -109,6 +111,7 @@ public class OptionPanel
 
     /**
      * FontChooserを返す。
+     *
      * @return FontChooser
      */
     public FontChooser getFontChooser(){
@@ -117,6 +120,7 @@ public class OptionPanel
 
     /**
      * ProxyChooserを返す。
+     *
      * @return ProxyChooser
      */
     public ProxyChooser getProxyChooser(){
@@ -125,6 +129,7 @@ public class OptionPanel
 
     /**
      * DialogPrefPanelを返す。
+     *
      * @return DialogPrefPanel
      */
     public DialogPrefPanel getDialogPrefPanel(){
@@ -133,7 +138,9 @@ public class OptionPanel
 
     /**
      * ダイアログが閉じられた原因が「キャンセル」か否か判定する。
-     * ウィンドウクローズ操作は「キャンセル」扱い。
+     *
+     * <p>ウィンドウクローズ操作は「キャンセル」扱い。
+     *
      * @return 「キャンセル」ならtrue
      */
     public boolean isCanceled(){
@@ -142,7 +149,8 @@ public class OptionPanel
 
     /**
      * OKボタン押下処理。
-     * ダイアログを閉じる。
+     *
+     * <p>ダイアログを閉じる。
      */
     private void actionOk(){
         this.isCanceled = false;
@@ -153,7 +161,8 @@ public class OptionPanel
 
     /**
      * キャンセルボタン押下処理。
-     * ダイアログを閉じる。
+     *
+     * <p>ダイアログを閉じる。
      */
     private void actionCancel(){
         this.isCanceled = true;
@@ -164,6 +173,7 @@ public class OptionPanel
 
     /**
      * ボタン押下イベント受信。
+     *
      * @param event イベント
      */
     @Override
@@ -176,6 +186,7 @@ public class OptionPanel
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -185,8 +196,11 @@ public class OptionPanel
 
     /**
      * {@inheritDoc}
-     * ダイアログを閉じる。
-     * キャンセルボタン押下時と同じ。
+     *
+     * <p>ダイアログを閉じる。
+     *
+     * <p>キャンセルボタン押下時と同じ。
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -197,6 +211,7 @@ public class OptionPanel
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -206,6 +221,7 @@ public class OptionPanel
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -215,6 +231,7 @@ public class OptionPanel
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -224,6 +241,7 @@ public class OptionPanel
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -233,6 +251,7 @@ public class OptionPanel
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
index 8315720..098340f 100644 (file)
@@ -46,7 +46,7 @@ import jp.sfjp.jindolf.data.Village;
  * プログレスバーとフッタメッセージの管理を行う。
  */
 @SuppressWarnings("serial")
-public class TopView extends JPanel{
+public final class TopView extends JPanel{
 
     private static final String INITCARD = "INITCARD";
     private static final String LANDCARD = "LANDINFO";
@@ -80,6 +80,7 @@ public class TopView extends JPanel{
         super();
 
         this.cards = createCards();
+
         JComponent split = createSplitPane(this.landsTreeView, this.cards);
         JComponent statusBar = createStatusBar();
 
@@ -129,7 +130,7 @@ public class TopView extends JPanel{
 
         StringBuilder warn = new StringBuilder();
         warn.append("※ たまにはWebブラウザでアクセスして、");
-        warn.append("<br></br>");
+        warn.append("<br/>");
         warn.append("運営の動向を確かめようね!");
         warn.insert(0, "<font 'size=+1'>").append("</font>");
         warn.insert(0, "<center>").append("</center>");
index 1949268..c46fb2a 100644 (file)
@@ -8,10 +8,10 @@
 package jp.sfjp.jindolf.view;
 
 import java.awt.EventQueue;
-import java.awt.Frame;
 import java.awt.Window;
 import java.util.LinkedList;
 import java.util.List;
+import javax.swing.JOptionPane;
 import javax.swing.SwingUtilities;
 import javax.swing.UIManager;
 import javax.swing.UnsupportedLookAndFeelException;
@@ -22,6 +22,32 @@ import jp.sfjp.jindolf.summary.VillageDigest;
 
 /**
  * ウィンドウ群の管理を行う。
+ *
+ * <p>原則として閉じても再利用されるウィンドウを管理対象とする。
+ *
+ * <p>管理対象ウィンドウは
+ *
+ * <ul>
+ * <li>アプリのトップウィンドウ
+ * <li>検索ウィンドウ
+ * <li>フィルタウィンドウ
+ * <li>発言集計ウィンドウ
+ * <li>村プレイ記録のダイジェストウィンドウ
+ * <li>オプション設定ウィンドウ
+ * <li>ヘルプウィンドウ
+ * <li>ログウィンドウ
+ * </ul>
+ *
+ * <p>である。
+ *
+ * <p>トップウィンドウとヘルプウィンドウは{@link javax.swing.JFrame}、
+ * その他は{@link javax.swing.JDialog}である。
+ *
+ * <p>非モーダルダイアログは、他のウィンドウの下側にも回れるのが望ましい。
+ *
+ * <p>各ウィンドウは、他のウィンドウの下に完全に隠れても
+ * Windowsタスクバーなどを介して前面に引っ張り出す操作手段を
+ * 提供することが望ましい。
  */
 public class WindowManager {
 
@@ -40,8 +66,8 @@ public class WindowManager {
     private static final String TITLE_HELP =
             getFrameTitle("ヘルプ");
 
-    private static final Frame NULLPARENT = null;
 
+    private final TopFrame topFrame;
 
     private FilterPanel filterPanel;
     private LogFrame logFrame;
@@ -50,9 +76,8 @@ public class WindowManager {
     private VillageDigest villageDigest;
     private DaySummary daySummary;
     private HelpFrame helpFrame;
-    private TopFrame topFrame;
 
-    private final List<Window> windowSet = new LinkedList<>();
+    private final List<Window> windowSet;
 
 
     /**
@@ -60,6 +85,15 @@ public class WindowManager {
      */
     public WindowManager(){
         super();
+
+        this.topFrame = new TopFrame();
+        this.topFrame.setVisible(false);
+
+        JOptionPane.setRootFrame(this.topFrame);
+
+        this.windowSet = new LinkedList<>();
+        this.windowSet.add(this.topFrame);
+
         return;
     }
 
@@ -84,7 +118,7 @@ public class WindowManager {
     protected FilterPanel createFilterPanel(){
         FilterPanel result;
 
-        result = new FilterPanel(NULLPARENT);
+        result = new FilterPanel();
         result.setTitle(TITLE_FILTER);
         result.pack();
         result.setVisible(false);
@@ -114,7 +148,7 @@ public class WindowManager {
     protected LogFrame createLogFrame(){
         LogFrame result;
 
-        result = new LogFrame(NULLPARENT);
+        result = new LogFrame();
         result.setTitle(TITLE_LOGGER);
         result.pack();
         result.setSize(600, 500);
@@ -146,7 +180,7 @@ public class WindowManager {
     protected OptionPanel createOptionPanel(){
         OptionPanel result;
 
-        result = new OptionPanel(NULLPARENT);
+        result = new OptionPanel();
         result.setTitle(TITLE_OPTION);
         result.pack();
         result.setSize(450, 500);
@@ -177,7 +211,7 @@ public class WindowManager {
     protected FindPanel createFindPanel(){
         FindPanel result;
 
-        result = new FindPanel(NULLPARENT);
+        result = new FindPanel();
         result.setTitle(TITLE_FIND);
         result.pack();
         result.setVisible(false);
@@ -207,7 +241,7 @@ public class WindowManager {
     protected VillageDigest createVillageDigest(){
         VillageDigest result;
 
-        result = new VillageDigest(NULLPARENT);
+        result = new VillageDigest();
         result.setTitle(TITLE_DIGEST);
         result.pack();
         result.setSize(600, 550);
@@ -238,7 +272,7 @@ public class WindowManager {
     protected DaySummary createDaySummary(){
         DaySummary result;
 
-        result = new DaySummary(NULLPARENT);
+        result = new DaySummary();
         result.setTitle(TITLE_DAYSUMMARY);
         result.pack();
         result.setSize(400, 500);
@@ -293,26 +327,11 @@ public class WindowManager {
     }
 
     /**
-     * トップフレームを生成する。
-     *
-     * @return トップフレーム
-     */
-    protected TopFrame createTopFrame(){
-        TopFrame result = new TopFrame();
-        result.setVisible(false);
-        this.windowSet.add(result);
-        return result;
-    }
-
-    /**
      * トップフレームを返す。
      *
      * @return トップフレーム
      */
     public TopFrame getTopFrame(){
-        if(this.topFrame == null){
-            this.topFrame = createTopFrame();
-        }
         return this.topFrame;
     }
 
@@ -332,12 +351,12 @@ public class WindowManager {
 
         UIManager.setLookAndFeel(className);
 
-        this.windowSet.forEach((window) -> {
+        this.windowSet.forEach(window -> {
             SwingUtilities.updateComponentTreeUI(window);
         });
 
-        if(this.filterPanel  != null) this.filterPanel.pack();
-        if(this.findPanel    != null) this.findPanel.pack();
+        if(this.filterPanel != null) this.filterPanel.pack();
+        if(this.findPanel   != null) this.findPanel.pack();
 
         return;
     }
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/README.txt b/src/main/resources/jp/sfjp/jindolf/resources/README.txt
new file mode 100644 (file)
index 0000000..04df194
--- /dev/null
@@ -0,0 +1,6 @@
+UTF-8 Japanese
+このディレクトリは、Jindolfの各種設定が格納されるディレクトリです。
+Jindolfの詳細は http://jindolf.osdn.jp/ を参照してください。
+このディレクトリを「Jindolf」の名前で起動元JARファイルと同じ位置に
+コピーすれば、そちらの設定が優先して使われます。
+「lock」の名前を持つファイルはロックファイルです。