OSDN Git Service

・ キャラクターデータディレクトリの読み込みを並列化した。
[charactermanaj/CharacterManaJ.git] / src / charactermanaj / model / io / CharacterDataPersistent.java
1 package charactermanaj.model.io;\r
2 \r
3 import java.awt.Color;\r
4 import java.awt.image.BufferedImage;\r
5 import java.io.ByteArrayOutputStream;\r
6 import java.io.File;\r
7 import java.io.FileFilter;\r
8 import java.io.FileInputStream;\r
9 import java.io.FileOutputStream;\r
10 import java.io.IOException;\r
11 import java.io.InputStream;\r
12 import java.io.OutputStream;\r
13 import java.io.OutputStreamWriter;\r
14 import java.net.URI;\r
15 import java.net.URL;\r
16 import java.nio.charset.Charset;\r
17 import java.text.SimpleDateFormat;\r
18 import java.util.ArrayList;\r
19 import java.util.Collection;\r
20 import java.util.Collections;\r
21 import java.util.Date;\r
22 import java.util.HashMap;\r
23 import java.util.Iterator;\r
24 import java.util.LinkedList;\r
25 import java.util.List;\r
26 import java.util.Locale;\r
27 import java.util.NoSuchElementException;\r
28 import java.util.concurrent.ExecutionException;\r
29 import java.util.concurrent.ExecutorService;\r
30 import java.util.concurrent.Executors;\r
31 import java.util.concurrent.Future;\r
32 import java.util.concurrent.TimeUnit;\r
33 import java.util.concurrent.TimeoutException;\r
34 import java.util.concurrent.atomic.AtomicBoolean;\r
35 import java.util.logging.Level;\r
36 import java.util.logging.Logger;\r
37 \r
38 import javax.xml.XMLConstants;\r
39 import javax.xml.namespace.NamespaceContext;\r
40 import javax.xml.parsers.DocumentBuilder;\r
41 import javax.xml.parsers.DocumentBuilderFactory;\r
42 import javax.xml.parsers.ParserConfigurationException;\r
43 import javax.xml.parsers.SAXParser;\r
44 import javax.xml.parsers.SAXParserFactory;\r
45 import javax.xml.transform.OutputKeys;\r
46 import javax.xml.transform.Transformer;\r
47 import javax.xml.transform.TransformerConfigurationException;\r
48 import javax.xml.transform.TransformerException;\r
49 import javax.xml.transform.TransformerFactory;\r
50 import javax.xml.transform.dom.DOMSource;\r
51 import javax.xml.transform.stream.StreamResult;\r
52 import javax.xml.validation.Schema;\r
53 import javax.xml.validation.SchemaFactory;\r
54 import javax.xml.xpath.XPath;\r
55 import javax.xml.xpath.XPathFactory;\r
56 \r
57 import org.w3c.dom.Attr;\r
58 import org.w3c.dom.Document;\r
59 import org.w3c.dom.Element;\r
60 import org.w3c.dom.Node;\r
61 import org.w3c.dom.NodeList;\r
62 import org.xml.sax.Attributes;\r
63 import org.xml.sax.ErrorHandler;\r
64 import org.xml.sax.SAXException;\r
65 import org.xml.sax.SAXParseException;\r
66 import org.xml.sax.helpers.DefaultHandler;\r
67 \r
68 import charactermanaj.graphics.io.FileImageResource;\r
69 import charactermanaj.graphics.io.ImageLoader;\r
70 import charactermanaj.graphics.io.ImageSaveHelper;\r
71 import charactermanaj.graphics.io.LoadedImage;\r
72 import charactermanaj.model.AppConfig;\r
73 import charactermanaj.model.CharacterData;\r
74 import charactermanaj.model.Layer;\r
75 import charactermanaj.model.PartsAuthorInfo;\r
76 import charactermanaj.model.PartsCategory;\r
77 import charactermanaj.model.PartsManageData;\r
78 import charactermanaj.model.PartsManageData.PartsKey;\r
79 import charactermanaj.ui.MainFrame;\r
80 import charactermanaj.util.DirectoryConfig;\r
81 import charactermanaj.util.FileNameNormalizer;\r
82 import charactermanaj.util.FileUserData;\r
83 import charactermanaj.util.UserData;\r
84 \r
85 public class CharacterDataPersistent {\r
86 \r
87         /**\r
88          * キャラクター定義ファイル名\r
89          */\r
90         public static final String CONFIG_FILE = "character.xml";\r
91 \r
92         /**\r
93          * キャラクターなんとか機用のiniファイル名\r
94          */\r
95         public static final String COMPATIBLE_CONFIG_NAME = "character.ini";\r
96 \r
97         /**\r
98          * ロガー\r
99          */\r
100         private static final Logger logger = Logger\r
101                         .getLogger(CharacterDataPersistent.class.getName());\r
102 \r
103         /**\r
104          * キャラクターデータを格納したXMLのリーダー\r
105          */\r
106         private final CharacterDataXMLReader characterDataXmlReader = new CharacterDataXMLReader();\r
107 \r
108         private final CharacterDataXMLWriter characterDataXmlWriter = new CharacterDataXMLWriter();\r
109 \r
110         /**\r
111          * サンプルイメージファイル名\r
112          */\r
113         public static final String SAMPLE_IMAGE_FILENAME = "preview.png";\r
114 \r
115         /**\r
116          * キャラクター定義バージョン\r
117          */\r
118         public static final String VERSION_SIG_1_0 = "1.0";\r
119 \r
120         /**\r
121          * キャラクター定義XMLファイルの名前空間\r
122          */\r
123         public static final String NS = "http://charactermanaj.sourceforge.jp/schema/charactermanaj";\r
124 \r
125         /**\r
126          * パーツ定義XMLファイルの名前空間\r
127          */\r
128         public static final String NS_PARTSDEF = "http://charactermanaj.sourceforge.jp/schema/charactermanaj-partsdef";\r
129 \r
130         /**\r
131          * キャラクター定義XML用のスキーマ定義リソース名\r
132          */\r
133         private static final String CHARACTER_XML_SCHEMA = "/schema/character.xsd";\r
134 \r
135         /**\r
136          * キャラクター定義XML用のスキーマ定義リソース名\r
137          */\r
138         private static final String CHARACTER_XML_SCHEMA_0_8 = "/schema/0.8/character.xsd";\r
139 \r
140         /**\r
141          * パーツセット定義XMLのスキーマ定義リソース名\r
142          */\r
143         private static final String PARTSSET_XML_SCHEMA = "/schema/partsset.xsd";\r
144 \r
145         /**\r
146          * パーツセット定義XMLのスキーマ定義リソース名\r
147          */\r
148         private static final String PARTSSET_XML_SCHEMA_0_8 = "/schema/0.8/partsset.xsd";\r
149 \r
150         /**\r
151          * XMLのデータ形式.<br>\r
152          * SchemaのバリデージョンチェックとDOMの解析を始める前にSAXで流し込んで、\r
153          * 最初のエレメント名や使用している名前空間、バージョンを読み込んで、 スキーマをきりかえられるようにするためのもの.\r
154          * \r
155          * @author seraphy\r
156          */\r
157         public static class DocInfo {\r
158 \r
159                 private String firstElementName;\r
160 \r
161                 private String version;\r
162 \r
163                 private String namespace;\r
164 \r
165                 public void setFirstElementName(String firstElementName) {\r
166                         this.firstElementName = firstElementName;\r
167                 }\r
168 \r
169                 /**\r
170                  * 最初の要素のqName\r
171                  * \r
172                  * @return\r
173                  */\r
174                 public String getFirstElementName() {\r
175                         return firstElementName;\r
176                 }\r
177 \r
178                 public void setNamespace(String namespace) {\r
179                         this.namespace = namespace;\r
180                 }\r
181 \r
182                 public void setVersion(String version) {\r
183                         this.version = version;\r
184                 }\r
185 \r
186                 /**\r
187                  * 最初の要素に指定されているxmlns属性の値\r
188                  * \r
189                  * @param namespace\r
190                  */\r
191                 public String getNamespace() {\r
192                         return namespace;\r
193                 }\r
194 \r
195                 /**\r
196                  * 最初の要素に指定されているversion属性の値\r
197                  * \r
198                  * @return\r
199                  */\r
200                 public String getVersion() {\r
201                         return version;\r
202                 }\r
203 \r
204                 @Override\r
205                 public String toString() {\r
206                         return firstElementName + " /version: " + version + " /namespace:"\r
207                                         + namespace;\r
208                 }\r
209         }\r
210 \r
211         /**\r
212          * プロファイルの列挙時のエラーハンドラ.<br>\r
213          * \r
214          * @author seraphy\r
215          */\r
216         public interface ProfileListErrorHandler {\r
217 \r
218                 /**\r
219                  * エラーが発生したことを通知される\r
220                  * \r
221                  * @param baseDir\r
222                  *            読み込み対象のXMLのファイル\r
223                  * @param ex\r
224                  *            例外\r
225                  */\r
226                 void occureException(File baseDir, Throwable ex);\r
227         }\r
228 \r
229         /**\r
230          * プロファイル列挙時のデフォルトのエラーハンドラ.<br>\r
231          * 標準エラー出力にメッセージをプリントするだけで何もしない.<br>\r
232          */\r
233         public static final ProfileListErrorHandler DEFAULT_ERROR_HANDLER = new ProfileListErrorHandler() {\r
234                 public void occureException(File baseDir, Throwable ex) {\r
235                         logger.log(Level.WARNING, "invalid profile. :" + baseDir, ex);\r
236                 }\r
237         };\r
238 \r
239         /**\r
240          * スキーマのキャッシュ.\r
241          */\r
242         private HashMap<String, Schema> schemaMap = new HashMap<String, Schema>();\r
243 \r
244         /**\r
245          * JAXPで使用するデフォルトのエラーハンドラ\r
246          */\r
247         private static final ErrorHandler errorHandler = new ErrorHandler() {\r
248                 public void error(SAXParseException exception) throws SAXException {\r
249                         throw exception;\r
250                 }\r
251                 public void fatalError(SAXParseException exception) throws SAXException {\r
252                         throw exception;\r
253                 }\r
254                 public void warning(SAXParseException exception) throws SAXException {\r
255                         throw exception;\r
256                 }\r
257         };\r
258 \r
259         /**\r
260          * プライベートコンストラクタ.<br>\r
261          * シングルトン実装であるため、一度だけ呼び出される.\r
262          */\r
263         private CharacterDataPersistent() {\r
264                 super();\r
265         }\r
266 \r
267         /**\r
268          * シングルトン\r
269          */\r
270         private static final CharacterDataPersistent singleton = new CharacterDataPersistent();\r
271 \r
272         /**\r
273          * インスタンスを取得する\r
274          * \r
275          * @return インスタンス\r
276          */\r
277         public static CharacterDataPersistent getInstance() {\r
278                 return singleton;\r
279         }\r
280 \r
281         /**\r
282          * キャラクターデータを新規に保存する.<br>\r
283          * REVがnullである場合は保存に先立ってランダムにREVが設定される.<br>\r
284          * 保存先ディレクトリはユーザー固有のキャラクターデータ保存先のディレクトリにキャラクター定義のIDを基本とする ディレクトリを作成して保存される.<br>\r
285          * ただし、そのディレクトリがすでに存在する場合はランダムな名前で決定される.<br>\r
286          * 実際のxmlの保存先にあわせてDocBaseが設定されて返される.<br>\r
287          * \r
288          * @param characterData\r
289          *            キャラクターデータ (IDは設定済みであること.それ以外はvalid, editableであること。)\r
290          * @throws IOException\r
291          *             失敗\r
292          */\r
293         public void createProfile(CharacterData characterData) throws IOException {\r
294                 if (characterData == null) {\r
295                         throw new IllegalArgumentException();\r
296                 }\r
297 \r
298                 String id = characterData.getId();\r
299                 if (id == null || id.trim().length() == 0) {\r
300                         throw new IOException("missing character-id:" + characterData);\r
301                 }\r
302 \r
303                 // ユーザー個別のキャラクターデータ保存先ディレクトリを取得\r
304                 DirectoryConfig dirConfig = DirectoryConfig.getInstance();\r
305                 File charactersDir = dirConfig.getCharactersDir();\r
306                 if (!charactersDir.exists()) {\r
307                         if (!charactersDir.mkdirs()) {\r
308                                 throw new IOException("can't create the characters directory. "\r
309                                                 + charactersDir);\r
310                         }\r
311                 }\r
312                 if (logger.isLoggable(Level.FINE)) {\r
313                         logger.log(Level.FINE, "check characters-dir: " + charactersDir\r
314                                         + ": exists=" + charactersDir.exists());\r
315                 }\r
316 \r
317                 // 新規に保存先ディレクトリを作成.\r
318                 // 同じ名前のディレクトリがある場合は日付+連番をつけて衝突を回避する\r
319                 File baseDir = null;\r
320                 String suffix = "";\r
321                 String name = characterData.getName();\r
322                 if (name == null) {\r
323                         // 表示名が定義されていなければIDで代用する.(IDは必須)\r
324                         name = characterData.getId();\r
325                 }\r
326                 for (int retry = 0;; retry++) {\r
327                         baseDir = new File(charactersDir, name + suffix);\r
328                         if (!baseDir.exists()) {\r
329                                 break;\r
330                         }\r
331                         if (retry > 100) {\r
332                                 throw new IOException("character directory conflict.:"\r
333                                                 + baseDir);\r
334                         }\r
335                         // 衝突回避の末尾文字を設定\r
336                         suffix = generateSuffix(retry);\r
337                 }\r
338                 if (!baseDir.exists()) {\r
339                         if (!baseDir.mkdirs()) {\r
340                                 throw new IOException("can't create directory. " + baseDir);\r
341                         }\r
342                         logger.log(Level.INFO, "create character-dir: " + baseDir);\r
343                 }\r
344 \r
345                 // 保存先を確認\r
346                 File characterPropXML = new File(baseDir, CONFIG_FILE);\r
347                 if (characterPropXML.exists() && !characterPropXML.isFile()) {\r
348                         throw new IOException("character.xml is not a regular file.:"\r
349                                         + characterPropXML);\r
350                 }\r
351                 if (characterPropXML.exists() && !characterPropXML.canWrite()) {\r
352                         throw new IOException("character.xml is not writable.:"\r
353                                         + characterPropXML);\r
354                 }\r
355 \r
356                 // DocBaseを実際の保存先に更新\r
357                 URI docBase = characterPropXML.toURI();\r
358                 characterData.setDocBase(docBase);\r
359 \r
360                 // リビジョンが指定されてなければ新規にリビジョンを割り当てる。\r
361                 if (characterData.getRev() == null) {\r
362                         characterData.setRev(generateRev());\r
363                 }\r
364 \r
365                 // 保存する.\r
366                 saveCharacterDataToXML(characterData);\r
367 \r
368                 // ディレクトリを準備する\r
369                 preparePartsDir(characterData);\r
370         }\r
371 \r
372         /**\r
373          * リビジョンを生成して返す.\r
374          * \r
375          * @return リビジョン用文字列\r
376          */\r
377         public String generateRev() {\r
378                 SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd_HHmmss");\r
379                 return fmt.format(new Date());\r
380         }\r
381 \r
382         /**\r
383          * 衝突回避用の末尾文字を生成する.\r
384          * \r
385          * @param retryCount\r
386          *            リトライ回数\r
387          * @return 末尾文字\r
388          */\r
389         protected String generateSuffix(int retryCount) {\r
390                 SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd_HHmmss");\r
391                 String suffix = "_" + fmt.format(new Date());\r
392                 if (retryCount > 0) {\r
393                         suffix = suffix + "_" + retryCount;\r
394                 }\r
395                 return suffix;\r
396         }\r
397 \r
398         /**\r
399          * キャラクターデータを更新する.\r
400          * \r
401          * @param characterData\r
402          *            キャラクターデータ(有効かつ編集可能であること)\r
403          * @throws IOException\r
404          *             失敗\r
405          */\r
406         public void updateProfile(CharacterData characterData) throws IOException {\r
407                 if (characterData == null) {\r
408                         throw new IllegalArgumentException();\r
409                 }\r
410 \r
411                 characterData.checkWritable();\r
412                 if (!characterData.isValid()) {\r
413                         throw new IOException("invalid profile: " + characterData);\r
414                 }\r
415 \r
416                 // 保存する\r
417                 saveCharacterDataToXML(characterData);\r
418 \r
419                 // ディレクトリを準備する\r
420                 preparePartsDir(characterData);\r
421         }\r
422 \r
423         /**\r
424          * キャラクターデータのパーツイメージを保存するディレクトリを準備する\r
425          * \r
426          * @param characterData\r
427          *            キャラクターデータ\r
428          * @param baseDir\r
429          *            ベースディレクトリ\r
430          * @throws IOException\r
431          *             失敗\r
432          */\r
433         protected void preparePartsDir(CharacterData characterData)\r
434                         throws IOException {\r
435                 if (characterData == null) {\r
436                         throw new IllegalArgumentException();\r
437                 }\r
438 \r
439                 characterData.checkWritable();\r
440                 if (!characterData.isValid()) {\r
441                         throw new IOException("invalid profile: " + characterData);\r
442                 }\r
443 \r
444                 URI docBase = characterData.getDocBase();\r
445                 if (!"file".equals(docBase.getScheme())) {\r
446                         throw new IOException("ファイル以外はサポートしていません。:" + docBase);\r
447                 }\r
448                 File docBaseFile = new File(docBase);\r
449                 File baseDir = docBaseFile.getParentFile();\r
450 \r
451                 if (!baseDir.exists()) {\r
452                         if (!baseDir.mkdirs()) {\r
453                                 throw new IOException("can't create directory. " + baseDir);\r
454                         }\r
455                 }\r
456                 for (PartsCategory partsCategory : characterData.getPartsCategories()) {\r
457                         for (Layer layer : partsCategory.getLayers()) {\r
458                                 File layerDir = new File(baseDir, layer.getDir());\r
459                                 if (!layerDir.exists()) {\r
460                                         if (!layerDir.mkdirs()) {\r
461                                                 throw new IOException("can't create directory. "\r
462                                                                 + layerDir);\r
463                                         }\r
464                                 }\r
465                         }\r
466                 }\r
467         }\r
468 \r
469         /**\r
470          * キャラクターデータを読み込んだ場合に返されるコールバック\r
471          */\r
472         public interface ListProfileCallback {\r
473 \r
474                 /**\r
475                  * キャラクターデータを読み込んだ場合.<br>\r
476                  * 戻り値がfalseの場合は読み込みを以降の読み込みを中断します.<br>\r
477                  * (ただし、すでに読み込み開始している分については中断されません.)\r
478                  * \r
479                  * @param characterData\r
480                  * @return 継続する場合はtrue、中止する場合はfalse\r
481                  */\r
482                 boolean receiveCharacterData(CharacterData characterData);\r
483 \r
484                 /**\r
485                  * キャラクターデータの読み込みに失敗した場合.<br>\r
486                  * 戻り値がfalseの場合は読み込みを以降の読み込みを中断します.<br>\r
487                  * (ただし、すでに読み込み開始している分については中断されません.)\r
488                  * \r
489                  * @param dir\r
490                  *            読み込み対象ディレクトリ\r
491                  * @param ex\r
492                  *            例外の内容\r
493                  * @return 継続する場合はtrue、中止する場合はfalse\r
494                  */\r
495                 boolean occureException(File dir, Exception ex);\r
496         }\r
497 \r
498         /**\r
499          * キャラクターデータを非同期に読み込む.<br>\r
500          * 読み込み完了したものが随時、コールバックに渡される.\r
501          * \r
502          * @param callback\r
503          * @return すべての読み込みが完了したか判定し待機することのできるFuture\r
504          */\r
505         public Future<?> listProfileAsync(final ListProfileCallback callback) {\r
506                 if (callback == null) {\r
507                         throw new IllegalArgumentException();\r
508                 }\r
509 \r
510                 // キャラクターデータが格納されている親ディレクトリのリスト\r
511                 DirectoryConfig dirConfig = DirectoryConfig.getInstance();\r
512                 File[] baseDirs = {dirConfig.getCharactersDir(),};\r
513 \r
514                 // ファイル名をノーマライズする\r
515                 FileNameNormalizer normalizer = FileNameNormalizer.getDefault();\r
516 \r
517                 // キャンセルしたことを示すフラグ\r
518                 final AtomicBoolean cancelled = new AtomicBoolean(false);\r
519 \r
520                 // 有効な論理CPU(CORE)数のスレッドで同時実行させる\r
521                 int numOfProcessors = Runtime.getRuntime().availableProcessors();\r
522                 final ExecutorService executorSrv = Executors\r
523                                 .newFixedThreadPool(numOfProcessors);\r
524                 try {\r
525                         // キャラクターデータ対象ディレクトリを列挙し、並列に解析する\r
526                         for (File baseDir : baseDirs) {\r
527                                 if (baseDir == null || !baseDir.exists()\r
528                                                 || !baseDir.isDirectory()) {\r
529                                         continue;\r
530                                 }\r
531                                 for (File dir : baseDir.listFiles(new FileFilter() {\r
532                                         public boolean accept(File pathname) {\r
533                                                 boolean accept = pathname.isDirectory()\r
534                                                                 && !pathname.getName().startsWith(".");\r
535                                                 if (accept) {\r
536                                                         File configFile = new File(pathname, CONFIG_FILE);\r
537                                                         accept = configFile.exists()\r
538                                                                         && configFile.canRead();\r
539                                                 }\r
540                                                 return accept;\r
541                                         }\r
542                                 })) {\r
543                                         String path = normalizer.normalize(dir.getPath());\r
544                                         final File normDir = new File(path);\r
545 \r
546                                         executorSrv.submit(new Runnable() {\r
547                                                 public void run() {\r
548                                                         boolean terminate = false;\r
549                                                         File characterDataXml = new File(normDir,\r
550                                                                         CONFIG_FILE);\r
551                                                         if (characterDataXml.exists()) {\r
552                                                                 try {\r
553                                                                         File docBaseFile = new File(normDir,\r
554                                                                                         CONFIG_FILE);\r
555                                                                         URI docBase = docBaseFile.toURI();\r
556                                                                         CharacterData characterData = loadProfile(docBase);\r
557                                                                         terminate = !callback\r
558                                                                                         .receiveCharacterData(characterData);\r
559 \r
560                                                                 } catch (Exception ex) {\r
561                                                                         terminate = !callback.occureException(\r
562                                                                                         normDir, ex);\r
563                                                                 }\r
564                                                         }\r
565                                                         if (terminate) {\r
566                                                                 // 中止が指示されたらスレッドプールを終了する\r
567                                                                 logger.log(Level.FINE,\r
568                                                                                 "shutdownNow listProfile");\r
569                                                                 executorSrv.shutdownNow();\r
570                                                                 cancelled.set(true);\r
571                                                         }\r
572                                                 }\r
573                                         });\r
574                                 }\r
575                         }\r
576                 } finally {\r
577                         // タスクの登録を受付終了し、現在のすべてのタスクが完了したらスレッドは終了する.\r
578                         executorSrv.shutdown();\r
579                 }\r
580 \r
581                 // タスクの終了を待機できる疑似フューチャーを作成する.\r
582                 Future<Object> awaiter = new Future<Object>() {\r
583                         public boolean cancel(boolean mayInterruptIfRunning) {\r
584                                 if (executorSrv.isTerminated()) {\r
585                                         // すでに停止完了済み\r
586                                         return false;\r
587                                 }\r
588                                 executorSrv.shutdownNow();\r
589                                 cancelled.set(true);\r
590                                 return true;\r
591                         }\r
592 \r
593                         public boolean isCancelled() {\r
594                                 return cancelled.get();\r
595                         }\r
596 \r
597                         public boolean isDone() {\r
598                                 return executorSrv.isTerminated();\r
599                         }\r
600 \r
601                         public Object get() throws InterruptedException, ExecutionException {\r
602                                 try {\r
603                                         return get(300, TimeUnit.SECONDS);\r
604 \r
605                                 } catch (TimeoutException ex) {\r
606                                         throw new ExecutionException(ex);\r
607                                 }\r
608                         }\r
609 \r
610                         public Object get(long timeout, TimeUnit unit)\r
611                                         throws InterruptedException, ExecutionException,\r
612                                         TimeoutException {\r
613                                 executorSrv.shutdown();\r
614                                 if (!executorSrv.isTerminated()) {\r
615                                         executorSrv.awaitTermination(timeout, unit);\r
616                                 }\r
617                                 return null;\r
618                         }\r
619                 };\r
620 \r
621                 return awaiter;\r
622         }\r
623 \r
624         /**\r
625          * プロファイルを列挙する.<br>\r
626          * 読み取りに失敗した場合はエラーハンドラに通知されるが例外は返されない.<br>\r
627          * 一つも正常なプロファイルがない場合は空のリストが返される.<br>\r
628          * エラーハンドラの通知は非同期に行われる.\r
629          * \r
630          * @param errorHandler\r
631          *            エラーハンドラ、不要ならばnull\r
632          * @return プロファイルのリスト(表示名順)、もしくは空\r
633          */\r
634         public List<CharacterData> listProfiles(\r
635                         final ProfileListErrorHandler errorHandler) {\r
636 \r
637                 final List<CharacterData> profiles = new ArrayList<CharacterData>();\r
638 \r
639                 Future<?> awaiter = listProfileAsync(new ListProfileCallback() {\r
640 \r
641                         public boolean receiveCharacterData(CharacterData characterData) {\r
642                                 synchronized (profiles) {\r
643                                         profiles.add(characterData);\r
644                                 }\r
645                                 return true;\r
646                         }\r
647 \r
648                         public boolean occureException(File dir, Exception ex) {\r
649                                 if (errorHandler != null) {\r
650                                         errorHandler.occureException(dir, ex);\r
651                                 }\r
652                                 return true;\r
653                         }\r
654                 });\r
655 \r
656                 // すべてのキャラクターデータが読み込まれるまで待機する.\r
657                 try {\r
658                         awaiter.get();\r
659 \r
660                 } catch (Exception ex) {\r
661                         logger.log(Level.WARNING, "listProfile abort.", ex);\r
662                 }\r
663 \r
664                 Collections.sort(profiles, CharacterData.SORT_DISPLAYNAME);\r
665 \r
666                 return Collections.unmodifiableList(profiles);\r
667         }\r
668 \r
669         public CharacterData loadProfile(URI docBase) throws IOException {\r
670                 if (docBase == null) {\r
671                         throw new IllegalArgumentException();\r
672                 }\r
673 \r
674                 // XMLから読み取る\r
675                 CharacterData characterData = characterDataXmlReader\r
676                                 .loadCharacterDataFromXML(docBase);\r
677 \r
678                 return characterData;\r
679         }\r
680 \r
681 \r
682         /**\r
683          * XMLの最初の要素と、その要素の属性にあるversionとxmlnsを取得して返す.<br>\r
684          * XMLとして不正などの理由で取得できない場合はnullを返す.<br>\r
685          * 読み込みそのものに失敗した場合は例外を返す.<br>\r
686          * \r
687          * @param is\r
688          *            XMLコンテンツと想定される入力ストリーム\r
689          * @return DocInfo情報、もしくはnull\r
690          * @throws IOException\r
691          *             読み込みに失敗した場合\r
692          */\r
693         public DocInfo readDocumentType(InputStream is) throws IOException {\r
694 \r
695                 SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();\r
696                 SAXParser saxParser;\r
697                 try {\r
698                         saxParser = saxParserFactory.newSAXParser();\r
699                 } catch (ParserConfigurationException ex) {\r
700                         throw new RuntimeException("JAXP Configuration Exception.", ex);\r
701                 } catch (SAXException ex) {\r
702                         throw new RuntimeException("JAXP Configuration Exception.", ex);\r
703                 }\r
704 \r
705                 try {\r
706                         final DocInfo[] result = new DocInfo[1];\r
707                         saxParser.parse(is, new DefaultHandler() {\r
708                                 private int elmCount = 0;\r
709                                 @Override\r
710                                 public void startElement(String uri, String localName,\r
711                                                 String qName, Attributes attributes)\r
712                                                 throws SAXException {\r
713                                         if (elmCount == 0) {\r
714                                                 String version = attributes.getValue("version");\r
715                                                 String namespace = attributes.getValue("xmlns");\r
716                                                 DocInfo docInfo = new DocInfo();\r
717                                                 docInfo.setFirstElementName(qName);\r
718                                                 docInfo.setVersion(version);\r
719                                                 docInfo.setNamespace(namespace);\r
720                                                 result[0] = docInfo;\r
721                                         }\r
722                                         elmCount++;\r
723                                 }\r
724                         });\r
725                         return result[0];\r
726 \r
727                 } catch (SAXException ex) {\r
728                         logger.log(Level.INFO, "character.xml check failed.", ex);\r
729                 }\r
730                 return null;\r
731         }\r
732 \r
733 \r
734         protected void saveCharacterDataToXML(CharacterData characterData)\r
735                         throws IOException {\r
736                 if (characterData == null) {\r
737                         throw new IllegalArgumentException();\r
738                 }\r
739 \r
740                 characterData.checkWritable();\r
741                 if (!characterData.isValid()) {\r
742                         throw new IOException("invalid profile: " + characterData);\r
743                 }\r
744 \r
745                 URI docBase = characterData.getDocBase();\r
746                 if (!"file".equals(docBase.getScheme())) {\r
747                         throw new IOException("ファイル以外はサポートしていません: " + docBase);\r
748                 }\r
749 \r
750                 // XML形式で保存(メモリへ)\r
751                 ByteArrayOutputStream bos = new ByteArrayOutputStream();\r
752                 try {\r
753                         characterDataXmlWriter.writeXMLCharacterData(characterData, bos);\r
754                 } finally {\r
755                         bos.close();\r
756                 }\r
757 \r
758                 // 成功したら実際にファイルに出力\r
759                 File characterPropXML = new File(docBase);\r
760                 File baseDir = characterPropXML.getParentFile();\r
761                 if (!baseDir.exists()) {\r
762                         if (!baseDir.mkdirs()) {\r
763                                 logger.log(Level.WARNING, "can't create directory. " + baseDir);\r
764                         }\r
765                 }\r
766 \r
767                 FileOutputStream fos = new FileOutputStream(characterPropXML);\r
768                 try {\r
769                         fos.write(bos.toByteArray());\r
770                 } finally {\r
771                         fos.close();\r
772                 }\r
773         }\r
774 \r
775         public void saveFavorites(CharacterData characterData) throws IOException {\r
776                 if (characterData == null) {\r
777                         throw new IllegalArgumentException();\r
778                 }\r
779 \r
780                 // xml形式\r
781                 UserData favoritesData = getFavoritesUserData(characterData);\r
782                 OutputStream os = favoritesData.getOutputStream();\r
783                 try {\r
784                         saveFavorites(characterData, os);\r
785 \r
786                 } finally {\r
787                         os.close();\r
788                 }\r
789         }\r
790 \r
791         private UserData getFavoritesUserData(CharacterData characterData) {\r
792                 if (characterData == null) {\r
793                         throw new IllegalArgumentException();\r
794                 }\r
795 \r
796                 // xml形式の場合、キャラクターディレクトリ上に設定する.\r
797                 URI docBase = characterData.getDocBase();\r
798                 File characterDir = new File(docBase).getParentFile();\r
799                 return new FileUserData(new File(characterDir, "favorites.xml"));\r
800         }\r
801 \r
802         protected void saveFavorites(CharacterData characterData,\r
803                         OutputStream outstm) throws IOException {\r
804                 if (characterData == null || outstm == null) {\r
805                         throw new IllegalArgumentException();\r
806                 }\r
807 \r
808                 Document doc;\r
809                 try {\r
810                         DocumentBuilderFactory factory = DocumentBuilderFactory\r
811                                         .newInstance();\r
812                         factory.setNamespaceAware(true);\r
813                         DocumentBuilder builder = factory.newDocumentBuilder();\r
814                         doc = builder.newDocument();\r
815 \r
816                 } catch (ParserConfigurationException ex) {\r
817                         throw new RuntimeException("JAXP Configuration Exception.", ex);\r
818                 }\r
819 \r
820                 Element root = doc.createElementNS(NS, "partssets");\r
821 \r
822                 root.setAttribute("xmlns:xml", XMLConstants.XML_NS_URI);\r
823                 root.setAttribute("xmlns:xsi",\r
824                                 "http://www.w3.org/2001/XMLSchema-instance");\r
825                 root.setAttribute("xsi:schemaLocation", NS + " partsset.xsd");\r
826                 doc.appendChild(root);\r
827 \r
828                 // presetsのelementを構築する.(Presetは除く)\r
829                 characterDataXmlWriter.writePartsSetElements(doc, root, characterData,\r
830                                 false, true);\r
831 \r
832                 // output xml\r
833                 TransformerFactory txFactory = TransformerFactory.newInstance();\r
834                 txFactory.setAttribute("indent-number", Integer.valueOf(4));\r
835                 Transformer tfmr;\r
836                 try {\r
837                         tfmr = txFactory.newTransformer();\r
838                 } catch (TransformerConfigurationException ex) {\r
839                         throw new RuntimeException("JAXP Configuration Failed.", ex);\r
840                 }\r
841                 tfmr.setOutputProperty(OutputKeys.INDENT, "yes");\r
842 \r
843                 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4504745\r
844                 final String encoding = "UTF-8";\r
845                 tfmr.setOutputProperty("encoding", encoding);\r
846                 try {\r
847                         tfmr.transform(new DOMSource(doc), new StreamResult(\r
848                                         new OutputStreamWriter(outstm, Charset.forName(encoding))));\r
849 \r
850                 } catch (TransformerException ex) {\r
851                         IOException ex2 = new IOException("XML Convert failed.");\r
852                         ex2.initCause(ex);\r
853                         throw ex2;\r
854                 }\r
855         }\r
856 \r
857 \r
858         /**\r
859          * お気に入り(Favorites)を読み込む.<br>\r
860          * 現在のパーツセットに追加する形で読み込まれ、同じパーツセットIDのものは上書きされます.<br>\r
861          * \r
862          * @param characterData\r
863          *            キャラクターデータ\r
864          * @throws IOException\r
865          *             読み込みに失敗した場合\r
866          */\r
867         public void loadFavorites(CharacterData characterData) throws IOException {\r
868                 if (characterData == null) {\r
869                         throw new IllegalArgumentException();\r
870                 }\r
871 \r
872                 UserData favoritesXml = getFavoritesUserData(characterData);\r
873                 if (favoritesXml.exists()) {\r
874                         InputStream is = favoritesXml.openStream();\r
875                         try {\r
876                                 characterDataXmlReader.loadPartsSet(characterData, is);\r
877 \r
878                         } finally {\r
879                                 is.close();\r
880                         }\r
881                 }\r
882         }\r
883 \r
884 \r
885         /**\r
886          * 既存のキャラクター定義を削除する.<br>\r
887          * 有効なdocBaseがあり、そのxmlファイルが存在するものについて、削除を行う.<br>\r
888          * forceRemoveがtrueでない場合はキャラクター定義 character.xmlファイルの拡張子を\r
889          * リネームすることでキャラクター定義として認識させなくする.<br>\r
890          * forceRevmoeがtrueの場合は実際にファイルを削除する.<br>\r
891          * character.xml、favorites、workingsetのキャッシュも削除される.<br>\r
892          * \r
893          * @param cd\r
894          *            キャラクター定義\r
895          * @param forceRemove\r
896          *            ファイルを削除する場合はtrue、リネームして無効にするだけならfalse\r
897          * @throws IOException\r
898          *             削除またはリネームできなかった場合\r
899          */\r
900         public void remove(CharacterData cd, boolean forceRemove)\r
901                         throws IOException {\r
902                 if (cd == null || cd.getDocBase() == null) {\r
903                         throw new IllegalArgumentException();\r
904                 }\r
905 \r
906                 URI docBase = cd.getDocBase();\r
907                 File xmlFile = new File(docBase);\r
908                 if (!xmlFile.exists() || !xmlFile.isFile()) {\r
909                         // すでに存在しない場合\r
910                         return;\r
911                 }\r
912 \r
913                 // favories.xmlの削除\r
914                 if (forceRemove) {\r
915                         UserData[] favoritesDatas = new UserData[]{getFavoritesUserData(cd)};\r
916                         for (UserData favoriteData : favoritesDatas) {\r
917                                 if (favoriteData != null && favoriteData.exists()) {\r
918                                         logger.log(Level.INFO, "remove file: " + favoriteData);\r
919                                         favoriteData.delete();\r
920                                 }\r
921                         }\r
922                 }\r
923 \r
924                 // ワーキングセットの削除\r
925                 UserData workingSetSer = MainFrame.getWorkingSetUserData(cd, true);\r
926                 if (workingSetSer != null && workingSetSer.exists()) {\r
927                         logger.log(Level.INFO, "remove file: " + workingSetSer);\r
928                         workingSetSer.delete();\r
929                 }\r
930 \r
931                 // xmlファイルの拡張子を変更することでキャラクター定義として認識させない.\r
932                 // (削除に失敗するケースに備えて先にリネームする.)\r
933                 String suffix = "." + System.currentTimeMillis() + ".deleted";\r
934                 File bakFile = new File(xmlFile.getPath() + suffix);\r
935                 if (!xmlFile.renameTo(bakFile)) {\r
936                         throw new IOException("can not rename configuration file.:"\r
937                                         + xmlFile);\r
938                 }\r
939 \r
940                 // ディレクトリ\r
941                 File baseDir = xmlFile.getParentFile();\r
942 \r
943                 if (!forceRemove) {\r
944                         // 削除されたディレクトリであることを識別できるようにディレクトリ名も変更する.\r
945                         File parentBak = new File(baseDir.getPath() + suffix);\r
946                         if (!baseDir.renameTo(parentBak)) {\r
947                                 throw new IOException("can't rename directory. " + baseDir);\r
948                         }\r
949 \r
950                 } else {\r
951                         // 完全に削除する\r
952                         removeRecursive(baseDir);\r
953                 }\r
954         }\r
955 \r
956         /**\r
957          * 指定したファイルを削除します.<br>\r
958          * 指定したファイルがディレクトリを示す場合、このディレクトリを含む配下のすべてのファイルとディレクトリを削除します.<br>\r
959          * \r
960          * @param file\r
961          *            ファイル、またはディレクトリ\r
962          * @throws IOException\r
963          *             削除できない場合\r
964          */\r
965         protected void removeRecursive(File file) throws IOException {\r
966                 if (file == null) {\r
967                         throw new IllegalArgumentException();\r
968                 }\r
969                 if (!file.exists()) {\r
970                         return;\r
971                 }\r
972                 if (file.isDirectory()) {\r
973                         for (File child : file.listFiles()) {\r
974                                 removeRecursive(child);\r
975                         }\r
976                 }\r
977                 if (!file.delete()) {\r
978                         throw new IOException("can't delete file. " + file);\r
979                 }\r
980         }\r
981 \r
982         protected Iterable<Node> iterable(final NodeList nodeList) {\r
983                 final int mx;\r
984                 if (nodeList == null) {\r
985                         mx = 0;\r
986                 } else {\r
987                         mx = nodeList.getLength();\r
988                 }\r
989                 return new Iterable<Node>() {\r
990                         public Iterator<Node> iterator() {\r
991                                 return new Iterator<Node>() {\r
992                                         private int idx = 0;\r
993                                         public boolean hasNext() {\r
994                                                 return idx < mx;\r
995                                         }\r
996                                         public Node next() {\r
997                                                 if (idx >= mx) {\r
998                                                         throw new NoSuchElementException();\r
999                                                 }\r
1000                                                 return nodeList.item(idx++);\r
1001                                         }\r
1002                                         public void remove() {\r
1003                                                 throw new UnsupportedOperationException();\r
1004                                         }\r
1005                                 };\r
1006                         }\r
1007                 };\r
1008         }\r
1009 \r
1010         protected Schema loadSchema(DocInfo docInfo) throws IOException {\r
1011                 if (docInfo == null) {\r
1012                         throw new IllegalArgumentException();\r
1013                 }\r
1014 \r
1015                 String schemaName = null;\r
1016                 if ("character".equals(docInfo.getFirstElementName())) {\r
1017                         if ("http://com.exmaple/charactermanaj".equals(docInfo\r
1018                                         .getNamespace())) {\r
1019                                 schemaName = CHARACTER_XML_SCHEMA_0_8;\r
1020                         } else if ("http://charactermanaj.sourceforge.jp/schema/charactermanaj"\r
1021                                         .equals(docInfo.getNamespace())) {\r
1022                                 schemaName = CHARACTER_XML_SCHEMA;\r
1023                         }\r
1024                 } else if ("partssets".equals(docInfo.getFirstElementName())) {\r
1025                         if ("http://com.exmaple/charactermanaj".equals(docInfo\r
1026                                         .getNamespace())) {\r
1027                                 schemaName = PARTSSET_XML_SCHEMA_0_8;\r
1028                         } else if ("http://charactermanaj.sourceforge.jp/schema/charactermanaj"\r
1029                                         .equals(docInfo.getNamespace())) {\r
1030                                 schemaName = PARTSSET_XML_SCHEMA;\r
1031                         }\r
1032                 }\r
1033                 if (schemaName == null) {\r
1034                         throw new IOException("unsupported namespace: " + docInfo);\r
1035                 }\r
1036 \r
1037                 Schema schema = schemaMap.get(schemaName);\r
1038                 if (schema != null) {\r
1039                         return schema;\r
1040                 }\r
1041 \r
1042                 URL schemaURL = null;\r
1043                 try {\r
1044                         SchemaFactory schemaFactory = SchemaFactory\r
1045                                         .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);\r
1046                         schemaFactory.setErrorHandler(errorHandler);\r
1047                         schemaURL = getEmbeddedResourceURL(schemaName);\r
1048                         schema = schemaFactory.newSchema(schemaURL);\r
1049                         schemaMap.put(schemaName, schema);\r
1050                         return schema;\r
1051 \r
1052                 } catch (Exception ex) {\r
1053                         throw new RuntimeException("schema creation failed. :" + schemaURL,\r
1054                                         ex);\r
1055                 }\r
1056         }\r
1057 \r
1058         protected URL getEmbeddedResourceURL(String schemaName) {\r
1059                 return this.getClass().getResource(schemaName);\r
1060         }\r
1061 \r
1062         protected XPath createXPath(DocInfo docInfo) {\r
1063                 if (docInfo == null) {\r
1064                         throw new IllegalArgumentException();\r
1065                 }\r
1066 \r
1067                 final String namespace;\r
1068                 if (docInfo.getNamespace() != null\r
1069                                 && docInfo.getNamespace().length() > 0) {\r
1070                         namespace = docInfo.getNamespace();\r
1071                 } else {\r
1072                         namespace = NS;\r
1073                 }\r
1074 \r
1075                 XPathFactory xpathFactory = XPathFactory.newInstance();\r
1076                 XPath xpath = xpathFactory.newXPath();\r
1077                 xpath.setNamespaceContext(new NamespaceContext() {\r
1078                         public String getNamespaceURI(String prefix) {\r
1079                                 if (prefix == null) {\r
1080                                         throw new IllegalArgumentException();\r
1081                                 }\r
1082                                 if (prefix.equals("pre")) {\r
1083                                         return namespace;\r
1084                                 }\r
1085                                 if (prefix.equals("xml")) {\r
1086                                         return XMLConstants.XML_NS_URI;\r
1087                                 }\r
1088                                 return XMLConstants.NULL_NS_URI;\r
1089                         }\r
1090                         public Iterator<?> getPrefixes(String namespaceURI) {\r
1091                                 throw new UnsupportedOperationException();\r
1092                         }\r
1093 \r
1094                         public String getPrefix(String namespaceURI) {\r
1095                                 throw new UnsupportedOperationException();\r
1096                         }\r
1097                 });\r
1098                 return xpath;\r
1099         }\r
1100 \r
1101         /**\r
1102          * サンプルピクチャを読み込む.<br>\r
1103          * ピクチャが存在しなければnullを返す. キャラクター定義がValidでない場合は常にnullを返す.<br>\r
1104          * \r
1105          * @param characterData\r
1106          *            キャラクター定義、null不可\r
1107          * @param loader\r
1108          *            イメージのローダー、null不可\r
1109          * @return ピクチャのイメージ、もしくはnull\r
1110          * @throws IOException\r
1111          *             ピクチャの読み取りに失敗した場合\r
1112          */\r
1113         public BufferedImage loadSamplePicture(CharacterData characterData,\r
1114                         ImageLoader loader) throws IOException {\r
1115                 if (characterData == null || loader == null) {\r
1116                         throw new IllegalArgumentException();\r
1117                 }\r
1118                 if (!characterData.isValid()) {\r
1119                         return null;\r
1120                 }\r
1121 \r
1122                 File sampleImageFile = getSamplePictureFile(characterData);\r
1123                 if (sampleImageFile != null && sampleImageFile.exists()) {\r
1124                         LoadedImage loadedImage = loader.load(new FileImageResource(\r
1125                                         sampleImageFile));\r
1126                         return loadedImage.getImage();\r
1127                 }\r
1128                 return null;\r
1129         }\r
1130 \r
1131         /**\r
1132          * キャラクターのサンプルピクチャが登録可能であるか?<br>\r
1133          * キャラクターデータが有効であり、且つ、ファイルの書き込みが可能であればtrueを返す.<br>\r
1134          * キャラクターデータがnullもしくは無効であるか、ファイルプロトコルでないか、ファイルが書き込み禁止であればfalseょ返す.<br>\r
1135          * \r
1136          * @param characterData\r
1137          *            キャラクターデータ\r
1138          * @return 書き込み可能であればtrue、そうでなければfalse\r
1139          */\r
1140         public boolean canSaveSamplePicture(CharacterData characterData) {\r
1141                 if (characterData == null || !characterData.isValid()) {\r
1142                         return false;\r
1143                 }\r
1144                 File sampleImageFile = getSamplePictureFile(characterData);\r
1145                 if (sampleImageFile != null) {\r
1146                         if (sampleImageFile.exists() && sampleImageFile.canWrite()) {\r
1147                                 return true;\r
1148                         }\r
1149                         if (!sampleImageFile.exists()) {\r
1150                                 File parentDir = sampleImageFile.getParentFile();\r
1151                                 if (parentDir != null) {\r
1152                                         return parentDir.canWrite();\r
1153                                 }\r
1154                         }\r
1155                 }\r
1156                 return false;\r
1157         }\r
1158 \r
1159         /**\r
1160          * サンプルピクチャとして認識されるファイル位置を返す.<br>\r
1161          * ファイルが実在するかは問わない.<br>\r
1162          * DocBaseが未設定であるか、ファィルプロトコルとして返せない場合はnullを返す.<br>\r
1163          * \r
1164          * @param characterData\r
1165          *            キャラクター定義\r
1166          * @return サンプルピクチャの保存先のファイル位置、もしくはnull\r
1167          */\r
1168         protected File getSamplePictureFile(CharacterData characterData) {\r
1169                 if (characterData == null) {\r
1170                         throw new IllegalArgumentException();\r
1171                 }\r
1172                 URI docBase = characterData.getDocBase();\r
1173                 if (docBase != null && "file".endsWith(docBase.getScheme())) {\r
1174                         File docBaseFile = new File(docBase);\r
1175                         return new File(docBaseFile.getParentFile(), SAMPLE_IMAGE_FILENAME);\r
1176                 }\r
1177                 return null;\r
1178         }\r
1179 \r
1180         /**\r
1181          * サンプルピクチャを保存する.\r
1182          * \r
1183          * @param characterData\r
1184          *            キャラクターデータ\r
1185          * @param samplePicture\r
1186          *            サンプルピクチャ\r
1187          * @throws IOException\r
1188          *             保存に失敗した場合\r
1189          */\r
1190         public void saveSamplePicture(CharacterData characterData,\r
1191                         BufferedImage samplePicture) throws IOException {\r
1192                 if (!canSaveSamplePicture(characterData)) {\r
1193                         throw new IOException("can not write a sample picture.:"\r
1194                                         + characterData);\r
1195                 }\r
1196                 File sampleImageFile = getSamplePictureFile(characterData); // canSaveSamplePictureで書き込み先検証済み\r
1197 \r
1198                 if (samplePicture != null) {\r
1199                         // 登録または更新\r
1200 \r
1201                         // pngで保存するので背景色は透過になるが、一応、コードとしては入れておく。\r
1202                         AppConfig appConfig = AppConfig.getInstance();\r
1203                         Color sampleImageBgColor = appConfig.getSampleImageBgColor();\r
1204 \r
1205                         ImageSaveHelper imageSaveHelper = new ImageSaveHelper();\r
1206                         imageSaveHelper.savePicture(samplePicture, sampleImageBgColor,\r
1207                                         sampleImageFile, null);\r
1208 \r
1209                 } else {\r
1210                         // 削除\r
1211                         if (sampleImageFile.exists()) {\r
1212                                 if (!sampleImageFile.delete()) {\r
1213                                         throw new IOException("sample pucture delete failed. :"\r
1214                                                         + sampleImageFile);\r
1215                                 }\r
1216                         }\r
1217                 }\r
1218         }\r
1219 \r
1220         /**\r
1221          * パーツ管理情報をDocBaseと同じフォルダ上のparts-info.xmlに書き出す.<br>\r
1222          * XML生成中に失敗した場合は既存の管理情報は残される.<br>\r
1223          * (管理情報の書き込み中にI/O例外が発生した場合は管理情報は破壊される.)<br>\r
1224          * \r
1225          * @param docBase\r
1226          *            character.xmlの位置\r
1227          * @param partsManageData\r
1228          *            パーツ管理情報\r
1229          * @throws IOException\r
1230          *             出力に失敗した場合\r
1231          */\r
1232         public void savePartsManageData(URI docBase, PartsManageData partsManageData)\r
1233                         throws IOException {\r
1234                 if (docBase == null || partsManageData == null) {\r
1235                         throw new IllegalArgumentException();\r
1236                 }\r
1237 \r
1238                 if (!"file".equals(docBase.getScheme())) {\r
1239                         throw new IOException("ファイル以外はサポートしていません: " + docBase);\r
1240                 }\r
1241                 File docBaseFile = new File(docBase);\r
1242                 File baseDir = docBaseFile.getParentFile();\r
1243 \r
1244                 // データからXMLを構築してストリームに出力する.\r
1245                 // 完全に成功したXMLのみ書き込むようにするため、一旦バッファする。\r
1246                 ByteArrayOutputStream bos = new ByteArrayOutputStream();\r
1247                 try {\r
1248                         savePartsManageData(partsManageData, bos);\r
1249                 } finally {\r
1250                         bos.close();\r
1251                 }\r
1252 \r
1253                 // バッファされたXMLデータを実際のファイルに書き込む\r
1254                 File partsInfoXML = new File(baseDir, "parts-info.xml");\r
1255                 FileOutputStream os = new FileOutputStream(partsInfoXML);\r
1256                 try {\r
1257                         os.write(bos.toByteArray());\r
1258                 } finally {\r
1259                         os.close();\r
1260                 }\r
1261         }\r
1262 \r
1263         /**\r
1264          * パーツ管理情報をXMLとしてストリームに書き出す.<br>\r
1265          * \r
1266          * @param partsManageData\r
1267          *            パーツ管理データ\r
1268          * @param outstm\r
1269          *            出力先ストリーム\r
1270          * @throws IOException\r
1271          *             出力に失敗した場合\r
1272          */\r
1273         public void savePartsManageData(PartsManageData partsManageData,\r
1274                         OutputStream outstm) throws IOException {\r
1275                 if (partsManageData == null || outstm == null) {\r
1276                         throw new IllegalArgumentException();\r
1277                 }\r
1278 \r
1279                 Document doc;\r
1280                 try {\r
1281                         DocumentBuilderFactory factory = DocumentBuilderFactory\r
1282                                         .newInstance();\r
1283                         factory.setNamespaceAware(true);\r
1284                         DocumentBuilder builder = factory.newDocumentBuilder();\r
1285                         doc = builder.newDocument();\r
1286 \r
1287                 } catch (ParserConfigurationException ex) {\r
1288                         throw new RuntimeException("JAXP Configuration Exception.", ex);\r
1289                 }\r
1290 \r
1291                 Locale locale = Locale.getDefault();\r
1292                 String lang = locale.getLanguage();\r
1293 \r
1294                 Element root = doc.createElementNS(NS_PARTSDEF, "parts-definition");\r
1295 \r
1296                 root.setAttribute("xmlns:xml", XMLConstants.XML_NS_URI);\r
1297                 root.setAttribute("xmlns:xsi",\r
1298                                 "http://www.w3.org/2001/XMLSchema-instance");\r
1299                 root.setAttribute("xsi:schemaLocation", NS_PARTSDEF\r
1300                                 + " parts-definition.xsd");\r
1301                 doc.appendChild(root);\r
1302 \r
1303                 // 作者情報を取得する\r
1304                 Collection<PartsAuthorInfo> partsAuthors = partsManageData\r
1305                                 .getAuthorInfos();\r
1306                 for (PartsAuthorInfo partsAuthorInfo : partsAuthors) {\r
1307                         String author = partsAuthorInfo.getAuthor();\r
1308                         if (author == null || author.length() == 0) {\r
1309                                 continue;\r
1310                         }\r
1311 \r
1312                         // 作者情報の登録\r
1313                         Element nodeAuthor = doc.createElementNS(NS_PARTSDEF, "author");\r
1314                         Element nodeAuthorName = doc.createElementNS(NS_PARTSDEF, "name");\r
1315                         Attr attrLang = doc.createAttributeNS(XMLConstants.XML_NS_URI,\r
1316                                         "lang");\r
1317                         attrLang.setValue(lang);\r
1318                         nodeAuthorName.setAttributeNodeNS(attrLang);\r
1319                         nodeAuthorName.setTextContent(author);\r
1320                         nodeAuthor.appendChild(nodeAuthorName);\r
1321 \r
1322                         String homepageURL = partsAuthorInfo.getHomePage();\r
1323                         if (homepageURL != null && homepageURL.length() > 0) {\r
1324                                 Element nodeHomepage = doc.createElementNS(NS_PARTSDEF,\r
1325                                                 "home-page");\r
1326                                 Attr attrHomepageLang = doc.createAttributeNS(\r
1327                                                 XMLConstants.XML_NS_URI, "lang");\r
1328                                 attrHomepageLang.setValue(lang);\r
1329                                 nodeHomepage.setAttributeNodeNS(attrHomepageLang);\r
1330                                 nodeHomepage.setTextContent(homepageURL);\r
1331                                 nodeAuthor.appendChild(nodeHomepage);\r
1332                         }\r
1333 \r
1334                         root.appendChild(nodeAuthor);\r
1335 \r
1336                         Collection<PartsKey> partsKeys = partsManageData\r
1337                                         .getPartsKeysByAuthor(author);\r
1338 \r
1339                         // ダウンロード別にパーツキーの集約\r
1340                         HashMap<String, List<PartsKey>> downloadMap = new HashMap<String, List<PartsKey>>();\r
1341                         for (PartsKey partsKey : partsKeys) {\r
1342                                 PartsManageData.PartsVersionInfo versionInfo = partsManageData\r
1343                                                 .getVersionStrict(partsKey);\r
1344                                 String downloadURL = versionInfo.getDownloadURL();\r
1345                                 if (downloadURL == null) {\r
1346                                         downloadURL = "";\r
1347                                 }\r
1348                                 List<PartsKey> partsKeyGrp = downloadMap.get(downloadURL);\r
1349                                 if (partsKeyGrp == null) {\r
1350                                         partsKeyGrp = new ArrayList<PartsKey>();\r
1351                                         downloadMap.put(downloadURL, partsKeyGrp);\r
1352                                 }\r
1353                                 partsKeyGrp.add(partsKey);\r
1354                         }\r
1355 \r
1356                         // ダウンロード別にパーツ情報の登録\r
1357                         ArrayList<String> downloadURLs = new ArrayList<String>(\r
1358                                         downloadMap.keySet());\r
1359                         Collections.sort(downloadURLs);\r
1360 \r
1361                         for (String downloadURL : downloadURLs) {\r
1362                                 List<PartsKey> partsKeyGrp = downloadMap.get(downloadURL);\r
1363                                 Collections.sort(partsKeyGrp);\r
1364 \r
1365                                 Element nodeDownload = doc.createElementNS(NS_PARTSDEF,\r
1366                                                 "download-url");\r
1367                                 nodeDownload.setTextContent(downloadURL);\r
1368                                 root.appendChild(nodeDownload);\r
1369 \r
1370                                 for (PartsKey partsKey : partsKeyGrp) {\r
1371                                         PartsManageData.PartsVersionInfo versionInfo = partsManageData\r
1372                                                         .getVersionStrict(partsKey);\r
1373 \r
1374                                         Element nodeParts = doc.createElementNS(NS_PARTSDEF,\r
1375                                                         "parts");\r
1376 \r
1377                                         nodeParts.setAttribute("name", partsKey.getPartsName());\r
1378                                         if (partsKey.getCategoryId() != null) {\r
1379                                                 nodeParts.setAttribute("category",\r
1380                                                                 partsKey.getCategoryId());\r
1381                                         }\r
1382                                         if (versionInfo.getVersion() > 0) {\r
1383                                                 nodeParts.setAttribute("version",\r
1384                                                                 Double.toString(versionInfo.getVersion()));\r
1385                                         }\r
1386 \r
1387                                         String localizedName = partsManageData\r
1388                                                         .getLocalizedName(partsKey);\r
1389                                         if (localizedName != null\r
1390                                                         && localizedName.trim().length() > 0) {\r
1391                                                 Element nodeLocalizedName = doc.createElementNS(\r
1392                                                                 NS_PARTSDEF, "local-name");\r
1393                                                 Attr attrLocalizedNameLang = doc.createAttributeNS(\r
1394                                                                 XMLConstants.XML_NS_URI, "lang");\r
1395                                                 attrLocalizedNameLang.setValue(lang);\r
1396                                                 nodeLocalizedName\r
1397                                                                 .setAttributeNodeNS(attrLocalizedNameLang);\r
1398                                                 nodeLocalizedName.setTextContent(localizedName);\r
1399                                                 nodeParts.appendChild(nodeLocalizedName);\r
1400                                         }\r
1401 \r
1402                                         root.appendChild(nodeParts);\r
1403                                 }\r
1404                         }\r
1405                 }\r
1406 \r
1407                 // output xml\r
1408                 TransformerFactory txFactory = TransformerFactory.newInstance();\r
1409                 txFactory.setAttribute("indent-number", Integer.valueOf(4));\r
1410                 Transformer tfmr;\r
1411                 try {\r
1412                         tfmr = txFactory.newTransformer();\r
1413                 } catch (TransformerConfigurationException ex) {\r
1414                         throw new RuntimeException("JAXP Configuration Failed.", ex);\r
1415                 }\r
1416                 tfmr.setOutputProperty(OutputKeys.INDENT, "yes");\r
1417 \r
1418                 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4504745\r
1419                 final String encoding = "UTF-8";\r
1420                 tfmr.setOutputProperty("encoding", encoding);\r
1421                 try {\r
1422                         tfmr.transform(new DOMSource(doc), new StreamResult(\r
1423                                         new OutputStreamWriter(outstm, Charset.forName(encoding))));\r
1424 \r
1425                 } catch (TransformerException ex) {\r
1426                         IOException ex2 = new IOException("XML Convert failed.");\r
1427                         ex2.initCause(ex);\r
1428                         throw ex2;\r
1429                 }\r
1430         }\r
1431 \r
1432         /**\r
1433          * 指定したDocBaseと同じフォルダにあるparts-info.xmlからパーツ管理情報を取得して返す.<br>\r
1434          * ファイルが存在しない場合は空のインスタンスを返す.<br>\r
1435          * 返されるインスタンスは編集可能です.<br>\r
1436          * \r
1437          * @param docBase\r
1438          *            character.xmlの位置\r
1439          * @return パーツ管理情報、存在しない場合は空のインスタンス\r
1440          * @throws IOException\r
1441          *             読み込み中に失敗した場合\r
1442          */\r
1443         public PartsManageData loadPartsManageData(URI docBase) throws IOException {\r
1444                 if (docBase == null) {\r
1445                         throw new IllegalArgumentException();\r
1446                 }\r
1447                 if (!"file".equals(docBase.getScheme())) {\r
1448                         throw new IOException("ファイル以外はサポートしていません。:" + docBase);\r
1449                 }\r
1450                 File docBaseFile = new File(docBase);\r
1451                 File baseDir = docBaseFile.getParentFile();\r
1452 \r
1453                 // パーツ管理情報ファイルの確認\r
1454                 final File partsInfoXML = new File(baseDir, "parts-info.xml");\r
1455                 if (!partsInfoXML.exists()) {\r
1456                         // ファイルが存在しなければ空を返す.\r
1457                         return new PartsManageData();\r
1458                 }\r
1459 \r
1460                 PartsManageData partsManageData;\r
1461                 InputStream is = new FileInputStream(partsInfoXML);\r
1462                 try {\r
1463                         partsManageData = loadPartsManageData(is);\r
1464                 } finally {\r
1465                         is.close();\r
1466                 }\r
1467                 return partsManageData;\r
1468         }\r
1469 \r
1470         public PartsManageData loadPartsManageData(InputStream is)\r
1471                         throws IOException {\r
1472                 if (is == null) {\r
1473                         throw new IllegalArgumentException();\r
1474                 }\r
1475 \r
1476                 // パーツ管理情報\r
1477                 final PartsManageData partsManageData = new PartsManageData();\r
1478 \r
1479                 // SAXParserの準備\r
1480                 SAXParser saxParser;\r
1481                 try {\r
1482                         SAXParserFactory saxPartserFactory = SAXParserFactory.newInstance();\r
1483                         saxPartserFactory.setNamespaceAware(true);\r
1484                         saxParser = saxPartserFactory.newSAXParser();\r
1485                 } catch (Exception ex) {\r
1486                         throw new RuntimeException("JAXP Configuration failed.", ex);\r
1487                 }\r
1488 \r
1489                 // デフォルトのロケールから言語を取得\r
1490                 final Locale locale = Locale.getDefault();\r
1491                 final String lang = locale.getLanguage();\r
1492 \r
1493                 try {\r
1494                         // 要素のスタック\r
1495                         final LinkedList<String> stack = new LinkedList<String>();\r
1496 \r
1497                         // DOMではなくSAXで読み流す.\r
1498                         saxParser.parse(is, new DefaultHandler() {\r
1499                                 private StringBuilder buf = new StringBuilder();\r
1500 \r
1501                                 private PartsAuthorInfo partsAuthorInfo;\r
1502 \r
1503                                 private String authorName;\r
1504                                 private String homepageURL;\r
1505                                 private String authorNameLang;\r
1506                                 private String homepageLang;\r
1507                                 private String downloadURL;\r
1508 \r
1509                                 private String partsLocalNameLang;\r
1510                                 private String partsLocalName;\r
1511                                 private String partsCategoryId;\r
1512                                 private String partsName;\r
1513                                 private double partsVersion;\r
1514 \r
1515                                 @Override\r
1516                                 public void startDocument() throws SAXException {\r
1517                                         logger.log(Level.FINEST, "parts-info : start");\r
1518                                 }\r
1519 \r
1520                                 @Override\r
1521                                 public void endDocument() throws SAXException {\r
1522                                         logger.log(Level.FINEST, "parts-info : end");\r
1523                                 }\r
1524 \r
1525                                 @Override\r
1526                                 public void characters(char[] ch, int start, int length)\r
1527                                                 throws SAXException {\r
1528                                         buf.append(ch, start, length);\r
1529                                 }\r
1530                                 @Override\r
1531                                 public void startElement(String uri, String localName,\r
1532                                                 String qName, Attributes attributes)\r
1533                                                 throws SAXException {\r
1534                                         stack.addFirst(qName);\r
1535                                         int mx = stack.size();\r
1536                                         if (mx >= 2 && stack.get(1).equals("parts")) {\r
1537                                                 if ("local-name".equals(qName)) {\r
1538                                                         partsLocalNameLang = attributes.getValue(\r
1539                                                                         XMLConstants.XML_NS_URI, "lang");\r
1540                                                 }\r
1541 \r
1542                                         } else if (mx >= 2 && stack.get(1).equals("author")) {\r
1543                                                 if ("name".equals(qName)) {\r
1544                                                         authorNameLang = attributes.getValue(\r
1545                                                                         XMLConstants.XML_NS_URI, "lang");\r
1546 \r
1547                                                 } else if ("home-page".equals(qName)) {\r
1548                                                         homepageLang = attributes.getValue(\r
1549                                                                         XMLConstants.XML_NS_URI, "lang");\r
1550                                                 }\r
1551 \r
1552                                         } else if ("author".equals(qName)) {\r
1553                                                 partsAuthorInfo = null;\r
1554                                                 authorName = null;\r
1555                                                 authorNameLang = null;\r
1556                                                 homepageURL = null;\r
1557                                                 homepageLang = null;\r
1558 \r
1559                                         } else if ("download-url".equals(qName)) {\r
1560                                                 downloadURL = null;\r
1561 \r
1562                                         } else if ("parts".equals(qName)) {\r
1563                                                 partsLocalName = null;\r
1564                                                 partsLocalNameLang = null;\r
1565                                                 partsCategoryId = attributes.getValue("category");\r
1566                                                 partsName = attributes.getValue("name");\r
1567                                                 String strVersion = attributes.getValue("version");\r
1568                                                 try {\r
1569                                                         if (strVersion == null || strVersion.length() == 0) {\r
1570                                                                 partsVersion = 0.;\r
1571 \r
1572                                                         } else {\r
1573                                                                 partsVersion = Double.parseDouble(strVersion);\r
1574                                                                 if (partsVersion < 0) {\r
1575                                                                         partsVersion = 0;\r
1576                                                                 }\r
1577                                                         }\r
1578 \r
1579                                                 } catch (Exception ex) {\r
1580                                                         logger.log(Level.INFO,\r
1581                                                                         "parts-info.xml: invalid version."\r
1582                                                                                         + strVersion);\r
1583                                                         partsVersion = 0;\r
1584                                                 }\r
1585                                         }\r
1586 \r
1587                                         buf = new StringBuilder();\r
1588                                 }\r
1589                                 @Override\r
1590                                 public void endElement(String uri, String localName,\r
1591                                                 String qName) throws SAXException {\r
1592 \r
1593                                         int mx = stack.size();\r
1594 \r
1595                                         if (mx >= 2 && "parts".equals(stack.get(1))) {\r
1596                                                 if ("local-name".equals(qName)) {\r
1597                                                         if (partsLocalName == null\r
1598                                                                         || lang.equals(partsLocalNameLang)) {\r
1599                                                                 partsLocalName = buf.toString();\r
1600                                                         }\r
1601                                                 }\r
1602 \r
1603                                         } else if (mx >= 2 && "author".equals(stack.get(1))) {\r
1604                                                 if ("name".equals(qName)) {\r
1605                                                         if (authorName == null\r
1606                                                                         || lang.equals(authorNameLang)) {\r
1607                                                                 authorName = buf.toString();\r
1608                                                         }\r
1609 \r
1610                                                 } else if ("home-page".equals(qName)) {\r
1611                                                         if (homepageURL == null\r
1612                                                                         || lang.equals(homepageLang)) {\r
1613                                                                 homepageURL = buf.toString();\r
1614                                                         }\r
1615                                                 }\r
1616 \r
1617                                         } else if ("author".equals(qName)) {\r
1618                                                 logger.log(Level.FINE, "parts-info: author: "\r
1619                                                                 + authorName + " /homepage:" + homepageURL);\r
1620                                                 if (authorName != null && authorName.length() > 0) {\r
1621                                                         partsAuthorInfo = new PartsAuthorInfo();\r
1622                                                         partsAuthorInfo.setAuthor(authorName);\r
1623                                                         partsAuthorInfo.setHomePage(homepageURL);\r
1624 \r
1625                                                 } else {\r
1626                                                         partsAuthorInfo = null;\r
1627                                                 }\r
1628 \r
1629                                         } else if ("download-url".equals(qName)) {\r
1630                                                 downloadURL = buf.toString();\r
1631                                                 logger.log(Level.FINE, "parts-info: download-url: "\r
1632                                                                 + downloadURL);\r
1633 \r
1634                                         } else if ("parts".equals(qName)) {\r
1635                                                 if (logger.isLoggable(Level.FINE)) {\r
1636                                                         logger.log(Level.FINE,\r
1637                                                                         "parts-info.xml: parts-name: " + partsName\r
1638                                                                                         + " /category: " + partsCategoryId\r
1639                                                                                         + " /parts-local-name: "\r
1640                                                                                         + partsLocalName + " /version:"\r
1641                                                                                         + partsVersion);\r
1642                                                 }\r
1643 \r
1644                                                 PartsManageData.PartsVersionInfo versionInfo = new PartsManageData.PartsVersionInfo();\r
1645                                                 versionInfo.setVersion(partsVersion);\r
1646                                                 versionInfo.setDownloadURL(downloadURL);\r
1647 \r
1648                                                 PartsManageData.PartsKey partsKey = new PartsManageData.PartsKey(\r
1649                                                                 partsName, partsCategoryId);\r
1650 \r
1651                                                 partsManageData.putPartsInfo(partsKey, partsLocalName,\r
1652                                                                 partsAuthorInfo, versionInfo);\r
1653 \r
1654                                         }\r
1655                                         stack.removeFirst();\r
1656                                 }\r
1657                         });\r
1658 \r
1659                 } catch (SAXException ex) {\r
1660                         IOException ex2 = new IOException("parts-info.xml read failed.");\r
1661                         ex2.initCause(ex);\r
1662                         throw ex2;\r
1663                 }\r
1664 \r
1665                 return partsManageData;\r
1666         }\r
1667 \r
1668 \r
1669         /**\r
1670          * character.iniを読み取り、character.xmlを生成します.<br>\r
1671          * character.xmlのシリアライズされた中間ファイルも生成されます.<br>\r
1672          * すでにcharacter.xmlがある場合は上書きされます.<br>\r
1673          * 途中でエラーが発生した場合はcharacter.xmlは削除されます.<br>\r
1674          * \r
1675          * @param characterIniFile\r
1676          *            読み取るcharatcer.iniファイル\r
1677          * @param characterXmlFile\r
1678          *            書き込まれるcharacter.xmlファイル\r
1679          * @throws IOException\r
1680          *             失敗した場合\r
1681          */\r
1682         public void convertFromCharacterIni(File characterIniFile,\r
1683                         File characterXmlFile) throws IOException {\r
1684                 if (characterIniFile == null || characterXmlFile == null) {\r
1685                         throw new IllegalArgumentException();\r
1686                 }\r
1687 \r
1688                 // character.iniから、character.xmlの内容を構築する.\r
1689                 FileInputStream is = new FileInputStream(characterIniFile);\r
1690                 CharacterData characterData;\r
1691                 try {\r
1692                         CharacterDataIniReader iniReader = new CharacterDataIniReader();\r
1693                         characterData = iniReader.readCharacterDataFromIni(is);\r
1694 \r
1695                 } finally {\r
1696                         is.close();\r
1697                 }\r
1698 \r
1699                 // docBase\r
1700                 URI docBase = characterXmlFile.toURI();\r
1701                 characterData.setDocBase(docBase);\r
1702 \r
1703                 // character.xmlの書き込み\r
1704                 boolean succeeded = false;\r
1705                 try {\r
1706                         FileOutputStream outstm = new FileOutputStream(characterXmlFile);\r
1707                         try {\r
1708                                 characterDataXmlWriter.writeXMLCharacterData(characterData,\r
1709                                                 outstm);\r
1710                         } finally {\r
1711                                 outstm.close();\r
1712                         }\r
1713 \r
1714                         succeeded = true;\r
1715 \r
1716                 } finally {\r
1717                         if (!succeeded) {\r
1718                                 // 途中で失敗した場合は生成ファイルを削除しておく.\r
1719                                 try {\r
1720                                         if (characterXmlFile.exists()) {\r
1721                                                 characterXmlFile.delete();\r
1722                                         }\r
1723 \r
1724                                 } catch (Exception ex) {\r
1725                                         logger.log(Level.WARNING, "ファイルの削除に失敗しました。:"\r
1726                                                         + characterXmlFile, ex);\r
1727                                 }\r
1728                         }\r
1729                 }\r
1730         }\r
1731 \r
1732         /**\r
1733          * お勧めリンクリストが設定されていない場合(nullの場合)、デフォルトのお勧めリストを設定する.<br>\r
1734          * すでに設定されている場合(空を含む)は何もしない.<br>\r
1735          * \r
1736          * @param characterData\r
1737          *            キャラクターデータ\r
1738          */\r
1739         public void compensateRecommendationList(CharacterData characterData) {\r
1740                 if (characterData == null) {\r
1741                         throw new IllegalArgumentException();\r
1742                 }\r
1743                 if (characterData.getRecommendationURLList() != null) {\r
1744                         // 補填の必要なし\r
1745                         return;\r
1746                 }\r
1747                 CharacterDataDefaultProvider defProv = new CharacterDataDefaultProvider();\r
1748                 CharacterData defaultCd = defProv.createDefaultCharacterData();\r
1749                 characterData.setRecommendationURLList(defaultCd\r
1750                                 .getRecommendationURLList());\r
1751         }\r
1752 \r
1753 }\r