OSDN Git Service

リポジトリ内改行コードのLFへの修正
[charactermanaj/CharacterManaJ.git] / src / main / java / charactermanaj / model / io / CharacterDataPersistent.java
1 package charactermanaj.model.io;
2
3 import java.awt.Color;
4 import java.awt.image.BufferedImage;
5 import java.io.ByteArrayOutputStream;
6 import java.io.File;
7 import java.io.FileFilter;
8 import java.io.FileInputStream;
9 import java.io.FileOutputStream;
10 import java.io.IOException;
11 import java.io.InputStream;
12 import java.io.OutputStream;
13 import java.net.URI;
14 import java.net.URL;
15 import java.text.SimpleDateFormat;
16 import java.util.ArrayList;
17 import java.util.Collections;
18 import java.util.Date;
19 import java.util.Iterator;
20 import java.util.List;
21 import java.util.NoSuchElementException;
22 import java.util.concurrent.ExecutionException;
23 import java.util.concurrent.ExecutorService;
24 import java.util.concurrent.Executors;
25 import java.util.concurrent.Future;
26 import java.util.concurrent.TimeUnit;
27 import java.util.concurrent.TimeoutException;
28 import java.util.concurrent.atomic.AtomicBoolean;
29 import java.util.logging.Level;
30 import java.util.logging.Logger;
31
32 import org.w3c.dom.Node;
33 import org.w3c.dom.NodeList;
34
35 import charactermanaj.graphics.io.FileImageResource;
36 import charactermanaj.graphics.io.ImageLoader;
37 import charactermanaj.graphics.io.ImageSaveHelper;
38 import charactermanaj.graphics.io.LoadedImage;
39 import charactermanaj.model.AppConfig;
40 import charactermanaj.model.CharacterData;
41 import charactermanaj.model.Layer;
42 import charactermanaj.model.PartsCategory;
43 import charactermanaj.model.io.CharacterDataDefaultProvider.DefaultCharacterDataVersion;
44 import charactermanaj.util.DirectoryConfig;
45 import charactermanaj.util.FileNameNormalizer;
46 import charactermanaj.util.FileUserData;
47 import charactermanaj.util.UserData;
48
49 public class CharacterDataPersistent {
50
51         /**
52          * キャラクター定義ファイル名
53          */
54         public static final String CONFIG_FILE = "character.xml";
55
56         /**
57          * キャラクターなんとか機用のiniファイル名
58          */
59         public static final String COMPATIBLE_CONFIG_NAME = "character.ini";
60
61         /**
62          * お気に入りのファイル名
63          */
64         private static final String FAVORITES_FILE_NAME = "favorites.xml";
65
66         /**
67          * サンプルイメージファイル名
68          */
69         private static final String SAMPLE_IMAGE_FILENAME = "preview.png";
70
71         /**
72          * ロガー
73          */
74         private static final Logger logger = Logger
75                         .getLogger(CharacterDataPersistent.class.getName());
76
77         /**
78          * キャラクターデータを格納したXMLのリーダー
79          */
80         private final CharacterDataXMLReader characterDataXmlReader = new CharacterDataXMLReader();
81
82         /**
83          * キャラクターデータを格納したXMLのライタ
84          */
85         private final CharacterDataXMLWriter characterDataXmlWriter = new CharacterDataXMLWriter();
86
87         /**
88          * プロファイルの列挙時のエラーハンドラ.<br>
89          *
90          * @author seraphy
91          */
92         public interface ProfileListErrorHandler {
93
94                 /**
95                  * エラーが発生したことを通知される
96                  *
97                  * @param baseDir
98                  *            読み込み対象のXMLのファイル
99                  * @param ex
100                  *            例外
101                  */
102                 void occureException(File baseDir, Throwable ex);
103         }
104
105         /**
106          * プライベートコンストラクタ.<br>
107          * シングルトン実装であるため、一度だけ呼び出される.
108          */
109         private CharacterDataPersistent() {
110                 super();
111         }
112
113         /**
114          * シングルトン
115          */
116         private static final CharacterDataPersistent singleton = new CharacterDataPersistent();
117
118         /**
119          * インスタンスを取得する
120          *
121          * @return インスタンス
122          */
123         public static CharacterDataPersistent getInstance() {
124                 return singleton;
125         }
126
127         /**
128          * キャラクターデータを新規に保存する.<br>
129          * REVがnullである場合は保存に先立ってランダムにREVが設定される.<br>
130          * 保存先ディレクトリはユーザー固有のキャラクターデータ保存先のディレクトリにキャラクター定義のIDを基本とする ディレクトリを作成して保存される.<br>
131          * ただし、そのディレクトリがすでに存在する場合はランダムな名前で決定される.<br>
132          * 実際のxmlの保存先にあわせてDocBaseが設定されて返される.<br>
133          *
134          * @param characterData
135          *            キャラクターデータ (IDは設定済みであること.それ以外はvalid, editableであること。)
136          * @throws IOException
137          *             失敗
138          */
139         public void createProfile(CharacterData characterData) throws IOException {
140                 if (characterData == null) {
141                         throw new IllegalArgumentException();
142                 }
143
144                 String id = characterData.getId();
145                 if (id == null || id.trim().length() == 0) {
146                         throw new IOException("missing character-id:" + characterData);
147                 }
148
149                 // ユーザー個別のキャラクターデータ保存先ディレクトリを取得
150                 DirectoryConfig dirConfig = DirectoryConfig.getInstance();
151                 File charactersDir = dirConfig.getCharactersDir();
152                 if (!charactersDir.exists()) {
153                         if (!charactersDir.mkdirs()) {
154                                 throw new IOException("can't create the characters directory. "
155                                                 + charactersDir);
156                         }
157                 }
158                 if (logger.isLoggable(Level.FINE)) {
159                         logger.log(Level.FINE, "check characters-dir: " + charactersDir
160                                         + ": exists=" + charactersDir.exists());
161                 }
162
163                 // 新規に保存先ディレクトリを作成.
164                 // 同じ名前のディレクトリがある場合は日付+連番をつけて衝突を回避する
165                 File baseDir = null;
166                 String suffix = "";
167                 String name = characterData.getName();
168                 if (name == null) {
169                         // 表示名が定義されていなければIDで代用する.(IDは必須)
170                         name = characterData.getId();
171                 }
172                 for (int retry = 0;; retry++) {
173                         baseDir = new File(charactersDir, name + suffix);
174                         if (!baseDir.exists()) {
175                                 break;
176                         }
177                         if (retry > 100) {
178                                 throw new IOException("character directory conflict.:"
179                                                 + baseDir);
180                         }
181                         // 衝突回避の末尾文字を設定
182                         suffix = generateSuffix(retry);
183                 }
184                 if (!baseDir.exists()) {
185                         if (!baseDir.mkdirs()) {
186                                 throw new IOException("can't create directory. " + baseDir);
187                         }
188                         logger.log(Level.INFO, "create character-dir: " + baseDir);
189                 }
190
191                 // 保存先を確認
192                 File characterPropXML = new File(baseDir, CONFIG_FILE);
193                 if (characterPropXML.exists() && !characterPropXML.isFile()) {
194                         throw new IOException(CONFIG_FILE + " is not a regular file.:"
195                                         + characterPropXML);
196                 }
197                 if (characterPropXML.exists() && !characterPropXML.canWrite()) {
198                         throw new IOException("character.xml is not writable.:"
199                                         + characterPropXML);
200                 }
201
202                 // DocBaseを実際の保存先に更新
203                 URI docBase = characterPropXML.toURI();
204                 characterData.setDocBase(docBase);
205
206                 // リビジョンが指定されてなければ新規にリビジョンを割り当てる。
207                 if (characterData.getRev() == null) {
208                         characterData.setRev(generateRev());
209                 }
210
211                 // 保存する.
212                 saveCharacterDataToXML(characterData);
213
214                 // ディレクトリを準備する
215                 preparePartsDir(characterData);
216         }
217
218         /**
219          * リビジョンを生成して返す.
220          *
221          * @return リビジョン用文字列
222          */
223         public String generateRev() {
224                 SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd_HHmmss");
225                 return fmt.format(new Date());
226         }
227
228         /**
229          * 衝突回避用の末尾文字を生成する.
230          *
231          * @param retryCount
232          *            リトライ回数
233          * @return 末尾文字
234          */
235         protected String generateSuffix(int retryCount) {
236                 SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd_HHmmss");
237                 String suffix = "_" + fmt.format(new Date());
238                 if (retryCount > 0) {
239                         suffix = suffix + "_" + retryCount;
240                 }
241                 return suffix;
242         }
243
244         /**
245          * キャラクターデータを更新する.
246          *
247          * @param characterData
248          *            キャラクターデータ(有効かつ編集可能であること)
249          * @throws IOException
250          *             失敗
251          */
252         public void updateProfile(CharacterData characterData) throws IOException {
253                 if (characterData == null) {
254                         throw new IllegalArgumentException();
255                 }
256
257                 characterData.checkWritable();
258                 if (!characterData.isValid()) {
259                         throw new IOException("invalid profile: " + characterData);
260                 }
261
262                 // 保存する
263                 saveCharacterDataToXML(characterData);
264
265                 // ディレクトリを準備する
266                 preparePartsDir(characterData);
267         }
268
269         /**
270          * キャラクターデータのパーツイメージを保存するディレクトリを準備する
271          *
272          * @param characterData
273          *            キャラクターデータ
274          * @param baseDir
275          *            ベースディレクトリ
276          * @throws IOException
277          *             失敗
278          */
279         protected void preparePartsDir(CharacterData characterData)
280                         throws IOException {
281                 if (characterData == null) {
282                         throw new IllegalArgumentException();
283                 }
284
285                 characterData.checkWritable();
286                 if (!characterData.isValid()) {
287                         throw new IOException("invalid profile: " + characterData);
288                 }
289
290                 URI docBase = characterData.getDocBase();
291                 if (!"file".equals(docBase.getScheme())) {
292                         throw new IOException("ファイル以外はサポートしていません。:" + docBase);
293                 }
294                 File docBaseFile = new File(docBase);
295                 File baseDir = docBaseFile.getParentFile();
296
297                 if (!baseDir.exists()) {
298                         if (!baseDir.mkdirs()) {
299                                 throw new IOException("can't create directory. " + baseDir);
300                         }
301                 }
302                 for (PartsCategory partsCategory : characterData.getPartsCategories()) {
303                         for (Layer layer : partsCategory.getLayers()) {
304                                 File layerDir = new File(baseDir, layer.getDir());
305                                 if (!layerDir.exists()) {
306                                         if (!layerDir.mkdirs()) {
307                                                 throw new IOException("can't create directory. "
308                                                                 + layerDir);
309                                         }
310                                 }
311                         }
312                 }
313         }
314
315         /**
316          * キャラクターデータを読み込んだ場合に返されるコールバック
317          */
318         public interface ListProfileCallback {
319
320                 /**
321                  * キャラクターデータを読み込んだ場合.<br>
322                  * 戻り値がfalseの場合は読み込みを以降の読み込みを中断します.<br>
323                  * (ただし、すでに読み込み開始している分については中断されません.)
324                  *
325                  * @param characterData
326                  * @return 継続する場合はtrue、中止する場合はfalse
327                  */
328                 boolean receiveCharacterData(CharacterData characterData);
329
330                 /**
331                  * キャラクターデータの読み込みに失敗した場合.<br>
332                  * 戻り値がfalseの場合は読み込みを以降の読み込みを中断します.<br>
333                  * (ただし、すでに読み込み開始している分については中断されません.)
334                  *
335                  * @param dir
336                  *            読み込み対象ディレクトリ
337                  * @param ex
338                  *            例外の内容
339                  * @return 継続する場合はtrue、中止する場合はfalse
340                  */
341                 boolean occureException(File dir, Exception ex);
342         }
343
344         /**
345          * 指定したディレクトリの下のサブフォルダに、character.iniがある場合は、
346          * キャラクターなんとか機のディレクトリとして、標準のcharacter.xmlを生成する.<br>
347          * ただし、書き込み禁止の場合は何もしない.<br>
348          * すでにcharacter.xmlがある場合も何もしない.<br>
349          *
350          * @param dataDir
351          *            キャラクターなんとか機のデータディレクトリ
352          */
353         public void convertCharacterNantokaIniToXml(File dataDir) {
354                 if (dataDir == null || !dataDir.isDirectory() || !dataDir.canWrite()) {
355                         return;
356                 }
357
358                 File[] dirs = dataDir.listFiles();
359                 if (dirs == null) {
360                         dirs = new File[0];
361                 }
362                 for (File dir : dirs) {
363                         if (!dir.isDirectory()) {
364                                 continue;
365                         }
366                         File characterXmlFile = new File(dir,
367                                         CharacterDataPersistent.CONFIG_FILE);
368                         if (characterXmlFile.exists()) {
369                                 // すでにキャラクター定義XMLが存在する場合はスキップする.
370                                 continue;
371                         }
372
373                         File characterIniFile = new File(dir,
374                                         CharacterDataPersistent.COMPATIBLE_CONFIG_NAME);
375                         if (characterIniFile.exists() && characterIniFile.canWrite()
376                                         && dir.canWrite()) {
377                                 // character.iniが存在し、書き込み可能であれば、それをcharacter.xmlに変換する.
378
379                                 // eye_colorフォルダがあるか?
380                                 File eyeColorFolder = new File(dir, "eye_color");
381                                 boolean hasEyeColorFolder = eyeColorFolder.exists()
382                                                 && eyeColorFolder.isDirectory();
383
384                                 DefaultCharacterDataVersion version;
385                                 if (hasEyeColorFolder) {
386                                         // eye_colorフォルダがあればver3形式とみなす.
387                                         version = DefaultCharacterDataVersion.V3;
388                                 } else {
389                                         version = DefaultCharacterDataVersion.V2;
390                                 }
391
392                                 // readmeがあるか?
393                                 String readme = null;
394                                 File readmeFile = new File(dir, "readme.txt");
395                                 if (readmeFile.exists() && readmeFile.canRead()) {
396                                         try {
397                                                 readme = TextReadHelper
398                                                                 .readTextTryEncoding(new FileInputStream(
399                                                                                 readmeFile));
400
401                                         } catch (IOException ex) {
402                                                 logger.log(Level.WARNING, ex.toString(), ex);
403                                         }
404                                 }
405
406                                 try {
407                                         convertFromCharacterIni(characterIniFile, characterXmlFile,
408                                                         version, readme);
409
410                                 } catch (Exception ex) {
411                                         logger.log(Level.WARNING, "character.xmlの生成に失敗しました。:"
412                                                         + characterXmlFile, ex);
413                                 }
414                         }
415                 }
416         }
417
418         /**
419          * キャラクターデータを非同期に読み込む.<br>
420          * 読み込み完了したものが随時、コールバックに渡される.
421          *
422          * @param callback
423          * @return すべての読み込みが完了したか判定し待機することのできるFuture
424          */
425         public Future<?> listProfileAsync(final ListProfileCallback callback) {
426                 if (callback == null) {
427                         throw new IllegalArgumentException();
428                 }
429
430                 // キャラクターデータが格納されている親ディレクトリ
431                 DirectoryConfig dirConfig = DirectoryConfig.getInstance();
432                 File baseDir = dirConfig.getCharactersDir();
433
434                 // キャラクターなんとか機のcharacter.iniがあれば、character.xmlに変換する.
435                 convertCharacterNantokaIniToXml(baseDir);
436
437                 // ファイル名をノーマライズする
438                 FileNameNormalizer normalizer = FileNameNormalizer.getDefault();
439
440                 // キャンセルしたことを示すフラグ
441                 final AtomicBoolean cancelled = new AtomicBoolean(false);
442
443                 // 有効な論理CPU(CORE)数のスレッドで同時実行させる
444                 int numOfProcessors = Runtime.getRuntime().availableProcessors();
445                 final ExecutorService executorSrv = Executors
446                                 .newFixedThreadPool(numOfProcessors);
447                 try {
448                         // キャラクターデータ対象ディレクトリを列挙し、並列に解析する
449                         File[] dirs = baseDir.listFiles(new FileFilter() {
450                                 public boolean accept(File pathname) {
451                                         boolean accept = pathname.isDirectory()
452                                                         && !pathname.getName().startsWith(".");
453                                         if (accept) {
454                                                 File configFile = new File(pathname, CONFIG_FILE);
455                                                 accept = configFile.exists() && configFile.canRead();
456                                         }
457                                         return accept;
458                                 }
459                         });
460                         if (dirs == null) {
461                                 dirs = new File[0];
462                         }
463                         for (File dir : dirs) {
464                                 String path = normalizer.normalize(dir.getPath());
465                                 final File normDir = new File(path);
466
467                                 executorSrv.submit(new Runnable() {
468                                         public void run() {
469                                                 boolean terminate = false;
470                                                 File characterDataXml = new File(normDir, CONFIG_FILE);
471                                                 if (characterDataXml.exists()) {
472                                                         try {
473                                                                 File docBaseFile = new File(normDir,
474                                                                                 CONFIG_FILE);
475                                                                 URI docBase = docBaseFile.toURI();
476                                                                 CharacterData characterData = loadProfile(docBase);
477                                                                 terminate = !callback
478                                                                                 .receiveCharacterData(characterData);
479
480                                                         } catch (Exception ex) {
481                                                                 terminate = !callback.occureException(normDir,
482                                                                                 ex);
483                                                         }
484                                                 }
485                                                 if (terminate) {
486                                                         // 中止が指示されたらスレッドプールを終了する
487                                                         logger.log(Level.FINE, "shutdownNow listProfile");
488                                                         executorSrv.shutdownNow();
489                                                         cancelled.set(true);
490                                                 }
491                                         }
492                                 });
493                         }
494                 } finally {
495                         // タスクの登録を受付終了し、現在のすべてのタスクが完了したらスレッドは終了する.
496                         executorSrv.shutdown();
497                 }
498
499                 // タスクの終了を待機できる疑似フューチャーを作成する.
500                 Future<Object> awaiter = new Future<Object>() {
501                         public boolean cancel(boolean mayInterruptIfRunning) {
502                                 if (executorSrv.isTerminated()) {
503                                         // すでに停止完了済み
504                                         return false;
505                                 }
506                                 executorSrv.shutdownNow();
507                                 cancelled.set(true);
508                                 return true;
509                         }
510
511                         public boolean isCancelled() {
512                                 return cancelled.get();
513                         }
514
515                         public boolean isDone() {
516                                 return executorSrv.isTerminated();
517                         }
518
519                         public Object get() throws InterruptedException, ExecutionException {
520                                 try {
521                                         return get(300, TimeUnit.SECONDS);
522
523                                 } catch (TimeoutException ex) {
524                                         throw new ExecutionException(ex);
525                                 }
526                         }
527
528                         public Object get(long timeout, TimeUnit unit)
529                                         throws InterruptedException, ExecutionException,
530                                         TimeoutException {
531                                 executorSrv.shutdown();
532                                 if (!executorSrv.isTerminated()) {
533                                         executorSrv.awaitTermination(timeout, unit);
534                                 }
535                                 return null;
536                         }
537                 };
538
539                 return awaiter;
540         }
541
542         /**
543          * プロファイルを列挙する.<br>
544          * 読み取りに失敗した場合はエラーハンドラに通知されるが例外は返されない.<br>
545          * 一つも正常なプロファイルがない場合は空のリストが返される.<br>
546          * エラーハンドラの通知は非同期に行われる.
547          *
548          * @param errorHandler
549          *            エラーハンドラ、不要ならばnull
550          * @return プロファイルのリスト(表示名順)、もしくは空
551          */
552         public List<CharacterData> listProfiles(
553                         final ProfileListErrorHandler errorHandler) {
554
555                 final List<CharacterData> profiles = new ArrayList<CharacterData>();
556
557                 Future<?> awaiter = listProfileAsync(new ListProfileCallback() {
558
559                         public boolean receiveCharacterData(CharacterData characterData) {
560                                 synchronized (profiles) {
561                                         profiles.add(characterData);
562                                 }
563                                 return true;
564                         }
565
566                         public boolean occureException(File dir, Exception ex) {
567                                 if (errorHandler != null) {
568                                         errorHandler.occureException(dir, ex);
569                                 }
570                                 return true;
571                         }
572                 });
573
574                 // すべてのキャラクターデータが読み込まれるまで待機する.
575                 try {
576                         awaiter.get();
577
578                 } catch (Exception ex) {
579                         logger.log(Level.WARNING, "listProfile abort.", ex);
580                 }
581
582                 Collections.sort(profiles, CharacterData.SORT_DISPLAYNAME);
583
584                 return Collections.unmodifiableList(profiles);
585         }
586
587         public CharacterData loadProfile(URI docBase) throws IOException {
588                 if (docBase == null) {
589                         throw new IllegalArgumentException();
590                 }
591
592                 // XMLから読み取る
593                 CharacterData characterData = characterDataXmlReader
594                                 .loadCharacterDataFromXML(docBase);
595
596                 return characterData;
597         }
598
599         protected void saveCharacterDataToXML(CharacterData characterData)
600                         throws IOException {
601                 if (characterData == null) {
602                         throw new IllegalArgumentException();
603                 }
604
605                 characterData.checkWritable();
606                 if (!characterData.isValid()) {
607                         throw new IOException("invalid profile: " + characterData);
608                 }
609
610                 URI docBase = characterData.getDocBase();
611                 if (!"file".equals(docBase.getScheme())) {
612                         throw new IOException("ファイル以外はサポートしていません: " + docBase);
613                 }
614
615                 // XML形式で保存(メモリへ)
616                 ByteArrayOutputStream bos = new ByteArrayOutputStream();
617                 try {
618                         characterDataXmlWriter.writeXMLCharacterData(characterData, bos);
619                 } finally {
620                         bos.close();
621                 }
622
623                 // 成功したら実際にファイルに出力
624                 File characterPropXML = new File(docBase);
625                 File baseDir = characterPropXML.getParentFile();
626                 if (!baseDir.exists()) {
627                         if (!baseDir.mkdirs()) {
628                                 logger.log(Level.WARNING, "can't create directory. " + baseDir);
629                         }
630                 }
631
632                 FileOutputStream fos = new FileOutputStream(characterPropXML);
633                 try {
634                         fos.write(bos.toByteArray());
635                 } finally {
636                         fos.close();
637                 }
638         }
639
640         public void saveFavorites(CharacterData characterData) throws IOException {
641                 if (characterData == null) {
642                         throw new IllegalArgumentException();
643                 }
644
645                 // xml形式
646                 UserData favoritesData = getFavoritesUserData(characterData);
647                 OutputStream os = favoritesData.getOutputStream();
648                 try {
649                         characterDataXmlWriter.saveFavorites(characterData, os);
650
651                 } finally {
652                         os.close();
653                 }
654         }
655
656         private UserData getFavoritesUserData(CharacterData characterData) {
657                 if (characterData == null) {
658                         throw new IllegalArgumentException();
659                 }
660
661                 // xml形式の場合、キャラクターディレクトリ上に設定する.
662                 URI docBase = characterData.getDocBase();
663                 File characterDir = new File(docBase).getParentFile();
664                 return new FileUserData(new File(characterDir, FAVORITES_FILE_NAME));
665         }
666
667
668         /**
669          * お気に入り(Favorites)を読み込む.<br>
670          * 現在のパーツセットに追加する形で読み込まれ、同じパーツセットIDのものは上書きされます.<br>
671          *
672          * @param characterData
673          *            キャラクターデータ
674          * @throws IOException
675          *             読み込みに失敗した場合
676          */
677         public void loadFavorites(CharacterData characterData) throws IOException {
678                 if (characterData == null) {
679                         throw new IllegalArgumentException();
680                 }
681
682                 UserData favoritesXml = getFavoritesUserData(characterData);
683                 if (favoritesXml.exists() && favoritesXml.length() > 0) {
684                         InputStream is = favoritesXml.openStream();
685                         try {
686                                 characterDataXmlReader.loadPartsSet(characterData, is);
687
688                         } finally {
689                                 is.close();
690                         }
691                 }
692         }
693
694
695         /**
696          * 既存のキャラクター定義を削除する.<br>
697          * 有効なdocBaseがあり、そのxmlファイルが存在するものについて、削除を行う.<br>
698          * forceRemoveがtrueでない場合はキャラクター定義 character.xmlファイルの拡張子を
699          * リネームすることでキャラクター定義として認識させなくする.<br>
700          * forceRevmoeがtrueの場合は実際にファイルを削除する.<br>
701          * character.xml、favorites、workingsetのキャッシュも削除される.<br>
702          *
703          * @param cd
704          *            キャラクター定義
705          * @param forceRemove
706          *            ファイルを削除する場合はtrue、リネームして無効にするだけならfalse
707          * @throws IOException
708          *             削除またはリネームできなかった場合
709          */
710         public void remove(CharacterData cd, boolean forceRemove)
711                         throws IOException {
712                 if (cd == null || cd.getDocBase() == null) {
713                         throw new IllegalArgumentException();
714                 }
715
716                 URI docBase = cd.getDocBase();
717                 File xmlFile = new File(docBase);
718                 if (!xmlFile.exists() || !xmlFile.isFile()) {
719                         // すでに存在しない場合
720                         return;
721                 }
722
723                 // favories.xmlの削除
724                 if (forceRemove) {
725                         UserData[] favoritesDatas = new UserData[]{getFavoritesUserData(cd)};
726                         for (UserData favoriteData : favoritesDatas) {
727                                 if (favoriteData != null && favoriteData.exists()) {
728                                         logger.log(Level.INFO, "remove file: " + favoriteData);
729                                         favoriteData.delete();
730                                 }
731                         }
732                 }
733
734                 // ワーキングセットの削除
735                 // XML形式でのワーキングセットの保存
736                 WorkingSetPersist workingSetPersist = WorkingSetPersist.getInstance();
737                 workingSetPersist.removeWorkingSet(cd);
738
739                 // xmlファイルの拡張子を変更することでキャラクター定義として認識させない.
740                 // (削除に失敗するケースに備えて先にリネームする.)
741                 String suffix = "." + System.currentTimeMillis() + ".deleted";
742                 File bakFile = new File(xmlFile.getPath() + suffix);
743                 if (!xmlFile.renameTo(bakFile)) {
744                         throw new IOException("can not rename configuration file.:"
745                                         + xmlFile);
746                 }
747
748                 // ディレクトリ
749                 File baseDir = xmlFile.getParentFile();
750
751                 if (!forceRemove) {
752                         // 削除されたディレクトリであることを識別できるようにディレクトリ名も変更する.
753                         File parentBak = new File(baseDir.getPath() + suffix);
754                         if (!baseDir.renameTo(parentBak)) {
755                                 throw new IOException("can't rename directory. " + baseDir);
756                         }
757
758                 } else {
759                         // 完全に削除する
760                         removeRecursive(baseDir);
761                 }
762         }
763
764         /**
765          * 指定したファイルを削除します.<br>
766          * 指定したファイルがディレクトリを示す場合、このディレクトリを含む配下のすべてのファイルとディレクトリを削除します.<br>
767          *
768          * @param file
769          *            ファイル、またはディレクトリ
770          * @throws IOException
771          *             削除できない場合
772          */
773         protected void removeRecursive(File file) throws IOException {
774                 if (file == null) {
775                         throw new IllegalArgumentException();
776                 }
777                 if (!file.exists()) {
778                         return;
779                 }
780                 if (file.isDirectory()) {
781                         File[] children = file.listFiles();
782                         if (children != null) {
783                                 for (File child : children) {
784                                         removeRecursive(child);
785                                 }
786                         }
787                 }
788                 if (!file.delete()) {
789                         throw new IOException("can't delete file. " + file);
790                 }
791         }
792
793         protected Iterable<Node> iterable(final NodeList nodeList) {
794                 final int mx;
795                 if (nodeList == null) {
796                         mx = 0;
797                 } else {
798                         mx = nodeList.getLength();
799                 }
800                 return new Iterable<Node>() {
801                         public Iterator<Node> iterator() {
802                                 return new Iterator<Node>() {
803                                         private int idx = 0;
804                                         public boolean hasNext() {
805                                                 return idx < mx;
806                                         }
807                                         public Node next() {
808                                                 if (idx >= mx) {
809                                                         throw new NoSuchElementException();
810                                                 }
811                                                 return nodeList.item(idx++);
812                                         }
813                                         public void remove() {
814                                                 throw new UnsupportedOperationException();
815                                         }
816                                 };
817                         }
818                 };
819         }
820
821         protected URL getEmbeddedResourceURL(String schemaName) {
822                 return this.getClass().getResource(schemaName);
823         }
824
825         /**
826          * サンプルピクチャを読み込む.<br>
827          * ピクチャが存在しなければnullを返す. キャラクター定義がValidでない場合は常にnullを返す.<br>
828          *
829          * @param characterData
830          *            キャラクター定義、null不可
831          * @param loader
832          *            イメージのローダー、null不可
833          * @return ピクチャのイメージ、もしくはnull
834          * @throws IOException
835          *             ピクチャの読み取りに失敗した場合
836          */
837         public BufferedImage loadSamplePicture(CharacterData characterData,
838                         ImageLoader loader) throws IOException {
839                 if (characterData == null || loader == null) {
840                         throw new IllegalArgumentException();
841                 }
842                 if (!characterData.isValid()) {
843                         return null;
844                 }
845
846                 File sampleImageFile = getSamplePictureFile(characterData);
847                 if (sampleImageFile != null && sampleImageFile.exists()) {
848                         LoadedImage loadedImage = loader.load(new FileImageResource(
849                                         sampleImageFile));
850                         return loadedImage.getImage();
851                 }
852                 return null;
853         }
854
855         /**
856          * キャラクターのサンプルピクチャが登録可能であるか?<br>
857          * キャラクターデータが有効であり、且つ、ファイルの書き込みが可能であればtrueを返す.<br>
858          * キャラクターデータがnullもしくは無効であるか、ファイルプロトコルでないか、ファイルが書き込み禁止であればfalseょ返す.<br>
859          *
860          * @param characterData
861          *            キャラクターデータ
862          * @return 書き込み可能であればtrue、そうでなければfalse
863          */
864         public boolean canSaveSamplePicture(CharacterData characterData) {
865                 if (characterData == null || !characterData.isValid()) {
866                         return false;
867                 }
868                 File sampleImageFile = getSamplePictureFile(characterData);
869                 if (sampleImageFile != null) {
870                         if (sampleImageFile.exists() && sampleImageFile.canWrite()) {
871                                 return true;
872                         }
873                         if (!sampleImageFile.exists()) {
874                                 File parentDir = sampleImageFile.getParentFile();
875                                 if (parentDir != null) {
876                                         return parentDir.canWrite();
877                                 }
878                         }
879                 }
880                 return false;
881         }
882
883         /**
884          * サンプルピクチャとして認識されるファイル位置を返す.<br>
885          * ファイルが実在するかは問わない.<br>
886          * DocBaseが未設定であるか、ファィルプロトコルとして返せない場合はnullを返す.<br>
887          *
888          * @param characterData
889          *            キャラクター定義
890          * @return サンプルピクチャの保存先のファイル位置、もしくはnull
891          */
892         protected File getSamplePictureFile(CharacterData characterData) {
893                 if (characterData == null) {
894                         throw new IllegalArgumentException();
895                 }
896                 URI docBase = characterData.getDocBase();
897                 if (docBase != null && "file".endsWith(docBase.getScheme())) {
898                         File docBaseFile = new File(docBase);
899                         return new File(docBaseFile.getParentFile(), SAMPLE_IMAGE_FILENAME);
900                 }
901                 return null;
902         }
903
904         /**
905          * サンプルピクチャを保存する.
906          *
907          * @param characterData
908          *            キャラクターデータ
909          * @param samplePicture
910          *            サンプルピクチャ
911          * @throws IOException
912          *             保存に失敗した場合
913          */
914         public void saveSamplePicture(CharacterData characterData,
915                         BufferedImage samplePicture) throws IOException {
916                 if (!canSaveSamplePicture(characterData)) {
917                         throw new IOException("can not write a sample picture.:"
918                                         + characterData);
919                 }
920                 File sampleImageFile = getSamplePictureFile(characterData); // canSaveSamplePictureで書き込み先検証済み
921
922                 if (samplePicture != null) {
923                         // 登録または更新
924
925                         // pngで保存するので背景色は透過になるが、一応、コードとしては入れておく。
926                         AppConfig appConfig = AppConfig.getInstance();
927                         Color sampleImageBgColor = appConfig.getSampleImageBgColor();
928
929                         ImageSaveHelper imageSaveHelper = new ImageSaveHelper();
930                         imageSaveHelper.savePicture(samplePicture, sampleImageBgColor,
931                                         sampleImageFile, null);
932
933                 } else {
934                         // 削除
935                         if (sampleImageFile.exists()) {
936                                 if (!sampleImageFile.delete()) {
937                                         throw new IOException("sample pucture delete failed. :"
938                                                         + sampleImageFile);
939                                 }
940                         }
941                 }
942         }
943
944
945
946
947         /**
948          * character.iniを読み取り、character.xmlを生成します.<br>
949          * すでにcharacter.xmlがある場合は上書きされます.<br>
950          * 途中でエラーが発生した場合はcharacter.xmlは削除されます.<br>
951          *
952          * @param characterIniFile
953          *            読み取るcharatcer.iniファイル
954          * @param characterXmlFile
955          *            書き込まれるcharacter.xmlファイル
956          * @param version
957          *            デフォルトキャラクターセットのバージョン
958          * @param description
959          *            説明
960          * @throws IOException
961          *             失敗した場合
962          */
963         public void convertFromCharacterIni(File characterIniFile,
964                         File characterXmlFile, DefaultCharacterDataVersion version,
965                         String description)
966                         throws IOException {
967                 if (characterIniFile == null || characterXmlFile == null
968                                 || version == null) {
969                         throw new IllegalArgumentException();
970                 }
971
972                 // character.iniから、character.xmlの内容を構築する.
973                 FileInputStream is = new FileInputStream(characterIniFile);
974                 CharacterData characterData;
975                 try {
976                         CharacterDataIniReader iniReader = new CharacterDataIniReader();
977                         characterData = iniReader.readCharacterDataFromIni(is, version);
978
979                 } finally {
980                         is.close();
981                 }
982
983                 // 説明文を設定する
984                 if (description != null) {
985                         characterData.setDescription(description);
986                 }
987
988                 // docBase
989                 URI docBase = characterXmlFile.toURI();
990                 characterData.setDocBase(docBase);
991
992                 // character.xmlの書き込み
993                 boolean succeeded = false;
994                 try {
995                         FileOutputStream outstm = new FileOutputStream(characterXmlFile);
996                         try {
997                                 characterDataXmlWriter.writeXMLCharacterData(characterData,
998                                                 outstm);
999                         } finally {
1000                                 outstm.close();
1001                         }
1002
1003                         succeeded = true;
1004
1005                 } finally {
1006                         if (!succeeded) {
1007                                 // 途中で失敗した場合は生成ファイルを削除しておく.
1008                                 try {
1009                                         if (characterXmlFile.exists()) {
1010                                                 characterXmlFile.delete();
1011                                         }
1012
1013                                 } catch (Exception ex) {
1014                                         logger.log(Level.WARNING, "ファイルの削除に失敗しました。:"
1015                                                         + characterXmlFile, ex);
1016                                 }
1017                         }
1018                 }
1019         }
1020
1021         /**
1022          * お勧めリンクリストが設定されていない場合(nullの場合)、デフォルトのお勧めリストを設定する.<br>
1023          * すでに設定されている場合(空を含む)は何もしない.<br>
1024          * <br>
1025          * おすすめリンクがサポートされてなかったころのデータは、おすすめリンク用のタグそのものが存在せずnullとなる.<br>
1026          * サポート後のデータでリンクを未設定にしている場合は、空のリストとなる.<br>
1027          * したがって、nullの場合のみ、おすすめリンクを補完する.<br>
1028          *
1029          * @param characterData
1030          *            キャラクターデータ
1031          */
1032         public void compensateRecommendationList(CharacterData characterData) {
1033                 if (characterData == null) {
1034                         throw new IllegalArgumentException();
1035                 }
1036                 if (characterData.getRecommendationURLList() != null) {
1037                         // 補填の必要なし
1038                         return;
1039                 }
1040                 CharacterDataDefaultProvider defProv = new CharacterDataDefaultProvider();
1041                 CharacterData defaultCd = defProv
1042                                 .createDefaultCharacterData(DefaultCharacterDataVersion.V3);
1043                 characterData.setRecommendationURLList(defaultCd
1044                                 .getRecommendationURLList());
1045         }
1046 }