OSDN Git Service

改行コード指定
[jindolf/Jindolf.git] / src / main / java / jp / sourceforge / jindolf / ConfigFile.java
index 69ad681..7e34b6c 100644 (file)
-/*\r
- * configuration file & directory\r
- *\r
- * License : The MIT License\r
- * Copyright(c) 2009 olyutorskii\r
- */\r
-\r
-package jp.sourceforge.jindolf;\r
-\r
-import java.awt.Component;\r
-import java.awt.HeadlessException;\r
-import java.awt.event.ActionEvent;\r
-import java.awt.event.ActionListener;\r
-import java.io.BufferedInputStream;\r
-import java.io.BufferedOutputStream;\r
-import java.io.File;\r
-import java.io.FileInputStream;\r
-import java.io.FileNotFoundException;\r
-import java.io.FileOutputStream;\r
-import java.io.IOException;\r
-import java.io.InputStream;\r
-import java.io.InputStreamReader;\r
-import java.io.OutputStream;\r
-import java.io.OutputStreamWriter;\r
-import java.io.PrintWriter;\r
-import java.io.Reader;\r
-import java.io.Writer;\r
-import java.nio.charset.Charset;\r
-import javax.swing.ButtonGroup;\r
-import javax.swing.JButton;\r
-import javax.swing.JDialog;\r
-import javax.swing.JOptionPane;\r
-import javax.swing.JRadioButton;\r
-import jp.sourceforge.jindolf.json.JsParseException;\r
-import jp.sourceforge.jindolf.json.JsValue;\r
-import jp.sourceforge.jindolf.json.Json;\r
-\r
-/**\r
- * Jindolf設定格納ディレクトリに関するあれこれ。\r
- */\r
-public final class ConfigFile{\r
-\r
-    private static final String TITLE_BUILDCONF =\r
-            Jindolf.TITLE + "設定格納ディレクトリの設定";\r
-\r
-    private static final String JINCONF     = "Jindolf";\r
-    private static final String JINCONF_DOT = ".jindolf";\r
-    private static final String FILE_README = "README.txt";\r
-    private static final Charset CHARSET_README = Charset.forName("UTF-8");\r
-    private static final Charset CHARSET_JSON = Charset.forName("UTF-8");\r
-\r
-    private static final String MSG_POST =\r
-            "<ul>"\r
-            + "<li><code>" + CmdOption.OPT_CONFDIR.toHyphened() + "</code>"\r
-            + "&nbsp;オプション指定により、<br>"\r
-            + "任意の設定格納ディレクトリを指定することができます。<br>"\r
-            + "<li><code>" + CmdOption.OPT_NOCONF.toHyphened() + "</code>"\r
-            + "&nbsp;オプション指定により、<br>"\r
-            + "設定格納ディレクトリを使わずに起動することができます。<br>"\r
-            + "</ul>";\r
-\r
-\r
-    /**\r
-     * 隠れコンストラクタ。\r
-     */\r
-    private ConfigFile(){\r
-        super();\r
-        return;\r
-    }\r
-\r
-\r
-    /**\r
-     * 設定格納ディレクトリのセットアップ。\r
-     * @return 設定格納ディレクトリ\r
-     */\r
-    public static File setupConfigDirectory(){\r
-        AppSetting setting = Jindolf.getAppSetting();\r
-        File configPath;\r
-\r
-        if( ! setting.useConfigPath() ){\r
-            configPath = null;\r
-        }else{\r
-            String optName;\r
-            if(setting.getConfigPath() != null){\r
-                configPath = setting.getConfigPath();\r
-                optName = CmdOption.OPT_CONFDIR.toHyphened();\r
-            }else{\r
-                configPath = ConfigFile.getImplicitConfigDirectory();\r
-                optName = null;\r
-            }\r
-            if( ! configPath.exists() ){\r
-                configPath =\r
-                        ConfigFile.buildConfigDirectory(configPath, optName);\r
-            }\r
-            ConfigFile.checkAccessibility(configPath);\r
-        }\r
-\r
-        setting.setConfigPath(configPath);\r
-\r
-        return configPath;\r
-    }\r
-\r
-    /**\r
-     * ロックファイルのセットアップ。\r
-     * @return ロックオブジェクト\r
-     */\r
-    public static InterVMLock setupLockFile(){\r
-        AppSetting setting = Jindolf.getAppSetting();\r
-\r
-        File configPath = setting.getConfigPath();\r
-        if(configPath == null) return null;\r
-\r
-        File lockFile = new File(configPath, "lock");\r
-        InterVMLock lock = new InterVMLock(lockFile);\r
-\r
-        lock.tryLock();\r
-\r
-        if( ! lock.isFileOwner() ){\r
-            confirmLockError(lock);\r
-            if( ! lock.isFileOwner() ){\r
-                setting.setConfigPath(null);\r
-                setting.setUseConfigPath(false);\r
-            }\r
-        }\r
-\r
-        return lock;\r
-    }\r
-\r
-    /**\r
-     * 暗黙的な設定格納ディレクトリを返す。\r
-     * 起動元JARファイルと同じディレクトリに、\r
-     * アクセス可能なディレクトリ"Jindolf"が\r
-     * すでに存在していればそれを返す。\r
-     * 起動元JARファイルおよび"Jindolf"が発見できなければ、\r
-     * MacOSX環境の場合"~/Library/Application Support/Jindolf/"を返す。\r
-     * Windows環境の場合"%USERPROFILE%\Jindolf\"を返す。\r
-     * それ以外の環境(Linux,etc?)の場合"~/.jindolf/"を返す。\r
-     * 返すディレクトリが存在しているか否か、\r
-     * アクセス可能か否かは呼び出し元で判断せよ。\r
-     * @return 設定格納ディレクトリ\r
-     */\r
-    public static File getImplicitConfigDirectory(){\r
-        File result;\r
-\r
-        File jarParent = FileUtils.getJarDirectory(Jindolf.class);\r
-        if(jarParent != null && FileUtils.isAccessibleDirectory(jarParent)){\r
-            result = new File(jarParent, JINCONF);\r
-            if(FileUtils.isAccessibleDirectory(result)){\r
-                return result;\r
-            }\r
-        }\r
-\r
-        File appset = FileUtils.getAppSetDir();\r
-        if(appset == null) return null;\r
-\r
-        if(FileUtils.isMacOSXFs() || FileUtils.isWindowsOSFs()){\r
-            result = new File(appset, JINCONF);\r
-        }else{\r
-            result = new File(appset, JINCONF_DOT);\r
-        }\r
-\r
-        return result;\r
-    }\r
-\r
-    /**\r
-     * まだ存在しない設定格納ディレクトリを新規に作成する。\r
-     * エラーがあればダイアログ提示とともにVM終了する。\r
-     * @param confPath 設定格納ディレクトリ\r
-     * @param optName 設定を指定したオプション名。\r
-     * 暗黙的に指示されたものならnullを渡すべし。\r
-     * @return 新規に作成した設定格納ディレクトリ\r
-     * @throws IllegalArgumentException すでにそのディレクトリは存在する。\r
-     */\r
-    public static File buildConfigDirectory(File confPath,\r
-                                               String optName)\r
-            throws IllegalArgumentException{\r
-        if(confPath.exists()) throw new IllegalArgumentException();\r
-\r
-        File absPath = FileUtils.supplyFullPath(confPath);\r
-\r
-        String preErrMessage =\r
-                "設定格納ディレクトリ<br>"\r
-                + getCenteredFileName(absPath)\r
-                + "の作成に失敗しました。";\r
-        if(optName != null){\r
-            preErrMessage =\r
-                    "<code>" + optName + "</code>&nbsp;オプション"\r
-                    + "で指定された、<br>"\r
-                    + preErrMessage;\r
-        }\r
-\r
-        File existsAncestor = FileUtils.findExistsAncestor(absPath);\r
-        if(existsAncestor == null){\r
-            abortNoRoot(absPath, preErrMessage);\r
-        }else if( ! existsAncestor.canWrite() ){\r
-            abortCantWriteAncestor(existsAncestor, preErrMessage);\r
-        }\r
-\r
-        String prompt =\r
-                "設定ファイル格納ディレクトリ<br>"\r
-                + getCenteredFileName(absPath)\r
-                + "を作成します。";\r
-        boolean confirmed = confirmBuildConfigDir(existsAncestor, prompt);\r
-        if( ! confirmed ){\r
-            abortQuitBuildConfigDir();\r
-        }\r
-\r
-        boolean success;\r
-        try{\r
-            success = absPath.mkdirs();\r
-        }catch(SecurityException e){\r
-            success = false;\r
-        }\r
-\r
-        if( ! success || ! absPath.exists() ){\r
-            abortCantBuildConfigDir(absPath);\r
-        }\r
-\r
-        FileUtils.setOwnerOnlyAccess(absPath);\r
-\r
-        checkAccessibility(absPath);\r
-\r
-        touchReadme(absPath);\r
-\r
-        return absPath;\r
-    }\r
-\r
-    /**\r
-     * 設定ディレクトリ操作の\r
-     * 共通エラーメッセージ確認ダイアログを表示する。\r
-     * 閉じるまで待つ。\r
-     * @param seq メッセージ\r
-     */\r
-    private static void showErrorMessage(CharSequence seq){\r
-        JOptionPane pane =\r
-                new JOptionPane(seq.toString(),\r
-                                JOptionPane.ERROR_MESSAGE);\r
-        showDialog(pane);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 設定ディレクトリ操作の\r
-     * 共通エラーメッセージ確認ダイアログを表示する。\r
-     * 閉じるまで待つ。\r
-     * @param seq メッセージ\r
-     */\r
-    private static void showWarnMessage(CharSequence seq){\r
-        JOptionPane pane =\r
-                new JOptionPane(seq.toString(),\r
-                                JOptionPane.WARNING_MESSAGE);\r
-        showDialog(pane);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 設定ディレクトリ操作の\r
-     * 情報提示メッセージ確認ダイアログを表示する。\r
-     * 閉じるまで待つ。\r
-     * @param seq メッセージ\r
-     */\r
-    private static void showInfoMessage(CharSequence seq){\r
-        JOptionPane pane =\r
-                new JOptionPane(seq.toString(),\r
-                                JOptionPane.INFORMATION_MESSAGE);\r
-        showDialog(pane);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * ダイアログを表示し、閉じられるまで待つ。\r
-     * @param pane ダイアログの元となるペイン\r
-     */\r
-    private static void showDialog(JOptionPane pane){\r
-        JDialog dialog = pane.createDialog(null, TITLE_BUILDCONF);\r
-        dialog.setResizable(true);\r
-        dialog.pack();\r
-\r
-        dialog.setVisible(true);\r
-        dialog.dispose();\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 設定ディレクトリのルートファイルシステムもしくはドライブレターに\r
-     * アクセスできないエラーをダイアログに提示し、VM終了する。\r
-     * @param path 設定ディレクトリ\r
-     * @param preMessage メッセージ前半\r
-     */\r
-    private static void abortNoRoot(File path, String preMessage){\r
-        File root = FileUtils.findRootFile(path);\r
-        showErrorMessage(\r
-                "<html>"\r
-                + preMessage + "<br>"\r
-                + getCenteredFileName(root)\r
-                + "を用意する方法が不明です。<br>"\r
-                + "起動を中止します。<br>"\r
-                + MSG_POST\r
-                + "</html>" );\r
-        Jindolf.exit(1);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 設定ディレクトリの祖先に書き込めないエラーをダイアログで提示し、\r
-     * VM終了する。\r
-     * @param existsAncestor 存在するもっとも近い祖先\r
-     * @param preMessage メッセージ前半\r
-     */\r
-    private static void abortCantWriteAncestor(File existsAncestor,\r
-                                                  String preMessage ){\r
-        showErrorMessage(\r
-                "<html>"\r
-                + preMessage + "<br>"\r
-                + getCenteredFileName(existsAncestor)\r
-                + "への書き込みができないため、"\r
-                + "処理の続行は不可能です。<br>"\r
-                + "起動を中止します。<br>"\r
-                + MSG_POST\r
-                + "</html>" );\r
-        Jindolf.exit(1);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 設定ディレクトリを新規に生成してよいかダイアログで問い合わせる。\r
-     * @param existsAncestor 存在するもっとも近い祖先\r
-     * @param preMessage メッセージ前半\r
-     * @return 生成してよいと指示があればtrue\r
-     */\r
-    private static boolean confirmBuildConfigDir(File existsAncestor,\r
-                                                    String preMessage){\r
-        String message =\r
-                "<html>"\r
-                + preMessage + "<br>"\r
-                + "このディレクトリを今から<br>"\r
-                + getCenteredFileName(existsAncestor)\r
-                + "に作成して構いませんか?<br>"\r
-                + "このディレクトリ名は、後からいつでもヘルプウィンドウで<br>"\r
-                + "確認することができます。"\r
-                + "</html>";\r
-\r
-        JOptionPane pane =\r
-                new JOptionPane(message,\r
-                                JOptionPane.QUESTION_MESSAGE,\r
-                                JOptionPane.YES_NO_OPTION);\r
-\r
-        showDialog(pane);\r
-\r
-        Object result = pane.getValue();\r
-        if(result == null) return false;\r
-        else if( ! (result instanceof Integer) ) return false;\r
-\r
-        int ival = (Integer) result;\r
-        if(ival == JOptionPane.YES_OPTION) return true;\r
-\r
-        return false;\r
-    }\r
-\r
-    /**\r
-     * 設定ディレクトリ生成をやめた操作への警告をダイアログで提示し、\r
-     * VM終了する。\r
-     */\r
-    private static void abortQuitBuildConfigDir(){\r
-        showWarnMessage(\r
-                "<html>"\r
-                + "設定ディレクトリの作成をせずに起動を中止します。<br>"\r
-                + MSG_POST\r
-                + "</html>" );\r
-        Jindolf.exit(1);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 設定ディレクトリが生成できないエラーをダイアログで提示し、\r
-     * VM終了する。\r
-     * @param path 生成できなかったディレクトリ\r
-     */\r
-    private static void abortCantBuildConfigDir(File path){\r
-        showErrorMessage(\r
-                "<html>"\r
-                + "設定ディレクトリ<br>"\r
-                + getCenteredFileName(path)\r
-                + "の作成に失敗しました。"\r
-                + "起動を中止します。<br>"\r
-                + MSG_POST\r
-                + "</html>" );\r
-        Jindolf.exit(1);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 設定ディレクトリへアクセスできないエラーをダイアログで提示し、\r
-     * VM終了する。\r
-     * @param path アクセスできないディレクトリ\r
-     */\r
-    private static void abortCantAccessConfigDir(File path){\r
-        showErrorMessage(\r
-                "<html>"\r
-                + "設定ディレクトリ<br>"\r
-                + getCenteredFileName(path)\r
-                + "へのアクセスができません。"\r
-                + "起動を中止します。<br>"\r
-                + "このディレクトリへのアクセス権を調整し"\r
-                + "読み書きできるようにしてください。<br>"\r
-                + MSG_POST\r
-                + "</html>" );\r
-        Jindolf.exit(1);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * ファイルに書き込めないエラーをダイアログで提示し、VM終了する。\r
-     * @param file 書き込めなかったファイル\r
-     */\r
-    private static void abortCantWrite(File file){\r
-        showErrorMessage(\r
-                "<html>"\r
-                + "ファイル<br>"\r
-                + getCenteredFileName(file)\r
-                + "への書き込みができません。"\r
-                + "起動を中止します。<br>"\r
-                + "</html>" );\r
-        Jindolf.exit(1);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 指定されたディレクトリにREADMEファイルを生成する。\r
-     * 生成できなければダイアログ表示とともにVM終了する。\r
-     * @param path READMEの格納ディレクトリ\r
-     */\r
-    private static void touchReadme(File path){\r
-        File file = new File(path, FILE_README);\r
-\r
-        try{\r
-            file.createNewFile();\r
-        }catch(IOException e){\r
-            abortCantAccessConfigDir(path);\r
-        }\r
-\r
-        PrintWriter writer = null;\r
-        try{\r
-            OutputStream ostream = new FileOutputStream(file);\r
-            Writer owriter = new OutputStreamWriter(ostream, CHARSET_README);\r
-            writer = new PrintWriter(owriter);\r
-            writer.println(CHARSET_README.name() + " Japanese");\r
-            writer.println(\r
-                    "このディレクトリは、"\r
-                    + "Jindolfの各種設定が格納されるディレクトリです。");\r
-            writer.println(\r
-                    "Jindolfの詳細は "\r
-                    + "http://jindolf.sourceforge.jp/"\r
-                    + " を参照してください。");\r
-            writer.println(\r
-                    "このディレクトリを"\r
-                    + "「" + JINCONF + "」"\r
-                    + "の名前で起動元JARファイルと"\r
-                    + "同じ位置に");\r
-            writer.println(\r
-                    "コピーすれば、そちらの設定が優先して使われます。");\r
-            writer.println(\r
-                    "「lock」の名前を持つファイルはロックファイルです。");\r
-        }catch(IOException e){\r
-            abortCantWrite(file);\r
-        }catch(SecurityException e){\r
-            abortCantWrite(file);\r
-        }finally{\r
-            if(writer != null){\r
-                writer.close();\r
-            }\r
-        }\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 設定ディレクトリがアクセス可能でなければ\r
-     * エラーダイアログを出してVM終了する。\r
-     * @param confDir 設定ディレクトリ\r
-     */\r
-    public static void checkAccessibility(File confDir){\r
-        if( ! FileUtils.isAccessibleDirectory(confDir) ){\r
-            abortCantAccessConfigDir(confDir);\r
-        }\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * センタリングされたファイル名表示のHTML表記を出力する。\r
-     * @param path ファイル\r
-     * @return HTML表記\r
-     */\r
-    public static String getCenteredFileName(File path){\r
-        return "<center>[&nbsp;"\r
-                + FileUtils.getHtmledFileName(path)\r
-                + "&nbsp;]</center>"\r
-                + "<br>";\r
-    }\r
-\r
-    /**\r
-     * ロックエラーダイアログの表示。\r
-     * 呼び出しから戻ってもまだロックオブジェクトが\r
-     * ロックファイルのオーナーでない場合、\r
-     * 今後設定ディレクトリは一切使わずに起動を続行するものとする。\r
-     * ロックファイルの強制解除に失敗した場合はVM終了する。\r
-     * @param lock エラーを起こしたロック\r
-     */\r
-    public static void confirmLockError(InterVMLock lock){\r
-        LockErrorPane pane = new LockErrorPane(lock);\r
-        JDialog dialog = pane.createDialog(null, TITLE_BUILDCONF);\r
-        dialog.setResizable(true);\r
-        dialog.pack();\r
-\r
-        for(;;){\r
-            dialog.setVisible(true);\r
-            dialog.dispose();\r
-\r
-            if(pane.isAborted() || pane.getValue() == null){\r
-                Jindolf.exit(1);\r
-                break;\r
-            }else if(pane.isRadioRetry()){\r
-                lock.tryLock();\r
-                if(lock.isFileOwner()) break;\r
-            }else if(pane.isRadioContinue()){\r
-                showInfoMessage(\r
-                        "<html>"\r
-                        + "設定ディレクトリを使わずに起動を続行します。<br>"\r
-                        + "今回、各種設定の読み込み・保存はできません。<br>"\r
-                        + "<code>"\r
-                        + CmdOption.OPT_NOCONF.toHyphened()\r
-                        + "</code> オプション"\r
-                        + "を使うとこの警告は出なくなります。"\r
-                        + "</html>");\r
-                break;\r
-            }else if(pane.isRadioForce()){\r
-                lock.forceRemove();\r
-                if(lock.isExistsFile()){\r
-                    showErrorMessage(\r
-                            "<html>"\r
-                            + "ロックファイルの強制解除に失敗しました。<br>"\r
-                            + "他に動いているJindolf"\r
-                            + "が見つからないのであれば、<br>"\r
-                            + "なんとかしてロックファイル<br>"\r
-                            + getCenteredFileName(lock.getLockFile())\r
-                            + "を削除してください。<br>"\r
-                            + "起動を中止します。"\r
-                            + "</html>");\r
-                    Jindolf.exit(1);\r
-                    break;\r
-                }\r
-                lock.tryLock();\r
-                if(lock.isFileOwner()) break;\r
-                showErrorMessage(\r
-                        "<html>"\r
-                        + "ロックファイル<br>"\r
-                        + getCenteredFileName(lock.getLockFile())\r
-                        + "を確保することができません。<br>"\r
-                        + "起動を中止します。"\r
-                        + "</html>");\r
-                Jindolf.exit(1);\r
-                break;\r
-            }\r
-        }\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 設定ディレクトリ上のJSONファイルを読み込む。\r
-     * @param file JSONファイルの相対パス\r
-     * @return JSON objectまたはarray。\r
-     * 設定ディレクトリを使わない設定、\r
-     * もしくはJSONファイルが存在しない、\r
-     * もしくは入力エラーがあればnull\r
-     */\r
-    public static JsValue loadJson(File file){\r
-        AppSetting setting = Jindolf.getAppSetting();\r
-        if( ! setting.useConfigPath() ) return null;\r
-\r
-        File absFile;\r
-        if(file.isAbsolute()){\r
-            absFile = file;\r
-        }else{\r
-            File configPath = setting.getConfigPath();\r
-            if(configPath == null) return null;\r
-            absFile = new File(configPath, file.getPath());\r
-            if( ! absFile.exists() ) return null;\r
-            if( ! absFile.isAbsolute() ) return null;\r
-        }\r
-\r
-        InputStream istream;\r
-        try{\r
-            istream = new FileInputStream(absFile);\r
-        }catch(FileNotFoundException e){\r
-            assert false;\r
-            return null;\r
-        }\r
-        istream = new BufferedInputStream(istream);\r
-\r
-        Reader reader = new InputStreamReader(istream, CHARSET_JSON);\r
-\r
-        JsValue value;\r
-        try{\r
-            value = Json.parseValue(reader);\r
-        }catch(IOException e){\r
-            Jindolf.logger().fatal(\r
-                    "JSONファイル["\r
-                    + absFile.getPath()\r
-                    + "]の読み込み時に支障がありました。", e);\r
-            return null;\r
-        }catch(JsParseException e){\r
-            Jindolf.logger().fatal(\r
-                    "JSONファイル["\r
-                    + absFile.getPath()\r
-                    + "]の内容に不備があります。", e);\r
-            return null;\r
-        }finally{\r
-            try{\r
-                reader.close();\r
-            }catch(IOException e){\r
-                Jindolf.logger().fatal(\r
-                        "JSONファイル["\r
-                        + absFile.getPath()\r
-                        + "]を閉じることができません。", e);\r
-                return null;\r
-            }\r
-        }\r
-\r
-        return value;\r
-    }\r
-\r
-    /**\r
-     * 設定ディレクトリ上のJSONファイルに書き込む。\r
-     * @param file JSONファイルの相対パス\r
-     * @param value JSON objectまたはarray\r
-     * @return 正しくセーブが行われればtrue。\r
-     * 何らかの理由でセーブが完了できなければfalse\r
-     */\r
-    public static boolean saveJson(File file, JsValue value){\r
-        AppSetting setting = Jindolf.getAppSetting();\r
-        if( ! setting.useConfigPath() ) return false;\r
-        File configPath = setting.getConfigPath();\r
-        if(configPath == null) return false;\r
-\r
-        // TODO テンポラリファイルを用いたより安全なファイル更新\r
-        File absFile = new File(configPath, file.getPath());\r
-        absFile.delete();\r
-        try{\r
-            if(absFile.createNewFile() != true) return false;\r
-        }catch(IOException e){\r
-            Jindolf.logger().fatal(\r
-                    "JSONファイル["\r
-                    + absFile.getPath()\r
-                    + "]の新規生成ができません。", e);\r
-            return false;\r
-        }\r
-\r
-        OutputStream ostream;\r
-        try{\r
-            ostream = new FileOutputStream(absFile);\r
-        }catch(FileNotFoundException e){\r
-            assert false;\r
-            return false;\r
-        }\r
-        ostream = new BufferedOutputStream(ostream);\r
-        Writer writer = new OutputStreamWriter(ostream, CHARSET_JSON);\r
-\r
-        try{\r
-            Json.writeJsonTop(writer, value);\r
-        }catch(IOException e){\r
-            Jindolf.logger().fatal(\r
-                    "JSONファイル["\r
-                    + absFile.getPath()\r
-                    + "]の書き込み時に支障がありました。", e);\r
-            return false;\r
-        }finally{\r
-            try{\r
-                writer.close();\r
-            }catch(IOException e){\r
-                Jindolf.logger().fatal(\r
-                        "JSONファイル["\r
-                        + absFile.getPath()\r
-                        + "]を閉じることができません。", e);\r
-                return false;\r
-            }\r
-        }\r
-\r
-        return true;\r
-    }\r
-\r
-    /**\r
-     * ロックエラー用ダイアログ。\r
-     * <ul>\r
-     * <li>強制解除\r
-     * <li>リトライ\r
-     * <li>設定ディレクトリを無視\r
-     * <li>起動中止\r
-     * </ul>\r
-     * の選択を利用者に求める。\r
-     */\r
-    @SuppressWarnings("serial")\r
-    private static class LockErrorPane\r
-            extends JOptionPane\r
-            implements ActionListener{\r
-\r
-        private final InterVMLock lock;\r
-\r
-        private final JRadioButton continueButton =\r
-                new JRadioButton("設定ディレクトリを使わずに起動を続行");\r
-        private final JRadioButton retryButton =\r
-                new JRadioButton("再度ロック取得を試す");\r
-        private final JRadioButton forceButton =\r
-                new JRadioButton(\r
-                "<html>"\r
-                + "ロックを強制解除<br>"\r
-                + " (※他のJindolfと設定ファイル書き込みが衝突するかも…)"\r
-                + "</html>");\r
-\r
-        private final JButton okButton = new JButton("OK");\r
-        private final JButton abortButton = new JButton("起動中止");\r
-\r
-        private boolean aborted = false;\r
-\r
-        /**\r
-         * コンストラクタ。\r
-         * @param lock 失敗したロック\r
-         */\r
-        public LockErrorPane(InterVMLock lock){\r
-            super();\r
-\r
-            this.lock = lock;\r
-\r
-            String htmlMessage =\r
-                    "<html>"\r
-                    + "設定ディレクトリのロックファイル<br>"\r
-                    + getCenteredFileName(this.lock.getLockFile())\r
-                    + "のロックに失敗しました。<br>"\r
-                    + "考えられる原因としては、<br>"\r
-                    + "<ul>"\r
-                    + "<li>前回起動したJindolfの終了が正しく行われなかった"\r
-                    + "<li>今どこかで他のJindolfが動いている"\r
-                    + "</ul>"\r
-                    + "などが考えられます。<br>"\r
-                    + "<hr>"\r
-                    + "</html>";\r
-\r
-            ButtonGroup bgrp = new ButtonGroup();\r
-            bgrp.add(this.continueButton);\r
-            bgrp.add(this.retryButton);\r
-            bgrp.add(this.forceButton);\r
-            this.continueButton.setSelected(true);\r
-\r
-            Object[] msg = {\r
-                htmlMessage,\r
-                this.continueButton,\r
-                this.retryButton,\r
-                this.forceButton,\r
-            };\r
-            setMessage(msg);\r
-\r
-            Object[] opts = {\r
-                this.okButton,\r
-                this.abortButton,\r
-            };\r
-            setOptions(opts);\r
-\r
-            setMessageType(JOptionPane.ERROR_MESSAGE);\r
-\r
-            this.okButton   .addActionListener(this);\r
-            this.abortButton.addActionListener(this);\r
-\r
-            return;\r
-        }\r
-\r
-        /**\r
-         * 「設定ディレクトリを無視して続行」が選択されたか判定する。\r
-         * @return 「無視して続行」が選択されていればtrue\r
-         */\r
-        public boolean isRadioContinue(){\r
-            return this.continueButton.isSelected();\r
-        }\r
-\r
-        /**\r
-         * 「リトライ」が選択されたか判定する。\r
-         * @return 「リトライ」が選択されていればtrue\r
-         */\r
-        public boolean isRadioRetry(){\r
-            return this.retryButton.isSelected();\r
-        }\r
-\r
-        /**\r
-         * 「強制解除」が選択されたか判定する。\r
-         * @return 「強制解除」が選択されていればtrue\r
-         */\r
-        public boolean isRadioForce(){\r
-            return this.forceButton.isSelected();\r
-        }\r
-\r
-        /**\r
-         * 「起動中止」が選択されたか判定する。\r
-         * @return 「起動中止」が押されていたならtrue\r
-         */\r
-        public boolean isAborted(){\r
-            return this.aborted;\r
-        }\r
-\r
-        /**\r
-         * {@inheritDoc}\r
-         * @param parentComponent {@inheritDoc}\r
-         * @param title {@inheritDoc}\r
-         * @return {@inheritDoc}\r
-         * @throws HeadlessException {@inheritDoc}\r
-         */\r
-        @Override\r
-        public JDialog createDialog(Component parentComponent,\r
-                                      String title)\r
-                throws HeadlessException{\r
-            final JDialog dialog =\r
-                    super.createDialog(parentComponent, title);\r
-\r
-            ActionListener listener = new ActionListener(){\r
-                public void actionPerformed(ActionEvent event){\r
-                    dialog.setVisible(false);\r
-                    return;\r
-                }\r
-            };\r
-\r
-            this.okButton   .addActionListener(listener);\r
-            this.abortButton.addActionListener(listener);\r
-\r
-            return dialog;\r
-        }\r
-\r
-        /**\r
-         * ボタン押下を受信する。\r
-         * @param event イベント\r
-         */\r
-        public void actionPerformed(ActionEvent event){\r
-            Object source = event.getSource();\r
-            if(source == this.okButton) this.aborted = false;\r
-            else                        this.aborted = true;\r
-            return;\r
-        }\r
-\r
-    }\r
-\r
-}\r
+/*
+ * configuration file & directory
+ *
+ * License : The MIT License
+ * Copyright(c) 2009 olyutorskii
+ */
+
+package jp.sourceforge.jindolf;
+
+import java.awt.Component;
+import java.awt.HeadlessException;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+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.PrintWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JOptionPane;
+import javax.swing.JRadioButton;
+import jp.sourceforge.jindolf.json.JsParseException;
+import jp.sourceforge.jindolf.json.JsValue;
+import jp.sourceforge.jindolf.json.Json;
+
+/**
+ * Jindolf設定格納ディレクトリに関するあれこれ。
+ */
+public final class ConfigFile{
+
+    private static final String TITLE_BUILDCONF =
+            Jindolf.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 = Charset.forName("UTF-8");
+    private static final Charset CHARSET_JSON = Charset.forName("UTF-8");
+
+    private static final String MSG_POST =
+            "<ul>"
+            + "<li><code>" + CmdOption.OPT_CONFDIR.toHyphened() + "</code>"
+            + "&nbsp;オプション指定により、<br>"
+            + "任意の設定格納ディレクトリを指定することができます。<br>"
+            + "<li><code>" + CmdOption.OPT_NOCONF.toHyphened() + "</code>"
+            + "&nbsp;オプション指定により、<br>"
+            + "設定格納ディレクトリを使わずに起動することができます。<br>"
+            + "</ul>";
+
+
+    /**
+     * 隠れコンストラクタ。
+     */
+    private ConfigFile(){
+        super();
+        return;
+    }
+
+
+    /**
+     * 設定格納ディレクトリのセットアップ。
+     * @return 設定格納ディレクトリ
+     */
+    public static File setupConfigDirectory(){
+        AppSetting setting = Jindolf.getAppSetting();
+        File configPath;
+
+        if( ! setting.useConfigPath() ){
+            configPath = null;
+        }else{
+            String optName;
+            if(setting.getConfigPath() != null){
+                configPath = setting.getConfigPath();
+                optName = CmdOption.OPT_CONFDIR.toHyphened();
+            }else{
+                configPath = ConfigFile.getImplicitConfigDirectory();
+                optName = null;
+            }
+            if( ! configPath.exists() ){
+                configPath =
+                        ConfigFile.buildConfigDirectory(configPath, optName);
+            }
+            ConfigFile.checkAccessibility(configPath);
+        }
+
+        setting.setConfigPath(configPath);
+
+        return configPath;
+    }
+
+    /**
+     * ロックファイルのセットアップ。
+     * @return ロックオブジェクト
+     */
+    public static InterVMLock setupLockFile(){
+        AppSetting setting = Jindolf.getAppSetting();
+
+        File configPath = setting.getConfigPath();
+        if(configPath == null) return null;
+
+        File lockFile = new File(configPath, "lock");
+        InterVMLock lock = new InterVMLock(lockFile);
+
+        lock.tryLock();
+
+        if( ! lock.isFileOwner() ){
+            confirmLockError(lock);
+            if( ! lock.isFileOwner() ){
+                setting.setConfigPath(null);
+                setting.setUseConfigPath(false);
+            }
+        }
+
+        return lock;
+    }
+
+    /**
+     * 暗黙的な設定格納ディレクトリを返す。
+     * 起動元JARファイルと同じディレクトリに、
+     * アクセス可能なディレクトリ"Jindolf"が
+     * すでに存在していればそれを返す。
+     * 起動元JARファイルおよび"Jindolf"が発見できなければ、
+     * MacOSX環境の場合"~/Library/Application Support/Jindolf/"を返す。
+     * Windows環境の場合"%USERPROFILE%\Jindolf\"を返す。
+     * それ以外の環境(Linux,etc?)の場合"~/.jindolf/"を返す。
+     * 返すディレクトリが存在しているか否か、
+     * アクセス可能か否かは呼び出し元で判断せよ。
+     * @return 設定格納ディレクトリ
+     */
+    public static File getImplicitConfigDirectory(){
+        File result;
+
+        File jarParent = FileUtils.getJarDirectory(Jindolf.class);
+        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;
+    }
+
+    /**
+     * まだ存在しない設定格納ディレクトリを新規に作成する。
+     * エラーがあればダイアログ提示とともにVM終了する。
+     * @param confPath 設定格納ディレクトリ
+     * @param optName 設定を指定したオプション名。
+     * 暗黙的に指示されたものならnullを渡すべし。
+     * @return 新規に作成した設定格納ディレクトリ
+     * @throws IllegalArgumentException すでにそのディレクトリは存在する。
+     */
+    public static File buildConfigDirectory(File confPath,
+                                               String optName)
+            throws IllegalArgumentException{
+        if(confPath.exists()) throw new IllegalArgumentException();
+
+        File absPath = FileUtils.supplyFullPath(confPath);
+
+        String preErrMessage =
+                "設定格納ディレクトリ<br>"
+                + getCenteredFileName(absPath)
+                + "の作成に失敗しました。";
+        if(optName != null){
+            preErrMessage =
+                    "<code>" + optName + "</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;
+    }
+
+    /**
+     * 設定ディレクトリ操作の
+     * 共通エラーメッセージ確認ダイアログを表示する。
+     * 閉じるまで待つ。
+     * @param seq メッセージ
+     */
+    private static void showErrorMessage(CharSequence seq){
+        JOptionPane pane =
+                new JOptionPane(seq.toString(),
+                                JOptionPane.ERROR_MESSAGE);
+        showDialog(pane);
+        return;
+    }
+
+    /**
+     * 設定ディレクトリ操作の
+     * 共通エラーメッセージ確認ダイアログを表示する。
+     * 閉じるまで待つ。
+     * @param seq メッセージ
+     */
+    private static void showWarnMessage(CharSequence seq){
+        JOptionPane pane =
+                new JOptionPane(seq.toString(),
+                                JOptionPane.WARNING_MESSAGE);
+        showDialog(pane);
+        return;
+    }
+
+    /**
+     * 設定ディレクトリ操作の
+     * 情報提示メッセージ確認ダイアログを表示する。
+     * 閉じるまで待つ。
+     * @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終了する。
+     * @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>" );
+        Jindolf.exit(1);
+        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>" );
+        Jindolf.exit(1);
+        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>" );
+        Jindolf.exit(1);
+        return;
+    }
+
+    /**
+     * 設定ディレクトリが生成できないエラーをダイアログで提示し、
+     * VM終了する。
+     * @param path 生成できなかったディレクトリ
+     */
+    private static void abortCantBuildConfigDir(File path){
+        showErrorMessage(
+                "<html>"
+                + "設定ディレクトリ<br>"
+                + getCenteredFileName(path)
+                + "の作成に失敗しました。"
+                + "起動を中止します。<br>"
+                + MSG_POST
+                + "</html>" );
+        Jindolf.exit(1);
+        return;
+    }
+
+    /**
+     * 設定ディレクトリへアクセスできないエラーをダイアログで提示し、
+     * VM終了する。
+     * @param path アクセスできないディレクトリ
+     */
+    private static void abortCantAccessConfigDir(File path){
+        showErrorMessage(
+                "<html>"
+                + "設定ディレクトリ<br>"
+                + getCenteredFileName(path)
+                + "へのアクセスができません。"
+                + "起動を中止します。<br>"
+                + "このディレクトリへのアクセス権を調整し"
+                + "読み書きできるようにしてください。<br>"
+                + MSG_POST
+                + "</html>" );
+        Jindolf.exit(1);
+        return;
+    }
+
+    /**
+     * ファイルに書き込めないエラーをダイアログで提示し、VM終了する。
+     * @param file 書き込めなかったファイル
+     */
+    private static void abortCantWrite(File file){
+        showErrorMessage(
+                "<html>"
+                + "ファイル<br>"
+                + getCenteredFileName(file)
+                + "への書き込みができません。"
+                + "起動を中止します。<br>"
+                + "</html>" );
+        Jindolf.exit(1);
+        return;
+    }
+
+    /**
+     * 指定されたディレクトリにREADMEファイルを生成する。
+     * 生成できなければダイアログ表示とともに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.sourceforge.jp/"
+                    + " を参照してください。");
+            writer.println(
+                    "このディレクトリを"
+                    + "「" + JINCONF + "」"
+                    + "の名前で起動元JARファイルと"
+                    + "同じ位置に");
+            writer.println(
+                    "コピーすれば、そちらの設定が優先して使われます。");
+            writer.println(
+                    "「lock」の名前を持つファイルはロックファイルです。");
+        }catch(IOException e){
+            abortCantWrite(file);
+        }catch(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>";
+    }
+
+    /**
+     * ロックエラーダイアログの表示。
+     * 呼び出しから戻ってもまだロックオブジェクトが
+     * ロックファイルのオーナーでない場合、
+     * 今後設定ディレクトリは一切使わずに起動を続行するものとする。
+     * ロックファイルの強制解除に失敗した場合は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){
+                Jindolf.exit(1);
+                break;
+            }else if(pane.isRadioRetry()){
+                lock.tryLock();
+                if(lock.isFileOwner()) break;
+            }else if(pane.isRadioContinue()){
+                showInfoMessage(
+                        "<html>"
+                        + "設定ディレクトリを使わずに起動を続行します。<br>"
+                        + "今回、各種設定の読み込み・保存はできません。<br>"
+                        + "<code>"
+                        + CmdOption.OPT_NOCONF.toHyphened()
+                        + "</code> オプション"
+                        + "を使うとこの警告は出なくなります。"
+                        + "</html>");
+                break;
+            }else if(pane.isRadioForce()){
+                lock.forceRemove();
+                if(lock.isExistsFile()){
+                    showErrorMessage(
+                            "<html>"
+                            + "ロックファイルの強制解除に失敗しました。<br>"
+                            + "他に動いているJindolf"
+                            + "が見つからないのであれば、<br>"
+                            + "なんとかしてロックファイル<br>"
+                            + getCenteredFileName(lock.getLockFile())
+                            + "を削除してください。<br>"
+                            + "起動を中止します。"
+                            + "</html>");
+                    Jindolf.exit(1);
+                    break;
+                }
+                lock.tryLock();
+                if(lock.isFileOwner()) break;
+                showErrorMessage(
+                        "<html>"
+                        + "ロックファイル<br>"
+                        + getCenteredFileName(lock.getLockFile())
+                        + "を確保することができません。<br>"
+                        + "起動を中止します。"
+                        + "</html>");
+                Jindolf.exit(1);
+                break;
+            }
+        }
+
+        return;
+    }
+
+    /**
+     * 設定ディレクトリ上のJSONファイルを読み込む。
+     * @param file JSONファイルの相対パス
+     * @return JSON objectまたはarray。
+     * 設定ディレクトリを使わない設定、
+     * もしくはJSONファイルが存在しない、
+     * もしくは入力エラーがあればnull
+     */
+    public static JsValue loadJson(File file){
+        AppSetting setting = Jindolf.getAppSetting();
+        if( ! setting.useConfigPath() ) return null;
+
+        File absFile;
+        if(file.isAbsolute()){
+            absFile = file;
+        }else{
+            File configPath = setting.getConfigPath();
+            if(configPath == null) return null;
+            absFile = new File(configPath, file.getPath());
+            if( ! absFile.exists() ) return null;
+            if( ! absFile.isAbsolute() ) return null;
+        }
+
+        InputStream istream;
+        try{
+            istream = new FileInputStream(absFile);
+        }catch(FileNotFoundException e){
+            assert false;
+            return null;
+        }
+        istream = new BufferedInputStream(istream);
+
+        Reader reader = new InputStreamReader(istream, CHARSET_JSON);
+
+        JsValue value;
+        try{
+            value = Json.parseValue(reader);
+        }catch(IOException e){
+            Jindolf.logger().fatal(
+                    "JSONファイル["
+                    + absFile.getPath()
+                    + "]の読み込み時に支障がありました。", e);
+            return null;
+        }catch(JsParseException e){
+            Jindolf.logger().fatal(
+                    "JSONファイル["
+                    + absFile.getPath()
+                    + "]の内容に不備があります。", e);
+            return null;
+        }finally{
+            try{
+                reader.close();
+            }catch(IOException e){
+                Jindolf.logger().fatal(
+                        "JSONファイル["
+                        + absFile.getPath()
+                        + "]を閉じることができません。", e);
+                return null;
+            }
+        }
+
+        return value;
+    }
+
+    /**
+     * 設定ディレクトリ上のJSONファイルに書き込む。
+     * @param file JSONファイルの相対パス
+     * @param value JSON objectまたはarray
+     * @return 正しくセーブが行われればtrue。
+     * 何らかの理由でセーブが完了できなければfalse
+     */
+    public static boolean saveJson(File file, JsValue value){
+        AppSetting setting = Jindolf.getAppSetting();
+        if( ! setting.useConfigPath() ) return false;
+        File configPath = setting.getConfigPath();
+        if(configPath == null) return false;
+
+        // TODO テンポラリファイルを用いたより安全なファイル更新
+        File absFile = new File(configPath, file.getPath());
+        absFile.delete();
+        try{
+            if(absFile.createNewFile() != true) return false;
+        }catch(IOException e){
+            Jindolf.logger().fatal(
+                    "JSONファイル["
+                    + absFile.getPath()
+                    + "]の新規生成ができません。", e);
+            return false;
+        }
+
+        OutputStream ostream;
+        try{
+            ostream = new FileOutputStream(absFile);
+        }catch(FileNotFoundException e){
+            assert false;
+            return false;
+        }
+        ostream = new BufferedOutputStream(ostream);
+        Writer writer = new OutputStreamWriter(ostream, CHARSET_JSON);
+
+        try{
+            Json.writeJsonTop(writer, value);
+        }catch(IOException e){
+            Jindolf.logger().fatal(
+                    "JSONファイル["
+                    + absFile.getPath()
+                    + "]の書き込み時に支障がありました。", e);
+            return false;
+        }finally{
+            try{
+                writer.close();
+            }catch(IOException e){
+                Jindolf.logger().fatal(
+                        "JSONファイル["
+                        + absFile.getPath()
+                        + "]を閉じることができません。", e);
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * ロックエラー用ダイアログ。
+     * <ul>
+     * <li>強制解除
+     * <li>リトライ
+     * <li>設定ディレクトリを無視
+     * <li>起動中止
+     * </ul>
+     * の選択を利用者に求める。
+     */
+    @SuppressWarnings("serial")
+    private static class LockErrorPane
+            extends JOptionPane
+            implements ActionListener{
+
+        private final InterVMLock lock;
+
+        private final JRadioButton continueButton =
+                new JRadioButton("設定ディレクトリを使わずに起動を続行");
+        private final JRadioButton retryButton =
+                new JRadioButton("再度ロック取得を試す");
+        private final JRadioButton forceButton =
+                new JRadioButton(
+                "<html>"
+                + "ロックを強制解除<br>"
+                + " (※他のJindolfと設定ファイル書き込みが衝突するかも…)"
+                + "</html>");
+
+        private final JButton okButton = new JButton("OK");
+        private final JButton abortButton = new JButton("起動中止");
+
+        private boolean aborted = false;
+
+        /**
+         * コンストラクタ。
+         * @param lock 失敗したロック
+         */
+        public LockErrorPane(InterVMLock lock){
+            super();
+
+            this.lock = lock;
+
+            String htmlMessage =
+                    "<html>"
+                    + "設定ディレクトリのロックファイル<br>"
+                    + getCenteredFileName(this.lock.getLockFile())
+                    + "のロックに失敗しました。<br>"
+                    + "考えられる原因としては、<br>"
+                    + "<ul>"
+                    + "<li>前回起動したJindolfの終了が正しく行われなかった"
+                    + "<li>今どこかで他のJindolfが動いている"
+                    + "</ul>"
+                    + "などが考えられます。<br>"
+                    + "<hr>"
+                    + "</html>";
+
+            ButtonGroup bgrp = new ButtonGroup();
+            bgrp.add(this.continueButton);
+            bgrp.add(this.retryButton);
+            bgrp.add(this.forceButton);
+            this.continueButton.setSelected(true);
+
+            Object[] msg = {
+                htmlMessage,
+                this.continueButton,
+                this.retryButton,
+                this.forceButton,
+            };
+            setMessage(msg);
+
+            Object[] opts = {
+                this.okButton,
+                this.abortButton,
+            };
+            setOptions(opts);
+
+            setMessageType(JOptionPane.ERROR_MESSAGE);
+
+            this.okButton   .addActionListener(this);
+            this.abortButton.addActionListener(this);
+
+            return;
+        }
+
+        /**
+         * 「設定ディレクトリを無視して続行」が選択されたか判定する。
+         * @return 「無視して続行」が選択されていればtrue
+         */
+        public boolean isRadioContinue(){
+            return this.continueButton.isSelected();
+        }
+
+        /**
+         * 「リトライ」が選択されたか判定する。
+         * @return 「リトライ」が選択されていればtrue
+         */
+        public boolean isRadioRetry(){
+            return this.retryButton.isSelected();
+        }
+
+        /**
+         * 「強制解除」が選択されたか判定する。
+         * @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(){
+                public void actionPerformed(ActionEvent event){
+                    dialog.setVisible(false);
+                    return;
+                }
+            };
+
+            this.okButton   .addActionListener(listener);
+            this.abortButton.addActionListener(listener);
+
+            return dialog;
+        }
+
+        /**
+         * ボタン押下を受信する。
+         * @param event イベント
+         */
+        public void actionPerformed(ActionEvent event){
+            Object source = event.getSource();
+            if(source == this.okButton) this.aborted = false;
+            else                        this.aborted = true;
+            return;
+        }
+
+    }
+
+}