OSDN Git Service

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