1 package charactermanaj.model;
3 import java.awt.Dimension;
5 import java.io.IOException;
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;
16 import java.util.Properties;
17 import java.util.logging.Level;
18 import java.util.logging.Logger;
20 import charactermanaj.model.io.PartsDataLoader;
26 public class CharacterData implements PartsSpecResolver {
31 private static final Logger logger = Logger.getLogger(CharacterData.class.getName());
35 * キャラクターデータを表示名順にソートするための比較器.<br>
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;
42 int ret = o1.getName().compareTo(o2.getName());
44 ret = o1.getId().compareTo(o2.getId());
47 ret = o1.getDocBase().toString().compareTo(o2.getDocBase().toString());
55 * キャラクターデータを定義しているXMLの位置.<br>
56 * docBase自身はxml定義には含まれず、xmlをロードした位置を記憶するためにPersistentクラスによって設定される.<br>
63 * キャラクターデータの内部用ID.<br>
64 * キャラクターデータの構造を判定するために用いる.<br>
70 * キャラクターデータの構造が変更されたことを識別するために用いる.<br>
77 private String localizedName;
82 private String author;
87 private String description;
92 private Dimension imageSize;
97 private OrderedMap<String, PartsCategory> partsCategories = OrderedMap.emptyMap();
102 private Properties properties = new Properties();
107 * キーはプリセット自身のID、値はプリセット自身.<br>
109 private HashMap<String, PartsSet> presets = new HashMap<String, PartsSet>();
114 private String defaultPartsSetId;
119 private OrderedMap<String, ColorGroup> colorGroups = OrderedMap.emptyMap();
124 * Ver0.96以前には存在しないのでnullになり得る.
126 private List<RecommendationURL> recommendationURLList;
130 * (非シリアライズデータ、デシリアライズ時には新規インスタンスが作成される).<br>
132 private transient PartsColorManager partsColorMrg = new PartsColorManager(this);
136 * パーツをロードしたときに設定され、リロードするときに使用する.<br>
137 * パーツを一度もロードしていない場合はnull.
138 * (非シリアライズデータ、デシリアライズ時はnullのまま).<br>
140 private transient PartsDataLoader partsDataLoader;
144 * (キャラクターセットはパーツイメージをもったままシリアライズされることは想定していないが、可能ではある。)
146 private Map<PartsCategory, Map<PartsIdentifier, PartsSpec>> images
147 = new HashMap<PartsCategory, Map<PartsIdentifier, PartsSpec>>();
150 * 基本情報のみをコピーして返します.<br>
151 * DocBase, ID, REV, Name, Author, Description, ImageSize、および, PartsCategory, ColorGroup, PartSetのコレクションがコピーされます.<br>
152 * それ以外のものはコピーされません.<br>
153 * @return 基本情報をコピーした新しいインスタンス
155 public CharacterData duplicateBasicInfo() {
156 return duplicateBasicInfo(true);
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 基本情報をコピーした新しいインスタンス
168 public CharacterData duplicateBasicInfo(boolean needPartsSets) {
169 CharacterData cd = new CharacterData();
173 cd.setDocBase(this.docBase);
175 cd.setName(this.localizedName);
177 cd.setAuthor(this.author);
178 cd.setDescription(this.description);
180 cd.setImageSize((Dimension)(this.imageSize == null ? null : this.imageSize.clone()));
182 cd.setWatchDirectory(this.isWatchDirectory());
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());
191 cd.setRecommendationURLList(recommendationURLList);
193 ArrayList<PartsCategory> partsCategories = new ArrayList<PartsCategory>();
194 partsCategories.addAll(this.getPartsCategories());
195 cd.setPartsCategories(partsCategories.toArray(new PartsCategory[partsCategories.size()]));
197 ArrayList<ColorGroup> colorGroups = new ArrayList<ColorGroup>();
198 colorGroups.addAll(this.getColorGroups());
199 cd.setColorGroups(colorGroups);
202 for (PartsSet partsSet : this.getPartsSets().values()) {
203 cd.addPartsSet(partsSet.clone());
205 cd.setDefaultPartsSetId(this.defaultPartsSetId);
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
223 public boolean isSameStructure(CharacterData other) {
224 if (!this.isValid() || other == null || !other.isValid()) {
225 // 自分または相手がinvalidであれば構造的には常に不一致と見なす.
229 // カラーグループが等しいか? (順序は問わない)
231 ArrayList<ColorGroup> colorGroup1 = new ArrayList<ColorGroup>(getColorGroups());
232 ArrayList<ColorGroup> colorGroup2 = new ArrayList<ColorGroup>(other.getColorGroups());
233 if (colorGroup1.size() != colorGroup2.size()) {
236 if (!colorGroup1.containsAll(colorGroup2)) {
240 // カテゴリが等しいか? (順序は問わない)
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());
248 ret = o1.getOrder() - o2.getOrder();
253 // カテゴリID順に並び替えて, IDのみを比較する.
254 Collections.sort(categories1, sortCategoryId);
255 Collections.sort(categories2, sortCategoryId);
256 int numOfCategories = categories1.size();
257 if (numOfCategories != categories2.size()) {
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)) {
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);
279 ArrayList<Layer> layers1 = new ArrayList<Layer>(category1.getLayers());
280 ArrayList<Layer> layers2 = new ArrayList<Layer>(category2.getLayers());
282 Comparator<Layer> sortLayerId = new Comparator<Layer>() {
283 public int compare(Layer o1, Layer o2) {
284 int ret = o1.getId().compareTo(o2.getId());
286 ret = o1.getOrder() - o2.getOrder();
292 Collections.sort(layers1, sortLayerId);
293 Collections.sort(layers2, sortLayerId);
295 // ID、順序、Dirで判断する.(それ以外のレイヤー情報はequalsでは比較されない)
296 if ( !layers1.equals(layers2)) {
306 * 引数で指定したキャラクター定義とアッパーコンパチブルであるか?<br>
307 * 構造が同一であるか、サイズ違い、もしくはレイヤーの順序、カテゴリの順序、
308 * もしくはレイヤーまたはカテゴリが増えている場合で、減っていない場合はtrueとなる.<br>
309 * 引数がnullの場合は常にfalseとなる.
310 * @param other 前の状態のキャラクター定義、null可
311 * @return アッパーコンパチブルであればtrue、そうでなければfalse
313 public boolean isUpperCompatibleStructure(CharacterData other) {
314 if (!this.isValid() || other == null || !other.isValid()) {
315 // 自分または相手がinvalidであれば構造的には常に互換性なしと見なす.
319 // カラーグループが等しいか? (順序は問わない)
321 ArrayList<ColorGroup> colorGroupNew = new ArrayList<ColorGroup>(getColorGroups());
322 ArrayList<ColorGroup> colorGroupOld = new ArrayList<ColorGroup>(other.getColorGroups());
323 if (!colorGroupNew.containsAll(colorGroupOld)) {
324 // 自分が相手分のすべてのものを持っていなければ互換性なし.
328 // カテゴリをすべて含むか? (順序は問わない)
330 Map<String, PartsCategory> categoriesNew = new HashMap<String, PartsCategory>();
331 for (PartsCategory category : getPartsCategories()) {
332 categoriesNew.put(category.getCategoryId(), category);
334 Map<String, PartsCategory> categoriesOld = new HashMap<String, PartsCategory>();
335 for (PartsCategory category : other.getPartsCategories()) {
336 categoriesOld.put(category.getCategoryId(), category);
338 if ( !categoriesNew.keySet().containsAll(categoriesOld.keySet())) {
339 // 自分が相手のすべてのカテゴリを持っていなければ互換性なし.
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) {
353 Map<String, Layer> layersNew = new HashMap<String, Layer>();
354 for (Layer layer : categoryNew.getLayers()) {
355 layersNew.put(layer.getId(), layer);
357 Map<String, Layer> layersOld = new HashMap<String, Layer>();
358 for (Layer layer : categoryOld.getLayers()) {
359 layersOld.put(layer.getId(), layer);
362 if ( !layersNew.keySet().containsAll(layersOld.keySet())) {
363 // 自分が相手のすべてのレイヤー(ID)を持っていなければ互換性なし.
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) {
373 File dirOld = new File(layerOld.getDir());
374 File dirNew = new File(layerNew.getDir());
375 if ( !dirOld.equals(dirNew)) {
376 // ディレクトリが一致しなければ互換性なし.
386 * キャラクターデータの構造を表す文字列を返す.<br>
387 * カテゴリ、レイヤー、色グループのみで構成される.<br>
388 * id, revなどは含まない.<br>
389 * @return キャラクターデータの構造を表す文字列
391 public String toStructureString() {
393 StringBuilder buf = new StringBuilder();
394 buf.append("{colorGroup:[");
395 for (ColorGroup colorGroup : getColorGroups()) {
396 buf.append(colorGroup.getId());
402 buf.append("category:[");
403 for (PartsCategory category : getPartsCategories()) {
405 buf.append(category.getCategoryId());
407 buf.append(",layer:[");
408 for (Layer layer : category.getLayers()) {
410 buf.append(layer.getId());
412 buf.append(layer.getDir());
419 return buf.toString();
423 * キャラクターデータのID, REVと構造を識別するシグネチャの文字列を返す.<br>
424 * (構造はカテゴリ、レイヤー、色グループのみ).<br>
427 public String toSignatureString() {
428 StringBuilder buf = new StringBuilder();
432 buf.append(getRev());
433 buf.append(",structure:");
434 buf.append(toStructureString());
436 return buf.toString();
440 * お勧めリンクのリストを取得する.<br>
441 * 古いキャラクターデータで、お勧めリストノードが存在しない場合はnullとなる.<br>
442 * @return お気に入りリンクのリスト、もしくはnull
444 public List<RecommendationURL> getRecommendationURLList() {
445 return recommendationURLList;
449 * お勧めリンクリストを設定する.<br>
450 * @param recommendationURLList、null可
452 public void setRecommendationURLList(
453 List<RecommendationURL> recommendationURLList) {
454 this.recommendationURLList = recommendationURLList;
462 public void setAuthor(String author) {
463 this.author = author;
468 * 説明の改行コードはプラットフォーム固有の改行コードに変換される.<br>
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"));
477 this.description = description;
480 public String getAuthor() {
486 * 説明の改行コードはプラットフォーム固有の改行コードとなる.<br>
489 public String getDescription() {
493 public String getId() {
497 public void setId(String id) {
501 public String getRev() {
505 public void setRev(String rev) {
509 public void setDocBase(URI docBase) {
510 this.docBase = docBase;
513 public URI getDocBase() {
518 * ディレクトリを監視するか? (デフォルトは監視する)
519 * @return ディレクトリを監視する場合はtrue
521 public boolean isWatchDirectory() {
523 String value = properties.getProperty("watch-dir");
525 return Boolean.parseBoolean(value);
527 } catch (RuntimeException ex) {
528 logger.log(Level.WARNING, "watch-dir property is invalid.", ex);
536 * @param watchDir 監視する場合はtrue、しない場合はfalse
538 public void setWatchDirectory(boolean watchDir) {
539 properties.setProperty("watch-dir", Boolean.toString(watchDir));
542 public String getProperty(String key) {
543 if (key == null || key.trim().length() == 0) {
544 throw new IllegalArgumentException();
546 return properties.getProperty(key.trim());
549 public void setProperty(String key, String value) {
550 if (key == null || key.trim().length() == 0) {
551 throw new IllegalArgumentException();
553 properties.setProperty(key.trim(), value);
556 public Collection<String> getPropertyNames() {
557 ArrayList<String> names = new ArrayList<String>();
558 for (Object key : properties.keySet()) {
559 names.add(key.toString());
566 * ID, Name, DocBaseが存在するものが有効なキャラクターデータである.<br>
569 public boolean isValid() {
570 return id != null && id.length() > 0 && localizedName != null
571 && localizedName.length() > 0 && docBase != null;
576 * まだdocbaseが指定されていない新しいインスタンスであるか、
577 * もしくはdocbaseが実在しファイルであり且つ読み込み可能であるか、
578 * もしくはdocbaseがまだ存在しない場合は、その親ディレクトリが読み書き可能であるか?
579 * @return 編集可能であればtrue
581 public boolean canWrite() {
586 } catch (IOException ex) {
593 * まだdocbaseが指定されていない新しいインスタンスであるか、
594 * もしくはdocbaseが実在しファイルであり且つ読み込み可能であるか、
595 * もしくはdocbaseがまだ存在しない場合は、その親ディレクトリが読み書き可能であるか?
596 * @throws IOException 編集可能でなければIOException例外が発生する.
598 public void checkWritable() throws IOException {
599 if (docBase == null) {
600 throw new IOException("invalid profile: " + this);
603 if ( !"file".equals(docBase.getScheme())) {
604 throw new IOException("ファイルプロトコルではないため書き込みはできません。:" + docBase);
607 File xmlFile = new File(docBase);
608 if (xmlFile.exists()) {
609 // character.xmlファイルがある場合
610 if ( !xmlFile.canWrite() || !xmlFile.canRead()) {
611 throw new IOException("書き込み、もしくは読み込みが禁止されているプロファイルです。" + docBase);
615 // character.xmlファイルが、まだ存在していない場合
616 File parent = xmlFile.getParentFile();
617 if ( !parent.exists()) {
618 throw new IOException("親ディレクトリがありません。" + docBase);
620 if ( !parent.canWrite() || !parent.canRead()) {
621 throw new IOException("親ディレクトリは書き込み、もしくは読み込みが禁止されています。" + docBase);
630 public void setName(String name) {
631 this.localizedName = name;
638 public String getName() {
639 return localizedName;
642 public void setImageSize(Dimension imageSize) {
643 if (imageSize != null) {
644 imageSize = (Dimension) imageSize.clone();
646 this.imageSize = imageSize;
649 public Dimension getImageSize() {
650 return imageSize != null ? (Dimension) imageSize.clone() : null;
653 public void setColorGroups(Collection<ColorGroup> colorGroups) {
654 if (colorGroups == null) {
655 throw new IllegalArgumentException();
658 ArrayList<ColorGroup> colorGroupWithNA = new ArrayList<ColorGroup>();
660 colorGroupWithNA.add(ColorGroup.NA);
661 for (ColorGroup colorGroup : colorGroups) {
662 if (colorGroup.isEnabled()) {
663 colorGroupWithNA.add(colorGroup);
667 OrderedMap<String, ColorGroup> ret = new OrderedMap<String, ColorGroup>(
669 new OrderedMap.KeyDetector<String, ColorGroup>() {
670 public String getKey(ColorGroup data) {
674 this.colorGroups = ret;
678 * カラーグループIDからカラーグループを取得する.<br>
679 * 存在しない場合はN/Aを返す.<br>
680 * @param colorGroupId カラーグループID
683 public ColorGroup getColorGroup(String colorGroupId) {
684 ColorGroup cg = colorGroups.get(colorGroupId);
688 return ColorGroup.NA;
691 public Collection<ColorGroup> getColorGroups() {
692 return colorGroups.values();
695 public PartsCategory getPartsCategory(String categoryId) {
696 if (partsCategories == null) {
699 return partsCategories.get(categoryId);
702 public void setPartsCategories(PartsCategory[] partsCategories) {
703 if (partsCategories == null) {
704 partsCategories = new PartsCategory[0];
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();
715 public List<PartsCategory> getPartsCategories() {
716 return partsCategories.asList();
720 * パーツデータがロード済みであるか?<br>
721 * 少なくとも{@link #loadPartsData(PartsDataLoader)}が一度呼び出されていればtrueとなる.<br>
722 * falseの場合はパーツローダが設定されていないことを示す.<br>
723 * @return パーツデータがロード済みであればtrue、そうでなければfalse
725 public boolean isPartsLoaded() {
726 return partsDataLoader != null;
731 * パーツローダを指定し、このローダはパーツの再ロード時に使用するため保持される.<br>
732 * @param partsDataLoader ローダー
734 public void loadPartsData(PartsDataLoader partsDataLoader) {
735 if (partsDataLoader == null) {
736 throw new IllegalArgumentException();
738 this.partsDataLoader = partsDataLoader;
744 * ロード時に使用したローダーを使ってパーツを再ロードします.<br>
745 * まだ一度もロードしていない場合はIllegalStateException例外が発生します.<br>
746 * @return 変更があった場合はtrue、ない場合はfalse
748 public boolean reloadPartsData() {
749 if (partsDataLoader == null) {
750 throw new IllegalStateException("partsDataLoader is not set.");
754 for (PartsCategory category : partsCategories.asList()) {
755 images.put(category, partsDataLoader.load(category));
757 // NOTE: とりあえずパーツの変更を検査せず、常に変更ありにしておく。とりあえず実害ない。
764 public PartsSpec getPartsSpec(PartsIdentifier partsIdentifier) {
765 if (partsIdentifier == null) {
766 throw new IllegalArgumentException();
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) {
782 public Map<PartsIdentifier, PartsSpec> getPartsSpecMap(PartsCategory category) {
783 Map<PartsIdentifier, PartsSpec> partsImageMap = images.get(category);
784 if (partsImageMap == null) {
785 return Collections.emptyMap();
787 return partsImageMap;
790 public PartsColorManager getPartsColorManager() {
791 return this.partsColorMrg;
796 * お気に入りとプリセットの両方の共用です.<br>
797 * IDおよび名前がないものは登録されず、falseを返します.<br>
798 * パーツセットは、このキャラクター定義に定義されているカテゴリに正規化されます.<br>
799 * 正規化された結果カテゴリが一つもなくなった場合は何も登録されず、falseを返します.<br>
800 * 登録された場合はtrueを返します.<br>
803 * @return 登録された場合はtrue、登録できない場合はfalse
805 public boolean addPartsSet(PartsSet partsSet) {
806 if (partsSet == null) {
807 throw new IllegalArgumentException();
809 if (partsSet.getPartsSetId() == null
810 || partsSet.getPartsSetId().length() == 0
811 || partsSet.getLocalizedName() == null
812 || partsSet.getLocalizedName().length() == 0) {
815 PartsSet compatiblePartsSet = partsSet.createCompatible(this);
816 if (compatiblePartsSet.isEmpty()) {
819 presets.put(compatiblePartsSet.getPartsSetId(), compatiblePartsSet);
824 * プリセットパーツおよびパーツセット(Favorites)のコレクション.
825 * @return パーツセットのコレクション
827 public Map<String, PartsSet> getPartsSets() {
832 * プリセットパーツおよびパーツセットをリセットします.<br>
833 * @param noRemovePreset プリセットは削除せず残し、プリセット以外のパーツセットをクリアする場合はtrue、falseの場合は全て削除される.
835 public void clearPartsSets(boolean noRemovePreset) {
836 if (!noRemovePreset) {
839 defaultPartsSetId = null;
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;
858 * デフォルトのパーツセットを取得する.<br>
859 * そのパーツセットIDが実在するか、あるいは、それがプリセットであるか、などは一切関知しない.<br>
860 * 呼び出しもとで必要に応じてチェックすること.<br>
861 * @return デフォルトとして指定されているパーツセットのID、なければnull
863 public String getDefaultPartsSetId() {
864 return defaultPartsSetId;
868 * デフォルトのパーツセットIDを指定する.<br>
869 * nullの場合はデフォルトのパーツセットがないことを示す.<br>
870 * パーツセットはプリセットであることが想定されるが、<br>
871 * 実際に、その名前のパーツセットが存在するか、あるいは、そのパーツセットがプリセットであるか、などの判定は一切行わない.<br>
872 * @param defaultPartsSetId パーツセットID、もしくはnull
874 public void setDefaultPartsSetId(String defaultPartsSetId) {
875 this.defaultPartsSetId = defaultPartsSetId;
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();