OSDN Git Service

rename ConfigFile to ConfigDirUtils
[jindolf/Jindolf.git] / src / main / java / jp / sfjp / jindolf / config / ConfigDirUtils.java
1 /*
2  * configuration file & directory
3  *
4  * License : The MIT License
5  * Copyright(c) 2009 olyutorskii
6  */
7
8 package jp.sfjp.jindolf.config;
9
10 import java.io.File;
11 import java.io.FileOutputStream;
12 import java.io.IOException;
13 import java.io.InputStream;
14 import java.io.OutputStream;
15 import java.io.OutputStreamWriter;
16 import java.io.PrintWriter;
17 import java.io.Writer;
18 import java.nio.charset.Charset;
19 import java.nio.charset.StandardCharsets;
20 import java.nio.file.Files;
21 import java.nio.file.Path;
22 import java.nio.file.Paths;
23 import javax.swing.JDialog;
24 import javax.swing.JOptionPane;
25 import jp.sfjp.jindolf.ResourceManager;
26 import jp.sfjp.jindolf.VerInfo;
27 import jp.sfjp.jindolf.view.LockErrorPane;
28
29 /**
30  * Jindolf設定格納ディレクトリに関するあれこれ。
31  */
32 public final class ConfigDirUtils{
33
34     private static final String TITLE_BUILDCONF =
35             VerInfo.TITLE + "設定格納ディレクトリの設定";
36
37     private static final String JINCONF     = "Jindolf";
38     private static final String JINCONF_DOT = ".jindolf";
39     private static final String FILE_README = "README.txt";
40     private static final Charset CHARSET_README = StandardCharsets.UTF_8;
41
42     private static final String MSG_POST =
43             "<ul>"
44             + "<li><code>" + CmdOption.OPT_CONFDIR + "</code>"
45             + "&nbsp;オプション指定により、<br/>"
46             + "任意の設定格納ディレクトリを指定することができます。<br/>"
47             + "<li><code>" + CmdOption.OPT_NOCONF + "</code>"
48             + "&nbsp;オプション指定により、<br/>"
49             + "設定格納ディレクトリを使わずに起動することができます。<br/>"
50             + "</ul>";
51
52
53     /**
54      * 隠れコンストラクタ。
55      */
56     private ConfigDirUtils(){
57         assert false;
58         return;
59     }
60
61
62     /**
63      * 暗黙的な設定格納ディレクトリを返す。
64      *
65      * <ul>
66      *
67      * <li>起動元JARファイルと同じディレクトリに、
68      * アクセス可能なディレクトリ"Jindolf"が
69      * すでに存在していればそれを返す。
70      *
71      * <li>起動元JARファイルおよび"Jindolf"が発見できなければ、
72      * MacOSX環境の場合"~/Library/Application Support/Jindolf/"を返す。
73      * Windows環境の場合"%USERPROFILE%\Jindolf\"を返す。
74      *
75      * <li>それ以外の環境(Linux,etc?)の場合"~/.jindolf/"を返す。
76      *
77      * </ul>
78      *
79      * <p>返すディレクトリが存在しているか否か、
80      * アクセス可能か否かは呼び出し元で判断せよ。
81      *
82      * @return 設定格納ディレクトリ
83      */
84     public static File getImplicitConfigDirectory(){
85         File result;
86
87         File jarParent = FileUtils.getJarDirectory();
88         if(jarParent != null && FileUtils.isAccessibleDirectory(jarParent)){
89             result = new File(jarParent, JINCONF);
90             if(FileUtils.isAccessibleDirectory(result)){
91                 return result;
92             }
93         }
94
95         File appset = FileUtils.getAppSetDir();
96         if(appset == null) return null;
97
98         if(FileUtils.isMacOSXFs() || FileUtils.isWindowsOSFs()){
99             result = new File(appset, JINCONF);
100         }else{
101             result = new File(appset, JINCONF_DOT);
102         }
103
104         return result;
105     }
106
107     /**
108      * まだ存在しない設定格納ディレクトリを新規に作成する。
109      *
110      * <p>エラーがあればダイアログ提示とともにVM終了する。
111      *
112      * @param confPath 設定格納ディレクトリ
113      * @param isImplicitPath ディレクトリが暗黙的に指定されたものならtrue。
114      * @return 新規に作成した設定格納ディレクトリ
115      * @throws IllegalArgumentException すでにそのディレクトリは存在する。
116      */
117     public static File buildConfigDirectory(File confPath,
118                                             boolean isImplicitPath )
119             throws IllegalArgumentException{
120         if(confPath.exists()) throw new IllegalArgumentException();
121
122         File absPath = FileUtils.supplyFullPath(confPath);
123
124         String preErrMessage =
125                 "設定格納ディレクトリ<br/>"
126                 + getCenteredFileName(absPath)
127                 + "の作成に失敗しました。";
128         if( ! isImplicitPath ){
129             preErrMessage =
130                     "<code>"
131                     + CmdOption.OPT_CONFDIR
132                     + "</code>&nbsp;オプション"
133                     + "で指定された、<br/>"
134                     + preErrMessage;
135         }
136
137         File existsAncestor = FileUtils.findExistsAncestor(absPath);
138         if(existsAncestor == null){
139             abortNoRoot(absPath, preErrMessage);
140         }else if( ! existsAncestor.canWrite() ){
141             abortCantWriteAncestor(existsAncestor, preErrMessage);
142         }
143
144         String prompt =
145                 "設定ファイル格納ディレクトリ<br/>"
146                 + getCenteredFileName(absPath)
147                 + "を作成します。";
148         boolean confirmed = confirmBuildConfigDir(existsAncestor, prompt);
149         if( ! confirmed ){
150             abortQuitBuildConfigDir();
151         }
152
153         boolean success;
154         try{
155             success = absPath.mkdirs();
156         }catch(SecurityException e){
157             success = false;
158         }
159
160         if( ! success || ! absPath.exists() ){
161             abortCantBuildConfigDir(absPath);
162         }
163
164         FileUtils.setOwnerOnlyAccess(absPath);
165
166         checkAccessibility(absPath);
167
168         touchReadme(absPath);
169
170         return absPath;
171     }
172
173     /**
174      * ローカル画像キャッシュディレクトリを作る。
175      *
176      * <p>作られたディレクトリ内に
177      * ファイルavatarCache.jsonが作られる。
178      *
179      * @param imgCacheDir ローカル画像キャッシュディレクトリ
180      */
181     public static void buildImageCacheDir(File imgCacheDir){
182         if(imgCacheDir.exists()) return;
183
184         String jsonRes = "resources/image/avatarCache.json";
185         InputStream is = ResourceManager.getResourceAsStream(jsonRes);
186         if(is == null) return;
187
188         imgCacheDir.mkdirs();
189         ConfigDirUtils.checkAccessibility(imgCacheDir);
190
191         Path cachePath = imgCacheDir.toPath();
192         Path jsonLeaf = Paths.get("avatarCache.json");
193         Path path = cachePath.resolve(jsonLeaf);
194         try{
195             Files.copy(is, path);
196         }catch(IOException e){
197             abortCantAccessConfigDir(path.toFile());
198         }
199
200         return;
201     }
202
203     /**
204      * 設定ディレクトリ操作の
205      * 共通エラーメッセージ確認ダイアログを表示する。
206      *
207      * <p>閉じるまで待つ。
208      *
209      * @param seq メッセージ
210      */
211     private static void showErrorMessage(CharSequence seq){
212         JOptionPane pane =
213                 new JOptionPane(seq.toString(),
214                                 JOptionPane.ERROR_MESSAGE);
215         showDialog(pane);
216         return;
217     }
218
219     /**
220      * 設定ディレクトリ操作の
221      * 共通エラーメッセージ確認ダイアログを表示する。
222      *
223      * <p>閉じるまで待つ。
224      *
225      * @param seq メッセージ
226      */
227     private static void showWarnMessage(CharSequence seq){
228         JOptionPane pane =
229                 new JOptionPane(seq.toString(),
230                                 JOptionPane.WARNING_MESSAGE);
231         showDialog(pane);
232         return;
233     }
234
235     /**
236      * 設定ディレクトリ操作の
237      * 情報提示メッセージ確認ダイアログを表示する。
238      *
239      * <p>閉じるまで待つ。
240      *
241      * @param seq メッセージ
242      */
243     private static void showInfoMessage(CharSequence seq){
244         JOptionPane pane =
245                 new JOptionPane(seq.toString(),
246                                 JOptionPane.INFORMATION_MESSAGE);
247         showDialog(pane);
248         return;
249     }
250
251     /**
252      * ダイアログを表示し、閉じられるまで待つ。
253      *
254      * @param pane ダイアログの元となるペイン
255      */
256     private static void showDialog(JOptionPane pane){
257         JDialog dialog = pane.createDialog(TITLE_BUILDCONF);
258         dialog.setResizable(true);
259         dialog.pack();
260
261         dialog.setVisible(true);
262         dialog.dispose();
263
264         return;
265     }
266
267     /**
268      * VMを異常終了させる。
269      */
270     private static void abort(){
271         System.exit(1);
272         assert false;
273         return;
274     }
275
276     /**
277      * 設定ディレクトリのルートファイルシステムもしくはドライブレターに
278      * アクセスできないエラーをダイアログに提示し、VM終了する。
279      *
280      * @param path 設定ディレクトリ
281      * @param preMessage メッセージ前半
282      */
283     private static void abortNoRoot(File path, String preMessage){
284         File root = FileUtils.findRootFile(path);
285         showErrorMessage(
286                 "<html>"
287                 + preMessage + "<br/>"
288                 + getCenteredFileName(root)
289                 + "を用意する方法が不明です。<br/>"
290                 + "起動を中止します。<br/>"
291                 + MSG_POST
292                 + "</html>" );
293         abort();
294         return;
295     }
296
297     /**
298      * 設定ディレクトリの祖先に書き込めないエラーをダイアログで提示し、
299      * VM終了する。
300      *
301      * @param existsAncestor 存在するもっとも近い祖先
302      * @param preMessage メッセージ前半
303      */
304     private static void abortCantWriteAncestor(File existsAncestor,
305                                                   String preMessage ){
306         showErrorMessage(
307                 "<html>"
308                 + preMessage + "<br/>"
309                 + getCenteredFileName(existsAncestor)
310                 + "への書き込みができないため、"
311                 + "処理の続行は不可能です。<br/>"
312                 + "起動を中止します。<br/>"
313                 + MSG_POST
314                 + "</html>" );
315         abort();
316         return;
317     }
318
319     /**
320      * 設定ディレクトリを新規に生成してよいかダイアログで問い合わせる。
321      *
322      * @param existsAncestor 存在するもっとも近い祖先
323      * @param preMessage メッセージ前半
324      * @return 生成してよいと指示があればtrue
325      */
326     private static boolean confirmBuildConfigDir(File existsAncestor,
327                                                     String preMessage){
328         String message =
329                 "<html>"
330                 + preMessage + "<br/>"
331                 + "このディレクトリを今から<br/>"
332                 + getCenteredFileName(existsAncestor)
333                 + "に作成して構いませんか?<br/>"
334                 + "このディレクトリ名は、後からいつでもヘルプウィンドウで<br/>"
335                 + "確認することができます。"
336                 + "</html>";
337
338         JOptionPane pane =
339                 new JOptionPane(message,
340                                 JOptionPane.QUESTION_MESSAGE,
341                                 JOptionPane.YES_NO_OPTION);
342
343         showDialog(pane);
344
345         Object result = pane.getValue();
346         if(result == null) return false;
347         else if( ! (result instanceof Integer) ) return false;
348
349         int ival = (Integer) result;
350         if(ival == JOptionPane.YES_OPTION) return true;
351
352         return false;
353     }
354
355     /**
356      * 設定ディレクトリ生成をやめた操作への警告をダイアログで提示し、
357      * VM終了する。
358      */
359     private static void abortQuitBuildConfigDir(){
360         showWarnMessage(
361                 "<html>"
362                 + "設定ディレクトリの作成をせずに起動を中止します。<br/>"
363                 + MSG_POST
364                 + "</html>" );
365         abort();
366         return;
367     }
368
369     /**
370      * 設定ディレクトリが生成できないエラーをダイアログで提示し、
371      * VM終了する。
372      *
373      * @param path 生成できなかったディレクトリ
374      */
375     private static void abortCantBuildConfigDir(File path){
376         showErrorMessage(
377                 "<html>"
378                 + "設定ディレクトリ<br/>"
379                 + getCenteredFileName(path)
380                 + "の作成に失敗しました。"
381                 + "起動を中止します。<br/>"
382                 + MSG_POST
383                 + "</html>" );
384         abort();
385         return;
386     }
387
388     /**
389      * 設定ディレクトリへアクセスできないエラーをダイアログで提示し、
390      * VM終了する。
391      *
392      * @param path アクセスできないディレクトリ
393      */
394     private static void abortCantAccessConfigDir(File path){
395         showErrorMessage(
396                 "<html>"
397                 + "設定ディレクトリ<br/>"
398                 + getCenteredFileName(path)
399                 + "へのアクセスができません。"
400                 + "起動を中止します。<br/>"
401                 + "このディレクトリへのアクセス権を調整し"
402                 + "読み書きできるようにしてください。<br/>"
403                 + MSG_POST
404                 + "</html>" );
405         abort();
406         return;
407     }
408
409     /**
410      * ファイルに書き込めないエラーをダイアログで提示し、VM終了する。
411      *
412      * @param file 書き込めなかったファイル
413      */
414     private static void abortCantWrite(File file){
415         showErrorMessage(
416                 "<html>"
417                 + "ファイル<br/>"
418                 + getCenteredFileName(file)
419                 + "への書き込みができません。"
420                 + "起動を中止します。<br/>"
421                 + "</html>" );
422         abort();
423         return;
424     }
425
426     /**
427      * 指定されたディレクトリにREADMEファイルを生成する。
428      *
429      * <p>生成できなければダイアログ表示とともにVM終了する。
430      *
431      * @param path READMEの格納ディレクトリ
432      */
433     private static void touchReadme(File path){
434         File file = new File(path, FILE_README);
435
436         try{
437             file.createNewFile();
438         }catch(IOException e){
439             abortCantAccessConfigDir(path);
440         }
441
442         PrintWriter writer = null;
443         try{
444             OutputStream ostream = new FileOutputStream(file);
445             Writer owriter = new OutputStreamWriter(ostream, CHARSET_README);
446             writer = new PrintWriter(owriter);
447             writer.println(CHARSET_README.name() + " Japanese");
448             writer.println(
449                     "このディレクトリは、"
450                     + "Jindolfの各種設定が格納されるディレクトリです。");
451             writer.println(
452                     "Jindolfの詳細は "
453                     + "http://jindolf.osdn.jp/"
454                     + " を参照してください。");
455             writer.println(
456                     "このディレクトリを"
457                     + "「" + JINCONF + "」"
458                     + "の名前で起動元JARファイルと"
459                     + "同じ位置に");
460             writer.println(
461                     "コピーすれば、そちらの設定が優先して使われます。");
462             writer.println(
463                     "「lock」の名前を持つファイルはロックファイルです。");
464         }catch(IOException | SecurityException e){
465             abortCantWrite(file);
466         }finally{
467             if(writer != null){
468                 writer.close();
469             }
470         }
471
472         return;
473     }
474
475     /**
476      * 設定ディレクトリがアクセス可能でなければ
477      * エラーダイアログを出してVM終了する。
478      *
479      * @param confDir 設定ディレクトリ
480      */
481     public static void checkAccessibility(File confDir){
482         if( ! FileUtils.isAccessibleDirectory(confDir) ){
483             abortCantAccessConfigDir(confDir);
484         }
485
486         return;
487     }
488
489     /**
490      * センタリングされたファイル名表示のHTML表記を出力する。
491      *
492      * @param path ファイル
493      * @return HTML表記
494      */
495     public static String getCenteredFileName(File path){
496         return "<center>[&nbsp;"
497                 + FileUtils.getHtmledFileName(path)
498                 + "&nbsp;]</center>"
499                 + "<br/>";
500     }
501
502     /**
503      * ロックエラーダイアログの表示。
504      *
505      * <p>呼び出しから戻ってもまだロックオブジェクトが
506      * ロックファイルのオーナーでない場合、
507      * 今後設定ディレクトリは一切使わずに起動を続行するものとする。
508      *
509      * <p>ロックファイルの強制解除に失敗した場合はVM終了する。
510      *
511      * @param lock エラーを起こしたロック
512      */
513     public static void confirmLockError(InterVMLock lock){
514         File lockFile = lock.getLockFile();
515         LockErrorPane lockPane = new LockErrorPane(lockFile);
516         JDialog lockDialog = lockPane.createDialog(TITLE_BUILDCONF);
517         lockDialog.setResizable(true);
518         lockDialog.pack();
519
520         for(;;){
521             lockDialog.setVisible(true);
522
523             Object result = lockPane.getValue();
524             boolean aborted = LockErrorPane.isAborted(result);
525             boolean windowClosed = result == null;
526
527             if(aborted || windowClosed){
528                 abort();
529                 break;
530             }else if(lockPane.isRadioRetry()){
531                 lock.tryLock();
532                 if(lock.isFileOwner()) break;
533             }else if(lockPane.isRadioContinue()){
534                 showInfoMessage(
535                         "<html>"
536                         + "設定ディレクトリを使わずに起動を続行します。<br/>"
537                         + "今回、各種設定の読み込み・保存はできません。<br/>"
538                         + "<code>"
539                         + CmdOption.OPT_NOCONF
540                         + "</code> オプション"
541                         + "を使うとこの警告は出なくなります。"
542                         + "</html>");
543                 break;
544             }else if(lockPane.isRadioForce()){
545                 lock.forceRemove();
546                 if(lock.isExistsFile()){
547                     showErrorMessage(
548                             "<html>"
549                             + "ロックファイルの強制解除に失敗しました。<br/>"
550                             + "他に動いているJindolf"
551                             + "が見つからないのであれば、<br/>"
552                             + "なんとかしてロックファイル<br/>"
553                             + getCenteredFileName(lock.getLockFile())
554                             + "を削除してください。<br/>"
555                             + "起動を中止します。"
556                             + "</html>");
557                     abort();
558                     break;
559                 }
560                 lock.tryLock();
561                 if(lock.isFileOwner()) break;
562                 showErrorMessage(
563                         "<html>"
564                         + "ロックファイル<br/>"
565                         + getCenteredFileName(lock.getLockFile())
566                         + "を確保することができません。<br/>"
567                         + "起動を中止します。"
568                         + "</html>");
569                 abort();
570                 break;
571             }
572         }
573
574         lockDialog.dispose();
575
576         return;
577     }
578
579 }