OSDN Git Service

31c717e7f0f8671879ef43c1918419a79318b447
[charactermanaj/CharacterManaJ.git] / src / charactermanaj / model / CharacterData.java
1 package charactermanaj.model;\r
2 \r
3 import java.awt.Dimension;\r
4 import java.io.File;\r
5 import java.io.IOException;\r
6 import java.io.Serializable;\r
7 import java.net.URI;\r
8 import java.util.ArrayList;\r
9 import java.util.Arrays;\r
10 import java.util.Collection;\r
11 import java.util.Collections;\r
12 import java.util.Comparator;\r
13 import java.util.HashMap;\r
14 import java.util.Iterator;\r
15 import java.util.List;\r
16 import java.util.Map;\r
17 import java.util.Properties;\r
18 import java.util.logging.Level;\r
19 import java.util.logging.Logger;\r
20 \r
21 import charactermanaj.model.io.PartsDataLoader;\r
22 \r
23 /**\r
24  * キャラクターデータ\r
25  * @author seraphy\r
26  */\r
27 public class CharacterData implements Serializable, PartsSpecResolver {\r
28 \r
29         /**\r
30          * シリアライズバージョン \r
31          */\r
32         private static final long serialVersionUID = -381763373314240953L;\r
33 \r
34         /**\r
35          * ロガー\r
36          */\r
37         private static final Logger logger = Logger.getLogger(CharacterData.class.getName());\r
38 \r
39 \r
40         /**\r
41          * キャラクターデータを表示名順にソートするための比較器.<br>\r
42          */\r
43         public static final Comparator<CharacterData> SORT_DISPLAYNAME = new Comparator<CharacterData>() {\r
44                 public int compare(CharacterData o1, CharacterData o2) {\r
45                         if (!o1.isValid() || !o2.isValid()) {\r
46                                 return o1.isValid() == o2.isValid() ? 0 : o1.isValid() ? 1 : -1;\r
47                         }\r
48                         int ret = o1.getName().compareTo(o2.getName());\r
49                         if (ret == 0) {\r
50                                 ret = o1.getId().compareTo(o2.getId());\r
51                         }\r
52                         if (ret == 0) {\r
53                                 ret = o1.getDocBase().toString().compareTo(o2.getDocBase().toString());\r
54                         }\r
55                         return ret;\r
56                 }\r
57         };\r
58 \r
59         \r
60         /**\r
61          * キャラクターデータを定義しているXMLの位置.<br>\r
62          * docBase自身はxml定義には含まれず、xmlをロードした位置を記憶するためにPersistentクラスによって設定される.<br>\r
63          */\r
64         private URI docBase;\r
65 \r
66         \r
67         \r
68         /**\r
69          * キャラクターデータの内部用ID.<br>\r
70          * キャラクターデータの構造を判定するために用いる.<br>\r
71          */\r
72         private String id;\r
73         \r
74         /**\r
75          * キャラクターデータの更新番号.<br>\r
76          * キャラクターデータの構造が変更されたことを識別するために用いる.<br>\r
77          */\r
78         private String rev;\r
79         \r
80         /**\r
81          * 表示用名\r
82          */\r
83         private String localizedName;\r
84 \r
85         /**\r
86          * 作成者\r
87          */\r
88         private String author;\r
89         \r
90         /**\r
91          * 説明\r
92          */\r
93         private String description;\r
94 \r
95         /**\r
96          * イメージサイズ\r
97          */\r
98         private Dimension imageSize;\r
99         \r
100         /**\r
101          * カテゴリ(定義順)\r
102          */\r
103         private OrderedMap<String, PartsCategory> partsCategories = OrderedMap.emptyMap();\r
104 \r
105         /**\r
106          * 雑多なプロパティ.<br>\r
107          */\r
108         private Properties properties = new Properties();\r
109         \r
110         \r
111         /**\r
112          * プリセットのマップ.<br>\r
113          * キーはプリセット自身のID、値はプリセット自身.<br>\r
114          */\r
115         private HashMap<String, PartsSet> presets = new HashMap<String, PartsSet>();\r
116         \r
117         /**\r
118          * デフォルトのプリセットのID\r
119          */\r
120         private String defaultPartsSetId;\r
121 \r
122         /**\r
123          * カラーグループの定義.<br>\r
124          */\r
125         private OrderedMap<String, ColorGroup> colorGroups = OrderedMap.emptyMap();\r
126         \r
127         \r
128         /**\r
129          * お勧めリンクリスト.<br>\r
130          * Ver0.96以前には存在しないのでnullになり得る.\r
131          */\r
132         private List<RecommendationURL> recommendationURLList;\r
133         \r
134         /**\r
135          * パーツカラーマネージャ.<br>\r
136          * (非シリアライズデータ、デシリアライズ時には新規インスタンスが作成される).<br>\r
137          */\r
138         private transient PartsColorManager partsColorMrg = new PartsColorManager(this);\r
139         \r
140         /**\r
141          * パーツデータローダー.<br>\r
142          * パーツをロードしたときに設定され、リロードするときに使用する.<br>\r
143          * パーツを一度もロードしていない場合はnull.\r
144          * (非シリアライズデータ、デシリアライズ時はnullのまま).<br>\r
145          */\r
146         private transient PartsDataLoader partsDataLoader;\r
147         \r
148         /**\r
149          * パーツイメージのセット.<br>\r
150          * (キャラクターセットはパーツイメージをもったままシリアライズされることは想定していないが、可能ではある。)\r
151          */\r
152         private Map<PartsCategory, Map<PartsIdentifier, PartsSpec>> images\r
153                 = new HashMap<PartsCategory, Map<PartsIdentifier, PartsSpec>>();\r
154 \r
155         \r
156         \r
157         /**\r
158          * シリアライズする\r
159          * @param stream 出力先\r
160          * @throws IOException 失敗\r
161          */\r
162         private void writeObject(java.io.ObjectOutputStream stream) throws IOException {\r
163                  stream.defaultWriteObject();\r
164         }\r
165 \r
166         /**\r
167          * 基本情報のみをコピーして返します.<br>\r
168          * DocBase, ID, REV, Name, Author, Description, ImageSize、および, PartsCategory, ColorGroup, PartSetのコレクションがコピーされます.<br>\r
169          * それ以外のものはコピーされません.<br>\r
170          * @return 基本情報をコピーした新しいインスタンス\r
171          */\r
172         public CharacterData duplicateBasicInfo() {\r
173                 return duplicateBasicInfo(true);\r
174         }\r
175 \r
176         /**\r
177          * 基本情報のみをコピーして返します.<br>\r
178          * DocBase, ID, REV, Name, Author, Description, ImageSize、および, PartsCategory, ColorGroupがコピーされます.<br>\r
179          * 引数のneedPartsSetがtrueの場合は,PartSetのコレクションもコピーされます.<br>\r
180          * ディレクトリの監視状態、お勧めリンクもコピーされます.<br>\r
181          * それ以外のものはコピーされません.<br>\r
182          * @param needPartsSets パーツセットもコピーする場合、falseの場合はパーツセットは空となります.\r
183          * @return 基本情報をコピーした新しいインスタンス\r
184          */\r
185         public CharacterData duplicateBasicInfo(boolean needPartsSets) {\r
186                 CharacterData cd = new CharacterData();\r
187 \r
188                 cd.setId(this.id);\r
189                 cd.setRev(this.rev);\r
190                 cd.setDocBase(this.docBase);\r
191 \r
192                 cd.setName(this.localizedName);\r
193 \r
194                 cd.setAuthor(this.author);\r
195                 cd.setDescription(this.description);\r
196                 \r
197                 cd.setImageSize((Dimension)(this.imageSize == null ? null : this.imageSize.clone()));\r
198                 \r
199                 cd.setWatchDirectory(this.isWatchDirectory());\r
200                 \r
201                 ArrayList<RecommendationURL> recommendationURLList = null;\r
202                 if (this.recommendationURLList != null) {\r
203                         recommendationURLList = new ArrayList<RecommendationURL>();\r
204                         for (RecommendationURL recommendationUrl : this.recommendationURLList) {\r
205                                 recommendationURLList.add(recommendationUrl.clone());\r
206                         }\r
207                 }\r
208                 cd.setRecommendationURLList(recommendationURLList);\r
209                 \r
210                 ArrayList<PartsCategory> partsCategories = new ArrayList<PartsCategory>();\r
211                 partsCategories.addAll(this.getPartsCategories());\r
212                 cd.setPartsCategories(partsCategories.toArray(new PartsCategory[partsCategories.size()]));\r
213 \r
214                 ArrayList<ColorGroup> colorGroups = new ArrayList<ColorGroup>();\r
215                 colorGroups.addAll(this.getColorGroups());\r
216                 cd.setColorGroups(colorGroups);\r
217 \r
218                 if (needPartsSets) {\r
219                         for (PartsSet partsSet : this.getPartsSets().values()) {\r
220                                 cd.addPartsSet(partsSet.clone());\r
221                         }\r
222                         cd.setDefaultPartsSetId(this.defaultPartsSetId);\r
223                 }\r
224                 \r
225                 return cd;\r
226         }\r
227         \r
228         /**\r
229          * キャラクターデータが同じ構造であるか?<br>\r
230          * カラーグループ、カテゴリ、レイヤーの各情報が等しければtrue、それ以外はfalse.<br>\r
231          * 上記以外の項目(コメントや作者、プリセット等)については判定しない.<br>\r
232          * サイズ、カラーグループの表示名や順序、カテゴリの順序や表示名、\r
233          * 複数アイテム可などの違いは構造の変更とみなさない.<br>\r
234          * レイヤーはレイヤーID、重ね合わせ順、対象ディレクトリの3点が変更されている場合は構造の変更とみなす.<br>\r
235          * いずれも個数そのものが変わっている場合は変更とみなす.<br>\r
236          * 自分または相手がValidでなければ常にfalseを返す.<br>\r
237          * @param other 比較対象, null可\r
238          * @return 同じ構造であればtrue、そうでなければfalse\r
239          */\r
240         public boolean isSameStructure(CharacterData other) {\r
241                 if (!this.isValid() || other == null || !other.isValid()) {\r
242                         // 自分または相手がinvalidであれば構造的には常に不一致と見なす.\r
243                         return false;\r
244                 }\r
245 \r
246                 // カラーグループが等しいか? (順序は問わない)\r
247                 // IDのみによって判定する\r
248                 ArrayList<ColorGroup> colorGroup1 = new ArrayList<ColorGroup>(getColorGroups());\r
249                 ArrayList<ColorGroup> colorGroup2 = new ArrayList<ColorGroup>(other.getColorGroups());\r
250                 if (colorGroup1.size() != colorGroup2.size()) {\r
251                         return false;\r
252                 }\r
253                 if (!colorGroup1.containsAll(colorGroup2)) {\r
254                         return false;\r
255                 }\r
256                 \r
257                 // カテゴリが等しいか? (順序は問わない)\r
258                 // IDによってのみ判定する.\r
259                 ArrayList<PartsCategory> categories1 = new ArrayList<PartsCategory>(getPartsCategories());\r
260                 ArrayList<PartsCategory> categories2 = new ArrayList<PartsCategory>(other.getPartsCategories()); \r
261                 Comparator<PartsCategory> sortCategoryId = new Comparator<PartsCategory>() {\r
262                         public int compare(PartsCategory o1, PartsCategory o2) {\r
263                                 int ret = o1.getCategoryId().compareTo(o2.getCategoryId());\r
264                                 if (ret == 0) {\r
265                                         ret = o1.getOrder() - o2.getOrder();\r
266                                 }\r
267                                 return ret;\r
268                         }\r
269                 };\r
270                 // カテゴリID順に並び替えて, IDのみを比較する.\r
271                 Collections.sort(categories1, sortCategoryId);\r
272                 Collections.sort(categories2, sortCategoryId);\r
273                 int numOfCategories = categories1.size();\r
274                 if (numOfCategories != categories2.size()) {\r
275                         // カテゴリ数不一致\r
276                         return false;\r
277                 }\r
278                 for (int idx = 0; idx < numOfCategories; idx++) {\r
279                         PartsCategory category1 = categories1.get(idx);\r
280                         PartsCategory category2 = categories2.get(idx);\r
281                         String categoryId1 = category1.getCategoryId();\r
282                         String categoryId2 = category2.getCategoryId();\r
283                         if ( !categoryId1.equals(categoryId2)) {\r
284                                 // カテゴリID不一致\r
285                                 return false;\r
286                         }\r
287                 }\r
288                 \r
289                 // レイヤーが等しいか?\r
290                 // ID、重ね順序、dirによってのみ判定する.\r
291                 int mx = categories1.size();\r
292                 for (int idx = 0; idx < mx; idx++) {\r
293                         PartsCategory category1 = categories1.get(idx);\r
294                         PartsCategory category2 = categories2.get(idx);\r
295                         \r
296                         ArrayList<Layer> layers1 = new ArrayList<Layer>(category1.getLayers());\r
297                         ArrayList<Layer> layers2 = new ArrayList<Layer>(category2.getLayers());\r
298                         \r
299                         Comparator<Layer> sortLayerId = new Comparator<Layer>() {\r
300                                 public int compare(Layer o1, Layer o2) {\r
301                                         int ret = o1.getId().compareTo(o2.getId());\r
302                                         if (ret == 0) {\r
303                                                 ret = o1.getOrder() - o2.getOrder();\r
304                                         }\r
305                                         return ret;\r
306                                 }\r
307                         };\r
308                         \r
309                         Collections.sort(layers1, sortLayerId);\r
310                         Collections.sort(layers2, sortLayerId);\r
311 \r
312                         // ID、順序、Dirで判断する.(それ以外のレイヤー情報はequalsでは比較されない)\r
313                         if ( !layers1.equals(layers2)) {\r
314                                 // レイヤー不一致\r
315                                 return false;\r
316                         }\r
317                 }\r
318                 \r
319                 return true;\r
320         }\r
321         \r
322         /**\r
323          * 引数で指定したキャラクター定義とアッパーコンパチブルであるか?<br>\r
324          * 構造が同一であるか、サイズ違い、もしくはレイヤーの順序、カテゴリの順序、\r
325          * もしくはレイヤーまたはカテゴリが増えている場合で、減っていない場合はtrueとなる.<br>\r
326          * 引数がnullの場合は常にfalseとなる.\r
327          * @param other 前の状態のキャラクター定義、null可\r
328          * @return アッパーコンパチブルであればtrue、そうでなければfalse\r
329          */\r
330         public boolean isUpperCompatibleStructure(CharacterData other) {\r
331                 if (!this.isValid() || other == null || !other.isValid()) {\r
332                         // 自分または相手がinvalidであれば構造的には常に互換性なしと見なす.\r
333                         return false;\r
334                 }\r
335 \r
336                 // カラーグループが等しいか? (順序は問わない)\r
337                 // IDのみによって判定する\r
338                 ArrayList<ColorGroup> colorGroupNew = new ArrayList<ColorGroup>(getColorGroups());\r
339                 ArrayList<ColorGroup> colorGroupOld = new ArrayList<ColorGroup>(other.getColorGroups());\r
340                 if (!colorGroupNew.containsAll(colorGroupOld)) {\r
341                         // 自分が相手分のすべてのものを持っていなければ互換性なし.\r
342                         return false;\r
343                 }\r
344                 \r
345                 // カテゴリをすべて含むか? (順序は問わない)\r
346                 // IDによってのみ判定する.\r
347                 Map<String, PartsCategory> categoriesNew = new HashMap<String, PartsCategory>();\r
348                 for (PartsCategory category : getPartsCategories()) {\r
349                         categoriesNew.put(category.getCategoryId(), category);\r
350                 }\r
351                 Map<String, PartsCategory> categoriesOld = new HashMap<String, PartsCategory>();\r
352                 for (PartsCategory category : other.getPartsCategories()) {\r
353                         categoriesOld.put(category.getCategoryId(), category);\r
354                 }\r
355                 if ( !categoriesNew.keySet().containsAll(categoriesOld.keySet())) {\r
356                         // 自分が相手のすべてのカテゴリを持っていなければ互換性なし.\r
357                         return false;\r
358                 }\r
359                 \r
360                 // レイヤーをすべて含むか?\r
361                 // ID、Dirによってのみ判定する.\r
362                 for (Map.Entry<String, PartsCategory> categoryOldEntry : categoriesOld.entrySet()) {\r
363                         String categoryId = categoryOldEntry.getKey();\r
364                         PartsCategory categoryOld = categoryOldEntry.getValue();\r
365                         PartsCategory categoryNew = categoriesNew.get(categoryId);\r
366                         if (categoryNew == null) {\r
367                                 return false;\r
368                         }\r
369                         \r
370                         Map<String, Layer> layersNew = new HashMap<String, Layer>();\r
371                         for (Layer layer : categoryNew.getLayers()) {\r
372                                 layersNew.put(layer.getId(), layer);\r
373                         }\r
374                         Map<String, Layer> layersOld = new HashMap<String, Layer>();\r
375                         for (Layer layer : categoryOld.getLayers()) {\r
376                                 layersOld.put(layer.getId(), layer);\r
377                         }\r
378                         \r
379                         if ( !layersNew.keySet().containsAll(layersOld.keySet())) {\r
380                                 // 自分が相手のすべてのレイヤー(ID)を持っていなければ互換性なし.\r
381                                 return false;\r
382                         }\r
383                         for (Map.Entry<String, Layer> layerOldEntry : layersOld.entrySet()) {\r
384                                 String layerId = layerOldEntry.getKey();\r
385                                 Layer layerOld = layerOldEntry.getValue();\r
386                                 Layer layerNew = layersNew.get(layerId);\r
387                                 if (layerNew == null) {\r
388                                         return false;\r
389                                 }\r
390                                 File dirOld = new File(layerOld.getDir());\r
391                                 File dirNew = new File(layerNew.getDir());\r
392                                 if ( !dirOld.equals(dirNew)) {\r
393                                         // ディレクトリが一致しなければ互換性なし.\r
394                                         return false;\r
395                                 }\r
396                         }\r
397                 }\r
398                 \r
399                 return true;\r
400         }\r
401         \r
402         /**\r
403          * キャラクターデータの構造を表す文字列を返す.<br>\r
404          * カテゴリ、レイヤー、色グループのみで構成される.<br>\r
405          * id, revなどは含まない.<br>\r
406          * @return キャラクターデータの構造を表す文字列\r
407          */\r
408         public String toStructureString() {\r
409                 // カラーグループ\r
410                 StringBuilder buf = new StringBuilder();\r
411                 buf.append("{colorGroup:[");\r
412                 for (ColorGroup colorGroup : getColorGroups()) {\r
413                         buf.append(colorGroup.getId());\r
414                         buf.append(",");\r
415                 }\r
416                 buf.append("],");\r
417                 \r
418                 // カテゴリ\r
419                 buf.append("category:[");\r
420                 for (PartsCategory category : getPartsCategories()) {\r
421                         buf.append("{id:");\r
422                         buf.append(category.getCategoryId());\r
423 \r
424                         buf.append(",layer:[");\r
425                         for (Layer layer : category.getLayers()) {\r
426                                 buf.append("{id:");\r
427                                 buf.append(layer.getId());\r
428                                 buf.append(",dir:");\r
429                                 buf.append(layer.getDir());\r
430                                 buf.append("},");\r
431                         }\r
432                         buf.append("]},");\r
433                 }\r
434                 buf.append("]}");\r
435                 \r
436                 return buf.toString();\r
437         }\r
438 \r
439         /**\r
440          * キャラクターデータのID, REVと構造を識別するシグネチャの文字列を返す.<br>\r
441          * (構造はカテゴリ、レイヤー、色グループのみ).<br>\r
442          * @return シグネチャの文字列\r
443          */\r
444         public String toSignatureString() {\r
445                 StringBuilder buf = new StringBuilder();\r
446                 buf.append("{id:");\r
447                 buf.append(getId());\r
448                 buf.append(",rev:");\r
449                 buf.append(getRev());\r
450                 buf.append(",structure:");\r
451                 buf.append(toStructureString());\r
452                 buf.append("}");\r
453                 return buf.toString();\r
454         }\r
455         \r
456         /**\r
457          * デシリアライズする.\r
458          * @param stream 入力もと\r
459          * @throws IOException 失敗\r
460          * @throws ClassNotFoundException 失敗\r
461          */\r
462         private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException {\r
463                  stream.defaultReadObject();\r
464                  partsColorMrg = new PartsColorManager(this);\r
465         }\r
466         \r
467         /**\r
468          * お勧めリンクのリストを取得する.<br>\r
469          * 古いキャラクターデータで、お勧めリストノードが存在しない場合はnullとなる.<br>\r
470          * @return お気に入りリンクのリスト、もしくはnull\r
471          */\r
472         public List<RecommendationURL> getRecommendationURLList() {\r
473                 return recommendationURLList;\r
474         }\r
475         \r
476         /**\r
477          * お勧めリンクリストを設定する.<br>\r
478          * @param recommendationURLList、null可\r
479          */\r
480         public void setRecommendationURLList(\r
481                         List<RecommendationURL> recommendationURLList) {\r
482                 this.recommendationURLList = recommendationURLList;\r
483         }\r
484         \r
485 \r
486         /**\r
487          * 作者を設定する.\r
488          * @param author 作者\r
489          */\r
490         public void setAuthor(String author) {\r
491                 this.author = author;\r
492         }\r
493         \r
494         /**\r
495          * 説明を設定する.<br>\r
496          * 説明の改行コードはプラットフォーム固有の改行コードに変換される.<br>\r
497          * @param description\r
498          */\r
499         public void setDescription(String description) {\r
500                 if (description != null) {\r
501                         description = description.replace("\r\n", "\n");\r
502                         description = description.replace("\r", "\n");\r
503                         description = description.replace("\n", System.getProperty("line.separator"));\r
504                 }\r
505                 this.description = description;\r
506         }\r
507         \r
508         public String getAuthor() {\r
509                 return author;\r
510         }\r
511         \r
512         /**\r
513          * 説明を取得する.<br>\r
514          * 説明の改行コードはプラットフォーム固有の改行コードとなる.<br>\r
515          * @return 説明\r
516          */\r
517         public String getDescription() {\r
518                 return description;\r
519         }\r
520         \r
521         public String getId() {\r
522                 return id;\r
523         }\r
524         \r
525         public void setId(String id) {\r
526                 this.id = id;\r
527         }\r
528         \r
529         public String getRev() {\r
530                 return rev;\r
531         }\r
532         \r
533         public void setRev(String rev) {\r
534                 this.rev = rev;\r
535         }\r
536         \r
537         public void setDocBase(URI docBase) {\r
538                 this.docBase = docBase;\r
539         }\r
540         \r
541         public URI getDocBase() {\r
542                 return docBase;\r
543         }\r
544 \r
545         /**\r
546          * ディレクトリを監視するか? (デフォルトは監視する)\r
547          * @return ディレクトリを監視する場合はtrue\r
548          */\r
549         public boolean isWatchDirectory() {\r
550                 try {\r
551                         String value = properties.getProperty("watch-dir");\r
552                         if (value != null) {\r
553                                 return Boolean.parseBoolean(value);\r
554                         }\r
555                 } catch (RuntimeException ex) {\r
556                         logger.log(Level.WARNING, "watch-dir property is invalid.", ex);\r
557                 }\r
558                 // デフォルトは監視する.\r
559                 return true;\r
560         }\r
561         \r
562         /**\r
563          * ディレクトリを監視するか指定する.\r
564          * @param watchDir 監視する場合はtrue、しない場合はfalse\r
565          */\r
566         public void setWatchDirectory(boolean watchDir) {\r
567                 properties.setProperty("watch-dir", Boolean.toString(watchDir));\r
568         }\r
569         \r
570         public String getProperty(String key) {\r
571                 if (key == null || key.trim().length() == 0) {\r
572                         throw new IllegalArgumentException();\r
573                 }\r
574                 return properties.getProperty(key.trim());\r
575         }\r
576         \r
577         public void setProperty(String key, String value) {\r
578                 if (key == null || key.trim().length() == 0) {\r
579                         throw new IllegalArgumentException();\r
580                 }\r
581                 properties.setProperty(key.trim(), value);\r
582         }\r
583         \r
584         public Collection<String> getPropertyNames() {\r
585                 ArrayList<String> names = new ArrayList<String>();\r
586                 for (Object key : properties.keySet()) {\r
587                         names.add(key.toString());\r
588                 }\r
589                 return names;\r
590         }\r
591         \r
592         /**\r
593          * 有効なキャラクターデータであるか?\r
594          * ID, Name, DocBaseが存在するものが有効なキャラクターデータである.<br>\r
595          * @return 有効であればtrue\r
596          */\r
597         public boolean isValid() {\r
598                 return id != null && id.length() > 0 && localizedName != null\r
599                                 && localizedName.length() > 0 && docBase != null;\r
600         }\r
601         \r
602         /**\r
603          * 編集可能か?<br>\r
604          * まだdocbaseが指定されていない新しいインスタンスであるか、\r
605          * もしくはdocbaseが実在しファイルであり且つ読み込み可能であるか、\r
606          * もしくはdocbaseがまだ存在しない場合は、その親ディレクトリが読み書き可能であるか?\r
607          * @return 編集可能であればtrue\r
608          */\r
609         public boolean canWrite() {\r
610                 try {\r
611                         checkWritable();\r
612                         return true;\r
613 \r
614                 } catch (IOException ex) {\r
615                         return false;\r
616                 }\r
617         }\r
618 \r
619         /**\r
620          * 編集可能か?<br>\r
621          * まだdocbaseが指定されていない新しいインスタンスであるか、\r
622          * もしくはdocbaseが実在しファイルであり且つ読み込み可能であるか、\r
623          * もしくはdocbaseがまだ存在しない場合は、その親ディレクトリが読み書き可能であるか?\r
624          * @throws IOException 編集可能でなければIOException例外が発生する.\r
625          */\r
626         public void checkWritable() throws IOException {\r
627                 if (docBase == null) {\r
628                         throw new IOException("invalid profile: " + this);\r
629                 }\r
630 \r
631                 if ( !"file".equals(docBase.getScheme())) {\r
632                         throw new IOException("ファイルプロトコルではないため書き込みはできません。:" + docBase);\r
633                 }\r
634 \r
635                 File xmlFile = new File(docBase);\r
636                 if (xmlFile.exists()) {\r
637                         // character.xmlファイルがある場合\r
638                         if ( !xmlFile.canWrite() || !xmlFile.canRead()) {\r
639                                 throw new IOException("書き込み、もしくは読み込みが禁止されているプロファイルです。" + docBase);\r
640                         }\r
641                         \r
642                 } else {\r
643                         // character.xmlファイルが、まだ存在していない場合\r
644                         File parent = xmlFile.getParentFile();\r
645                         if ( !parent.exists()) {\r
646                                 throw new IOException("親ディレクトリがありません。" + docBase);\r
647                         }\r
648                         if ( !parent.canWrite() || !parent.canRead()) {\r
649                                 throw new IOException("親ディレクトリは書き込み、もしくは読み込みが禁止されています。" + docBase);\r
650                         }\r
651                 }\r
652         }\r
653 \r
654         /**\r
655          * キャラクター名を設定する.\r
656          * @param name\r
657          */\r
658         public void setName(String name) {\r
659                 this.localizedName = name;\r
660         }\r
661         \r
662         /**\r
663          * キャラクター名を取得する.\r
664          * @return\r
665          */\r
666         public String getName() {\r
667                 return localizedName;\r
668         }\r
669         \r
670         public void setImageSize(Dimension imageSize) {\r
671                 if (imageSize != null) {\r
672                         imageSize = (Dimension) imageSize.clone();\r
673                 }\r
674                 this.imageSize = imageSize;\r
675         }\r
676         \r
677         public Dimension getImageSize() {\r
678                 return imageSize != null ? (Dimension) imageSize.clone() : null;\r
679         }\r
680         \r
681         public void setColorGroups(Collection<ColorGroup> colorGroups) {\r
682                 if (colorGroups == null) {\r
683                         throw new IllegalArgumentException();\r
684                 }\r
685                 \r
686                 ArrayList<ColorGroup> colorGroupWithNA = new ArrayList<ColorGroup>();\r
687 \r
688                 colorGroupWithNA.add(ColorGroup.NA);\r
689                 for (ColorGroup colorGroup : colorGroups) {\r
690                         if (colorGroup.isEnabled()) {\r
691                                 colorGroupWithNA.add(colorGroup);\r
692                         }\r
693                 }\r
694                 \r
695                 OrderedMap<String, ColorGroup> ret = new OrderedMap<String, ColorGroup>(\r
696                                 colorGroupWithNA,\r
697                                 new OrderedMap.KeyDetector<String, ColorGroup>() {\r
698                                         public String getKey(ColorGroup data) {\r
699                                                 return data.getId();\r
700                                         }\r
701                                 });\r
702                 this.colorGroups = ret;\r
703         }\r
704         \r
705         /**\r
706          * カラーグループIDからカラーグループを取得する.<br>\r
707          * 存在しない場合はN/Aを返す.<br>\r
708          * @param colorGroupId カラーグループID\r
709          * @return カラーグループ\r
710          */\r
711         public ColorGroup getColorGroup(String colorGroupId) {\r
712                 ColorGroup cg = colorGroups.get(colorGroupId);\r
713                 if (cg != null) {\r
714                         return cg;\r
715                 }\r
716                 return ColorGroup.NA;\r
717         }\r
718         \r
719         public Collection<ColorGroup> getColorGroups() {\r
720                 return colorGroups.values();\r
721         }\r
722         \r
723         public PartsCategory getPartsCategory(String categoryId) {\r
724                 if (partsCategories == null) {\r
725                         return null;\r
726                 }\r
727                 return partsCategories.get(categoryId);\r
728         }\r
729         \r
730         public void setPartsCategories(PartsCategory[] partsCategories) {\r
731                 if (partsCategories == null) {\r
732                         partsCategories = new PartsCategory[0];\r
733                 }\r
734                 this.partsCategories = new OrderedMap<String, PartsCategory>(\r
735                                 Arrays.asList(partsCategories),\r
736                                 new OrderedMap.KeyDetector<String, PartsCategory>() {\r
737                                         public String getKey(PartsCategory data) {\r
738                                                 return data.getCategoryId();\r
739                                         }\r
740                                 });\r
741         }\r
742         \r
743         public List<PartsCategory> getPartsCategories() {\r
744                 return partsCategories.asList();\r
745         }\r
746         \r
747         /**\r
748          * パーツデータがロード済みであるか?<br>\r
749          * 少なくとも{@link #loadPartsData(PartsDataLoader)}が一度呼び出されていればtrueとなる.<br>\r
750          * falseの場合はパーツローダが設定されていないことを示す.<br>\r
751          * @return パーツデータがロード済みであればtrue、そうでなければfalse\r
752          */\r
753         public boolean isPartsLoaded() {\r
754                 return partsDataLoader != null;\r
755         }\r
756 \r
757         /**\r
758          * パーツデータをロードする.<br>\r
759          * パーツローダを指定し、このローダはパーツの再ロード時に使用するため保持される.<br>\r
760          * @param partsDataLoader ローダー\r
761          */\r
762         public void loadPartsData(PartsDataLoader partsDataLoader) {\r
763                 if (partsDataLoader == null) {\r
764                         throw new IllegalArgumentException();\r
765                 }\r
766                 this.partsDataLoader = partsDataLoader;\r
767                 reloadPartsData();\r
768         }\r
769         \r
770         /**\r
771          * パーツデータをリロードする.<br>\r
772          * ロード時に使用したローダーを使ってパーツを再ロードします.<br>\r
773          * まだ一度もロードしていない場合はIllegalStateException例外が発生します.<br>\r
774          * @return 変更があった場合はtrue、ない場合はfalse\r
775          */\r
776         public boolean reloadPartsData() {\r
777                 if (partsDataLoader == null) {\r
778                         throw new IllegalStateException("partsDataLoader is not set.");\r
779                 }\r
780                 // パーツデータのロード\r
781                 images.clear();\r
782                 for (PartsCategory category : partsCategories.asList()) {\r
783                         images.put(category, partsDataLoader.load(category));\r
784                 }\r
785                 // NOTE: とりあえずパーツの変更を検査せず、常に変更ありにしておく。とりあえず実害ない。\r
786                 return true;\r
787         }\r
788         \r
789         /**\r
790          * {@inheritDoc}\r
791          */\r
792         public PartsSpec getPartsSpec(PartsIdentifier partsIdentifier) {\r
793                 if (partsIdentifier == null) {\r
794                         throw new IllegalArgumentException();\r
795                 }\r
796                 PartsCategory partsCategory = partsIdentifier.getPartsCategory();\r
797                 Map<PartsIdentifier, PartsSpec> partsSpecMap = images.get(partsCategory);\r
798                 if (partsSpecMap != null) {\r
799                         PartsSpec partsSpec = partsSpecMap.get(partsIdentifier);\r
800                         if (partsSpec != null) {\r
801                                 return partsSpec;\r
802                         }\r
803                 }\r
804                 return null;\r
805         }\r
806         \r
807         /**\r
808          * {@inheritDoc}\r
809          */\r
810         public Map<PartsIdentifier, PartsSpec> getPartsSpecMap(PartsCategory category) {\r
811                 Map<PartsIdentifier, PartsSpec> partsImageMap = images.get(category);\r
812                 if (partsImageMap == null) {\r
813                         return Collections.emptyMap();\r
814                 }\r
815                 return partsImageMap;\r
816         }\r
817 \r
818         public PartsColorManager getPartsColorManager() {\r
819                 return this.partsColorMrg;\r
820         }\r
821         \r
822         /**\r
823          * パーツセットを登録します.<br>\r
824          * お気に入りとプリセットの両方の共用です.<br>\r
825          * IDおよび名前がないものは登録されず、falseを返します.<br>\r
826          * パーツセットは、このキャラクター定義に定義されているカテゴリに正規化されます.<br>\r
827          * 正規化された結果カテゴリが一つもなくなった場合は何も登録されず、falseを返します.<br>\r
828          * 登録された場合はtrueを返します.<br>\r
829          * 同一のIDは上書きされます.<br>\r
830          * @param partsSet\r
831          * @return 登録された場合はtrue、登録できない場合はfalse\r
832          */\r
833         public boolean addPartsSet(PartsSet partsSet) {\r
834                 if (partsSet == null) {\r
835                         throw new IllegalArgumentException();\r
836                 }\r
837                 if (partsSet.getPartsSetId() == null\r
838                                 || partsSet.getPartsSetId().length() == 0\r
839                                 || partsSet.getLocalizedName() == null\r
840                                 || partsSet.getLocalizedName().length() == 0) {\r
841                         return false;\r
842                 }\r
843                 PartsSet compatiblePartsSet = partsSet.createCompatible(this);\r
844                 if (compatiblePartsSet.isEmpty()) {\r
845                         return false;\r
846                 }\r
847                 presets.put(compatiblePartsSet.getPartsSetId(), compatiblePartsSet);\r
848                 return true;\r
849         }\r
850         \r
851         /**\r
852          * プリセットパーツおよびパーツセット(Favorites)のコレクション.\r
853          * @return パーツセットのコレクション\r
854          */\r
855         public Map<String, PartsSet> getPartsSets() {\r
856                 return presets;\r
857         }\r
858 \r
859         /**\r
860          * プリセットパーツおよびパーツセットをリセットします.<br>\r
861          * @param noRemovePreset プリセットは削除せず残し、プリセット以外のパーツセットをクリアする場合はtrue、falseの場合は全て削除される.\r
862          */\r
863         public void clearPartsSets(boolean noRemovePreset) {\r
864                 if (!noRemovePreset) {\r
865                         // 全部消す\r
866                         presets.clear();\r
867                         defaultPartsSetId = null;\r
868                 } else {\r
869                         // プリセット以外を消す\r
870                         Iterator<Map.Entry<String, PartsSet>> ite = presets.entrySet().iterator();\r
871                         while (ite.hasNext()) {\r
872                                 Map.Entry<String, PartsSet> entry = ite.next();\r
873                                 if (!entry.getValue().isPresetParts()) {\r
874                                         // デフォルトパーツセットであれば、デフォルトパーツセットもnullにする.\r
875                                         // (ただし、デフォルトパーツセットはプリセットであることを想定しているので、この処理は安全策用。)\r
876                                         if (entry.getKey().equals(defaultPartsSetId)) {\r
877                                                 defaultPartsSetId = null;\r
878                                         }\r
879                                         ite.remove();\r
880                                 }\r
881                         }\r
882                 }\r
883         }\r
884         \r
885         /**\r
886          * デフォルトのパーツセットを取得する.<br>\r
887          * そのパーツセットIDが実在するか、あるいは、それがプリセットであるか、などは一切関知しない.<br>\r
888          * 呼び出しもとで必要に応じてチェックすること.<br>\r
889          * @return デフォルトとして指定されているパーツセットのID、なければnull\r
890          */\r
891         public String getDefaultPartsSetId() {\r
892                 return defaultPartsSetId;\r
893         }\r
894         \r
895         /**\r
896          * デフォルトのパーツセットIDを指定する.<br>\r
897          * nullの場合はデフォルトのパーツセットがないことを示す.<br>\r
898          * パーツセットはプリセットであることが想定されるが、<br>\r
899          * 実際に、その名前のパーツセットが存在するか、あるいは、そのパーツセットがプリセットであるか、などの判定は一切行わない.<br>\r
900          * @param defaultPartsSetId パーツセットID、もしくはnull\r
901          */\r
902         public void setDefaultPartsSetId(String defaultPartsSetId) {\r
903                 this.defaultPartsSetId = defaultPartsSetId;\r
904         }\r
905         \r
906         @Override\r
907         public String toString() {\r
908                 StringBuilder buf = new StringBuilder();\r
909                 buf.append("character-id: " + id);\r
910                 buf.append("/rev:" + rev);\r
911                 buf.append("/name:" + localizedName);\r
912                 buf.append("/image-size:" + imageSize.width + "x" + imageSize.height);\r
913                 buf.append("/docBase:" + docBase);\r
914                 return buf.toString();\r
915         }\r
916         \r
917 }\r