OSDN Git Service

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