OSDN Git Service

・キャラクターディレクトリの選択可能化に伴う、キャッシュのパージ処理の変更
[charactermanaj/CharacterManaJ.git] / src / charactermanaj / model / io / AbstractCharacterDataArchiveFile.java
1 package charactermanaj.model.io;\r
2 \r
3 import java.awt.image.BufferedImage;\r
4 import java.io.ByteArrayInputStream;\r
5 import java.io.ByteArrayOutputStream;\r
6 import java.io.File;\r
7 import java.io.IOException;\r
8 import java.io.InputStream;\r
9 import java.io.InputStreamReader;\r
10 import java.net.URI;\r
11 import java.net.URISyntaxException;\r
12 import java.util.ArrayList;\r
13 import java.util.Collection;\r
14 import java.util.Collections;\r
15 import java.util.HashMap;\r
16 import java.util.HashSet;\r
17 import java.util.Locale;\r
18 import java.util.Map;\r
19 import java.util.logging.Level;\r
20 import java.util.logging.Logger;\r
21 \r
22 import javax.imageio.ImageIO;\r
23 \r
24 import charactermanaj.graphics.io.PNGFileImageHeader;\r
25 import charactermanaj.graphics.io.PNGFileImageHeaderReader;\r
26 import charactermanaj.model.CharacterData;\r
27 import charactermanaj.model.Layer;\r
28 import charactermanaj.model.PartsCategory;\r
29 import charactermanaj.model.PartsManageData;\r
30 import charactermanaj.model.io.CharacterDataPersistent.DocInfo;\r
31 \r
32 public abstract class AbstractCharacterDataArchiveFile implements CharacterDataArchiveFile {\r
33 \r
34         private static final Logger logger = Logger.getLogger(AbstractCharacterDataArchiveFile.class.getName());\r
35         \r
36         protected File archiveFile;\r
37         \r
38         protected String rootPrefix = "";\r
39         \r
40         public interface FileContent {\r
41                 \r
42                 String getEntryName();\r
43                 \r
44                 long lastModified();\r
45                 \r
46                 InputStream openStream() throws IOException;\r
47                 \r
48         }\r
49         \r
50         @Override\r
51         public String toString() {\r
52                 return "CharacterDataArchiveFile(file=" + archiveFile + ")";\r
53         }\r
54         \r
55         public static final class CategoryLayerPair {\r
56                 \r
57                 private PartsCategory partsCategory;\r
58                 \r
59                 private Layer layer;\r
60                 \r
61                 public CategoryLayerPair(PartsCategory partsCategory, Layer layer) {\r
62                         if (partsCategory == null || layer == null) {\r
63                                 throw new IllegalArgumentException();\r
64                         }\r
65                         this.partsCategory = partsCategory;\r
66                         this.layer = layer;\r
67                 }\r
68                 \r
69                 @Override\r
70                 public int hashCode() {\r
71                         return partsCategory.hashCode() ^ layer.hashCode();\r
72                 }\r
73                 \r
74                 @Override\r
75                 public boolean equals(Object obj) {\r
76                         if (obj == this) {\r
77                                 return true;\r
78                         }\r
79                         if (obj != null && obj instanceof CategoryLayerPair) {\r
80                                 CategoryLayerPair o = (CategoryLayerPair) obj;\r
81                                 return partsCategory.equals(o.partsCategory) && layer.equals(o.layer);\r
82                         }\r
83                         return false;\r
84                 }\r
85                 \r
86                 public Layer getLayer() {\r
87                         return layer;\r
88                 }\r
89                 \r
90                 public PartsCategory getPartsCategory() {\r
91                         return partsCategory;\r
92                 }\r
93                 \r
94                 @Override\r
95                 public String toString() {\r
96                         return "(" + partsCategory + ":" + layer +")";\r
97                 }\r
98         }\r
99         \r
100         public static final class PartsImageContent implements FileContent {\r
101                 \r
102                 private final FileContent fileContent;\r
103                 \r
104                 private final Collection<CategoryLayerPair> categoryLayerPairs;\r
105                 \r
106                 private final String dirName;\r
107                 \r
108                 private final String partsName;\r
109                 \r
110                 private final String fileName;\r
111                 \r
112                 private final PNGFileImageHeader pngFileImageHeader;\r
113                 \r
114                 /**\r
115                  * パーツイメージを示す.<br>\r
116                  * @param fileContent ファイルコンテンツ\r
117                  * @param categoryLayerPairs カテゴリとレイヤーのペア\r
118                  * @param partsName ファイル名(ファイル名のみ。拡張子を含まない。パーツ名の元として用いることを想定。)\r
119                  * @param pngFileImageHeader PNGファイルヘッダ\r
120                  */\r
121                 protected PartsImageContent(FileContent fileContent,\r
122                                 Collection<CategoryLayerPair> categoryLayerPairs,\r
123                                 String fileName, String partsName, PNGFileImageHeader pngFileImageHeader) {\r
124                         if (fileContent == null || categoryLayerPairs == null\r
125                                         || categoryLayerPairs.isEmpty() || fileName == null\r
126                                         || partsName == null || pngFileImageHeader == null) {\r
127                                 throw new IllegalArgumentException();\r
128                         }\r
129                         this.fileContent = fileContent;\r
130                         this.categoryLayerPairs = Collections.unmodifiableCollection(categoryLayerPairs);\r
131                         this.fileName = fileName;\r
132                         this.partsName = partsName;\r
133                         this.pngFileImageHeader = pngFileImageHeader;\r
134                         \r
135                         CategoryLayerPair categoryLayerPair = categoryLayerPairs.iterator().next();\r
136                         dirName = categoryLayerPair.getLayer().getDir();\r
137                 }\r
138                 \r
139                 @Override\r
140                 public int hashCode() {\r
141                         return getEntryName().hashCode();\r
142                 }\r
143                 \r
144                 @Override\r
145                 public boolean equals(Object obj) {\r
146                         if (obj == this) {\r
147                                 return true;\r
148                         }\r
149                         if (obj != null && obj instanceof PartsImageContent) {\r
150                                 return getEntryName().equals(((PartsImageContent) obj).getEntryName()); \r
151                         }\r
152                         return false;\r
153                 }\r
154                 \r
155                 public Collection<CategoryLayerPair> getCategoryLayerPairs() {\r
156                         return categoryLayerPairs;\r
157                 }\r
158                 \r
159                 public String getDirName() {\r
160                         return dirName;\r
161                 }\r
162                 \r
163                 public String getEntryName() {\r
164                         return fileContent.getEntryName();\r
165                 }\r
166                 \r
167                 public String getFileName() {\r
168                         return fileName;\r
169                 }\r
170                 \r
171                 public String getPartsName() {\r
172                         return partsName;\r
173                 }\r
174                 \r
175                 public PNGFileImageHeader getPngFileImageHeader() {\r
176                         return pngFileImageHeader;\r
177                 }\r
178                 \r
179                 public long lastModified() {\r
180                         return fileContent.lastModified();\r
181                 }\r
182                 \r
183                 public InputStream openStream() throws IOException {\r
184                         return fileContent.openStream();\r
185                 }\r
186                 \r
187                 @Override\r
188                 public String toString() {\r
189                         return fileContent.getEntryName();\r
190                 }\r
191         }\r
192         \r
193         protected HashMap<String, FileContent> entries = new HashMap<String, FileContent>();\r
194 \r
195         protected AbstractCharacterDataArchiveFile(File archiveFile) {\r
196                 if (archiveFile == null) {\r
197                         throw new IllegalArgumentException();\r
198                 }\r
199                 this.archiveFile = archiveFile;\r
200         }\r
201         \r
202         public File getArchiveFile() {\r
203                 return this.archiveFile;\r
204         }\r
205         \r
206         protected void addEntry(FileContent fileContent) {\r
207                 if (fileContent == null) {\r
208                         throw new IllegalArgumentException();\r
209                 }\r
210                 if (logger.isLoggable(Level.FINE)) {\r
211                         logger.log(Level.FINE, fileContent.getEntryName());\r
212                 }\r
213                 entries.put(fileContent.getEntryName(), fileContent);\r
214         }\r
215 \r
216         /**\r
217          * アーカイブファイルをベースとしたURIを返す.<br>\r
218          * @param name コンテンツ名\r
219          * @return URI\r
220          * @throws IOException URIを生成できない場合\r
221          */\r
222         protected URI getContentURI(String name) throws IOException {\r
223                 try {\r
224                         URI baseURI = archiveFile.toURI();\r
225                         return new URI("jar:" + baseURI.toString() + "/!" + name);\r
226 \r
227                 } catch (URISyntaxException ex) {\r
228                         IOException iex = new IOException(ex.getMessage());\r
229                         iex.initCause(ex);\r
230                         throw iex;\r
231                 }\r
232         }\r
233 \r
234         /**\r
235          * 指定したコンテンツが存在するか?\r
236          * @param name コンテンツ名\r
237          * @return 存在すればtrue、存在しなければfalse\r
238          */\r
239         public boolean hasContent(String name) {\r
240                 return entries.containsKey(name);\r
241         }\r
242         \r
243         /**\r
244          * 指定したコンテンツを取得する.<br>\r
245          * 存在しない場合はnullを返す.<br>\r
246          * @param name コンテンツ名\r
247          * @return 存在すればコンテンツ、存在しなければnull\r
248          */\r
249         public FileContent getContent(String name) {\r
250                 return entries.get(name);\r
251         }\r
252         \r
253         public String getRootPrefix() {\r
254                 return this.rootPrefix;\r
255         }\r
256         \r
257         /**\r
258          * アーカイブのルート上に単一のフォルダしかない場合、そのフォルダを真のルートとして設定する.<br>\r
259          * 返されるルート名には末尾に「/」を含む.<br>\r
260          * ルート上に複数のフォルダがあるかファイルが存在する場合は空文字を設定する.<br>\r
261          */\r
262         protected void searchRootPrefix() {\r
263                 HashSet<String> dirs = new HashSet<String>();\r
264                 for (String name : entries.keySet()) {\r
265                         int pos = name.indexOf('/');\r
266                         if (pos < 0) {\r
267                                 // ルート上にファイルがあるので、ここがルート\r
268                                 rootPrefix = "";\r
269                                 return;\r
270                         }\r
271                         if (pos >= 0) {\r
272                                 String dir = name.substring(0, pos + 1);\r
273                                 dirs.add(dir);\r
274                         }\r
275                 }\r
276                 if (dirs.size() == 1) {\r
277                         // ルート上に単一のフォルダしかないので、\r
278                         // このフォルダが真のルート\r
279                         rootPrefix = dirs.iterator().next();\r
280                         return;\r
281                 }\r
282                 // ルート上に複数のフォルダがあるので、ここがルート\r
283                 rootPrefix = "";\r
284         }\r
285         \r
286         /**\r
287          * 指定したディレクトリ(フルパス指定)のファイルのみを取り出す.<br>\r
288          * サブフォルダは含まない.<br>\r
289          * ディレクトリに空文字またはnullまたは「/」を指定した場合はルートを意味する.<br>\r
290          * @param dir ディレクトリ\r
291          * @return ファイルへのフルパスのコレクション\r
292          */\r
293         public Map<String, FileContent> getFiles(String dir) {\r
294                 if (dir == null) {\r
295                         dir = "";\r
296                 }\r
297                 if (dir.length() > 0 && !dir.endsWith("/")) {\r
298                         dir += "/";\r
299                 }\r
300                 if (dir.equals("/")) {\r
301                         dir = ""; // アーカイブ内コンテンツのパスは先頭は「/」ではないため。\r
302                 }\r
303                 \r
304                 HashMap<String, FileContent> files = new HashMap<String, FileContent>();\r
305                 \r
306                 int ep = dir.length();\r
307                 for (Map.Entry<String, FileContent> entry : entries.entrySet()) {\r
308                         String name = entry.getKey();\r
309                         FileContent fileContent = entry.getValue();\r
310                         if (name.startsWith(dir)) {\r
311                                 String suffix = name.substring(ep);\r
312                                 int sep = suffix.indexOf('/');\r
313                                 if (sep < 0) {\r
314                                         files.put(name, fileContent);\r
315                                 }\r
316                         }\r
317                 }\r
318                 \r
319                 return files;\r
320         }\r
321 \r
322         /**\r
323          * キャラクター定義を読み込む.<br>\r
324          * アーカイブに存在しなければnull\r
325          * @return キャラクター定義、もしくはnull\r
326          * @throws IOException 読み取りに失敗した場合\r
327          */\r
328         public CharacterData readCharacterData() throws IOException {\r
329                 FileContent characterFile = entries.get(rootPrefix + CharacterDataPersistent.CONFIG_FILE);\r
330                 if (characterFile == null) {\r
331                         return null;\r
332                 }\r
333                 \r
334                 CharacterDataPersistent persist = CharacterDataPersistent.getInstance();\r
335 \r
336                 // character.xmlとして妥当な文書であるか検査する.\r
337                 DocInfo docInfo;\r
338                 InputStream is = characterFile.openStream();\r
339                 try {\r
340                         docInfo = persist.readDocumentType(is);\r
341                         logger.log(Level.INFO, docInfo == null ? "not xml document" : docInfo.toString());\r
342                 } finally {\r
343                         is.close();\r
344                 }\r
345                 if (docInfo == null || !"character".equals(docInfo.getFirstElementName())) {\r
346                         return null;\r
347                 }\r
348                 \r
349                 // character.xmlを読み込む\r
350                 CharacterData cd;\r
351                 is = characterFile.openStream();\r
352                 try {\r
353                         URI docBase = getContentURI(rootPrefix + CharacterDataPersistent.CONFIG_FILE);\r
354                         cd = persist.loadCharacterDataFromXML(is, docBase, docInfo);\r
355                 } finally {\r
356                         is.close();\r
357                 }\r
358                 \r
359                 return cd;\r
360         }\r
361         \r
362         /**\r
363          * キャラクター定義をINIファイルより読み取る.<br>\r
364          * アーカイブのコンテンツルート上のcharacter.iniを探す.<br>\r
365          * それがなければ、アーカイブ上のどこかにある/character.iniを探して、もっとも短い場所にある1つを採用する.<br>\r
366          * character.iniが何処にも存在しなければnull.<br>\r
367          * 「キャラクターなんとか機 v2.2」の設定ファイルを想定している.<br>\r
368          * @return キャラクター定義、もしくはnull\r
369          * @throws IOException 読み取りに失敗した場合\r
370          */\r
371         public CharacterData readCharacterINI() throws IOException {\r
372                 FileContent characterFile = null;\r
373                 characterFile = entries.get(rootPrefix + CharacterDataPersistent.COMPATIBLE_CONFIG_NAME);\r
374                 if (characterFile == null) {\r
375                         // どこかにあるcharacter.iniを探す\r
376                         ArrayList<String> characterInis = new ArrayList<String>();\r
377                         for (Map.Entry<String, FileContent> entry : entries.entrySet()) {\r
378                                 String entryName = entry.getKey();\r
379                                 if (entryName.endsWith("/" + CharacterDataPersistent.COMPATIBLE_CONFIG_NAME)) {\r
380                                         characterInis.add(entryName);\r
381                                 }\r
382                         }\r
383                         // もっとも短い名前のものを採用\r
384                         Collections.sort(characterInis);\r
385                         if (characterInis.size() > 0) {\r
386                                 characterFile = entries.get(characterInis.get(0));\r
387                         }\r
388                 }\r
389                 if (characterFile == null) {\r
390                         // character.iniがないので何もしない.\r
391                         return null;\r
392                 }\r
393 \r
394                 // デフォルトのキャラクター定義を構築する.\r
395                 CharacterDataPersistent persist = CharacterDataPersistent.getInstance();\r
396                 CharacterData cd;\r
397                 InputStream is = characterFile.openStream();\r
398                 try {\r
399                         cd = persist.readCharacterDataFromIni(is);\r
400                 } finally {\r
401                         is.close();\r
402                 }\r
403 \r
404                 // docBaseを設定する.\r
405                 URI docBase = getContentURI(rootPrefix + CharacterDataPersistent.COMPATIBLE_CONFIG_NAME);\r
406                 cd.setDocBase(docBase);\r
407 \r
408                 return cd;\r
409         }\r
410         \r
411         /**\r
412          * お気に入りを読み込みキャラクター定義に追加する.<br>\r
413          * アーカイブにお気に入り(favorites.xml)が存在しなければ何もしない.<br>\r
414          * @param characterData キャラクター定義(お気に入りが追加される)\r
415          * @throws IOException 読み取りに失敗した場合\r
416          */\r
417         public void readFavorites(CharacterData characterData) throws IOException {\r
418                 if (characterData == null) {\r
419                         throw new IllegalArgumentException("characterDataにnullは指定できません。");\r
420                 }\r
421                 FileContent favoritesXml = entries.get(rootPrefix + "favorites.xml");\r
422                 if (favoritesXml == null) {\r
423                         // favorites.xmlがなければ何もしない\r
424                         return;\r
425                 }\r
426 \r
427                 CharacterDataPersistent persist = CharacterDataPersistent.getInstance();\r
428 \r
429                 // favorites.xmlとして妥当な文書であるか検査する.\r
430                 DocInfo docInfo;\r
431                 InputStream is = favoritesXml.openStream();\r
432                 try {\r
433                         docInfo = persist.readDocumentType(is);\r
434                         logger.log(Level.INFO, docInfo == null ? "not xml document" : docInfo.toString());\r
435                 } finally {\r
436                         is.close();\r
437                 }\r
438                 if (docInfo == null || !"partssets".equals(docInfo.getFirstElementName())) {\r
439                         // favorites.xmlの文書でない場合は何もしない.\r
440                         return;\r
441                 }\r
442                 \r
443                 // favorites.xmlを読み込む\r
444                 is = favoritesXml.openStream();\r
445                 try {\r
446                         persist.loadPartsSet(characterData, is, docInfo);\r
447 \r
448                 } catch (Exception ex) {\r
449                         logger.log(Level.INFO, "favorites.xml load failed.", ex);\r
450                 \r
451                 } finally {\r
452                         is.close();\r
453                 }\r
454         }\r
455         \r
456         /**\r
457          * サンプルピクチャを読み込む.<br>\r
458          * アーカイブに存在しなければnull.\r
459          * @return サンプルピクチャ、もしくはnull\r
460          * @throws IOException 読み取りに失敗した場合\r
461          */\r
462         public BufferedImage readSamplePicture() throws IOException {\r
463                 FileContent samplePictureFile = entries.get(rootPrefix + "preview.png");\r
464                 if (samplePictureFile == null) {\r
465                         Map<String, FileContent> files = getFiles(rootPrefix);\r
466                         \r
467                         samplePictureFile = files.get("preview.jpg");\r
468                         if (samplePictureFile == null) {\r
469                                 samplePictureFile = files.get("preview.jpeg");\r
470                         }\r
471                         \r
472                         if (samplePictureFile == null) {\r
473                                 for (Map.Entry<String, FileContent> entry : files.entrySet()) {\r
474                                         String name = entry.getKey();\r
475                                         if (name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".png")) {\r
476                                                 samplePictureFile = entry.getValue();\r
477                                                 break;\r
478                                         }\r
479                                 }\r
480                         }\r
481                 }\r
482                 if (samplePictureFile == null) {\r
483                         return null;\r
484                 }\r
485                 \r
486                 BufferedImage pic;\r
487                 InputStream is = samplePictureFile.openStream();\r
488                 try {\r
489                         pic = ImageIO.read(is);\r
490                 } finally {\r
491                         is.close();\r
492                 }\r
493                 \r
494                 return pic;\r
495         }\r
496         \r
497         /**\r
498          * アーカイブにある「readme.txt」、もしくは「readme」というファイルをテキストファイルとして読み込む.<br>\r
499          * readme.txtが優先される。ともに存在しない場合はnull.<br> \r
500          * 改行コードはプラットフォーム固有の改行コードに変換して返される.<br>\r
501          * @return テキスト、もしくはnull\r
502          * @throws 読み込みに失敗した場合\r
503          */\r
504         public String readReadMe() throws IOException {\r
505                 \r
506                 Locale locale = Locale.getDefault();\r
507                 String lang = locale.getLanguage();\r
508 \r
509                 ArrayList<FileContent> candiates = new ArrayList<FileContent>();\r
510 \r
511                 Map<String, FileContent> files = getFiles(rootPrefix);\r
512                 for (String findName : new String[] {\r
513                                 "readme_" + lang + ".txt", "readme_" + lang, "readme.txt", "readme", null }) {\r
514                         for (Map.Entry<String, FileContent> entry : files.entrySet()) {\r
515                                 String name = entry.getKey().toLowerCase();\r
516                                 if (findName == null && name.endsWith(".txt")) {\r
517                                         candiates.add(entry.getValue());\r
518                                         break;\r
519                                 }\r
520                                 if (name.equals(findName)) {\r
521                                         candiates.add(entry.getValue());\r
522                                 }\r
523                         }\r
524                 }\r
525                 if (candiates.isEmpty()) {\r
526                         return null;\r
527                 }\r
528 \r
529                 // みつかったファイルについて読み込みを優先順で試行する.\r
530                 for (FileContent file : candiates) {\r
531                         try {\r
532                                 return readTextUTF16(file);\r
533 \r
534                         } catch (Exception ex) {\r
535                                 logger.log(Level.WARNING, "read file failed. :" + file, ex);\r
536                                 // 無視して次の候補を試行する.\r
537                         }\r
538                 }\r
539 \r
540                 // すべて失敗したか、そもそもファイルがみつからなかった。\r
541                 return null;\r
542         }\r
543         \r
544         /**\r
545          * ファイルをテキストとして読み取り、返す.<br>\r
546          * UTF-16LE/BE/UTF-8についてはBOMにより判定する.<br>\r
547          * BOMがない場合はUTF-16/8ともに判定できない.<br>\r
548          * BOMがなければMS932もしくはEUC_JPであると仮定して読み込む.<br>\r
549          * 改行コードはプラットフォーム固有の改行コードに変換して返される.<br>\r
550          * @param name コンテンツ名 \r
551          * @return テキスト、コンテンツが存在しない場合はnull\r
552          * @throws IOException 読み込みに失敗した場合\r
553          */\r
554         public String readTextFile(String name) throws IOException {\r
555                 if (name == null) {\r
556                         throw new IllegalArgumentException();\r
557                 }\r
558                 FileContent content = entries.get(name);\r
559                 if (content == null) {\r
560                         return null;\r
561                 }\r
562                 return readTextUTF16(content);\r
563         }\r
564         \r
565         /**\r
566          * ファイルをテキストとして読み取り、返す.<br>\r
567          * UTF-16LE/BE/UTF-8についてはBOMにより判定する.<br>\r
568          * BOMがない場合はUTF-16/8ともに判定できない.<br>\r
569          * BOMがなければMS932もしくはEUC_JPであると仮定して読み込む.<br>\r
570          * @param content コンテンツ \r
571          * @return テキスト\r
572          * @throws IOException 読み込みに失敗した場合\r
573          */\r
574         public String readTextUTF16(FileContent content) throws IOException {\r
575                 if (content == null) {\r
576                         throw new IllegalArgumentException();\r
577                 }\r
578 \r
579                 // 一旦メモリに取り込む\r
580                 ByteArrayOutputStream bos = new ByteArrayOutputStream();\r
581                 InputStream is = content.openStream();\r
582                 try {\r
583                         int ch;\r
584                         while ((ch = is.read()) != -1) {\r
585                                 bos.write((byte) ch);\r
586                         }\r
587                 } finally {\r
588                         is.close();\r
589                 }\r
590                 byte[] buf = bos.toByteArray();\r
591                 \r
592                 String enc = null;\r
593                 if (buf.length >= 2) {\r
594                         // Windowsのメモ帳はUTF-16にBOMをつけるので、これで判定できる。\r
595                         // 本アプリケーションのエクスポート時もUTF-16LEのBOM付きで出力する。\r
596                         // 一般的なエディタはUTF-16BEにはBOMをつけないので、事前に判定することはできない。\r
597                         if ((buf[0] & 0xff) == 0xff && (buf[1] & 0xff) == 0xfe) {\r
598                                 enc = "UTF-16LE";\r
599                         } else if ((buf[0] & 0xff) == 0xfe && (buf[1] & 0xff) == 0xff) {\r
600                                 enc = "UTF-16BE";\r
601                         }\r
602                 }\r
603                 if (enc == null && buf.length >= 3) {\r
604                         if ((buf[0] & 0xff) == 0xef && (buf[1] & 0xff) == 0xbb && (buf[1] & 0xff) == 0xbf) {\r
605                                 // Windowsのメモ帳などはUTF-8にBOMをつけるので、これで判定できる。\r
606                                 // 一般的なエディタではUTF-8のBOMはつけないのでUTF-8であるかどうかを事前判定することはできない。\r
607                                 enc = "UTF-8";\r
608                         }\r
609                 }\r
610                 if (enc == null) {\r
611                         // BOMがない場合はMS932かEUC_JPのいずれかであろう、と仮定する。\r
612                         enc = "JISAutoDetect"; // SJIS/EUC_JPの自動判定\r
613                 }\r
614 \r
615                 // 文字列として変換\r
616                 StringBuilder str = new StringBuilder();\r
617                 InputStreamReader rd = new InputStreamReader(new ByteArrayInputStream(buf), enc);\r
618                 try {\r
619                         int ch;\r
620                         while ((ch = rd.read()) != -1) {\r
621                                 str.append((char) ch);\r
622                         }\r
623                 } finally {\r
624                         rd.close();\r
625                 }\r
626                 \r
627                 // 改行コードをプラットフォーム固有のものに変換\r
628                 String line = str.toString();\r
629                 line = line.replace("\r\n", "\n");\r
630                 line = line.replace("\r", "\n");\r
631                 line = line.replace("\n", System.getProperty("line.separator"));\r
632 \r
633                 return line;\r
634         }\r
635 \r
636         /**\r
637          * キャラクター定義のカテゴリと、そのレイヤー情報から、画像のディレクトリの一覧をアーカイブ上のディレクトリの一覧として返す.<br>\r
638          * ディレクトリの末尾は常にスラ付きとなる.<br>\r
639          * enabledRootPefixがtrueの場合、ディレクトリの先頭はアーカイブのコンテンツルートとなる.<br>\r
640          * 同一のディレクトリに対して複数のレイヤー(複数カテゴリを含む)が参照している場合、それらを含めて返す.<br>\r
641          * 参照されているディレクトリがない場合は返される結果には含まれない.<br>\r
642          * @param characterData キャラクター定義\r
643          * @param enabledRootPrefix ルートプレフィックス(アーカイブのコンテンツルート)を付与する場合\r
644          * @return パーツで使用する画像のディレクトリとして認識されるディレクトリの一覧、キーはアーカイブのディレクトリ位置、値は参照する1つ以上のレイヤー\r
645          */\r
646         protected Map<String, Collection<CategoryLayerPair>> getLayerDirs(CharacterData characterData, boolean enabledRootPrefix) {\r
647                 if (characterData == null) {\r
648                         return Collections.emptyMap();\r
649                 }\r
650                 // イメージディレクトリの一覧\r
651                 String rootPrefix = getRootPrefix();\r
652                 HashMap<String, Collection<CategoryLayerPair>> layerDirs = new HashMap<String, Collection<CategoryLayerPair>>();\r
653                 for (PartsCategory partsCategory : characterData.getPartsCategories()) {\r
654                         for (Layer layer : partsCategory.getLayers()) {\r
655                                 String dir = layer.getDir();\r
656                                 if (!dir.endsWith("/")) {\r
657                                         dir += "/"; // スラ付きにする.\r
658                                 }\r
659                                 if (enabledRootPrefix) {\r
660                                         // アーカイブのルートコンテキストからのディレクトリ位置とする.\r
661                                         dir = rootPrefix + dir;\r
662                                 }\r
663                                 Collection<CategoryLayerPair> sameDirLayers = layerDirs.get(dir);\r
664                                 if (sameDirLayers == null) {\r
665                                         sameDirLayers = new ArrayList<CategoryLayerPair>();\r
666                                         layerDirs.put(dir, sameDirLayers);\r
667                                 }\r
668                                 sameDirLayers.add(new CategoryLayerPair(partsCategory, layer));\r
669                         }\r
670                 }\r
671                 return layerDirs;\r
672         }\r
673 \r
674         /**\r
675          * アーカイブに含まれるフォルダをもつpngファイルからパーツイメージを取得する.<br>\r
676          * \r
677          * @param インポート先のキャラクターデータ\r
678          *            、フォルダ名などを判別するため。nullの場合はディレクトリなしとみなす.<br>\r
679          * @param newly\r
680          *            新規インポート用であるか?(新規でない場合は引数で指定したキャラクターセットと同じパーツは読み込まれない).\r
681          *            (アーカイブファイルからの読み込みでは無視される)\r
682          * @return パーツイメージコンテンツのコレクション、なければ空\r
683          */\r
684         public Collection<PartsImageContent> getPartsImageContents(CharacterData characterData, boolean newly) {\r
685                 // コンテンツルートからの絶対位置指定でパーツイメージを取得する.\r
686                 Collection<PartsImageContent> results = getPartsImageContentsStrict(characterData);\r
687                 if (results.isEmpty()) {\r
688                         // コンテンツルートからの絶対位置にパーツがない場合は、任意のディレクトリ位置からパーツイメージを推定する.\r
689                         results = getPartsImageContentsLazy(characterData);\r
690                 }\r
691                 return results;\r
692         }\r
693 \r
694         /**\r
695          * コンテンツルートからの絶対位置のフォルダからpngファイルからパーツイメージを取得する.<br>\r
696          * @param インポート先のキャラクターデータ、フォルダ名などを判別するため。nullの場合はディレクトリなしとみなす.<br>\r
697          * @return パーツイメージコンテンツのコレクション、なければ空\r
698          */\r
699         protected Collection<PartsImageContent> getPartsImageContentsStrict(CharacterData characterData) {\r
700                 final Map<String, Collection<CategoryLayerPair>> layerDirMap = getLayerDirs(characterData, true);\r
701                 \r
702                 CategoryLayerPairResolveStrategy strategy = new CategoryLayerPairResolveStrategy() {\r
703                         public Collection<CategoryLayerPair> resolveCategoryLayerPairs(String dir) {\r
704                                 Collection<CategoryLayerPair> categoryLayerPairs = layerDirMap.get(dir);\r
705                                 if (categoryLayerPairs == null || categoryLayerPairs.isEmpty()) {\r
706                                         // ディレクトリ名に一致するものがないので、この画像は無視する\r
707                                         return null;\r
708                                 }\r
709                                 return categoryLayerPairs;\r
710                         }\r
711                 };\r
712                 \r
713                 return getPartsImageContents(strategy);\r
714         }\r
715         \r
716         /**\r
717          * アーカイブに含まれる任意のフォルダからpngファイルからパーツイメージを取得する.<br>\r
718          * ディレクトリ名の大文字・小文字は区別されません.<br>\r
719          * @param インポート先のキャラクターデータ、フォルダ名などを判別するため。nullの場合はディレクトリなしとみなす.<br>\r
720          * @return パーツイメージコンテンツのコレクション、なければ空\r
721          */\r
722         protected Collection<PartsImageContent> getPartsImageContentsLazy(CharacterData characterData) {\r
723                 final Map<String, Collection<CategoryLayerPair>> layerDirMap = getLayerDirs(characterData, false);\r
724                 \r
725                 CategoryLayerPairResolveStrategy strategy = new CategoryLayerPairResolveStrategy() {\r
726                         public Collection<CategoryLayerPair> resolveCategoryLayerPairs(String dir) {\r
727                                 dir = (dir == null) ? "" : dir.toLowerCase();\r
728                                 for (Map.Entry<String, Collection<CategoryLayerPair>> entry : layerDirMap.entrySet()) {\r
729                                         String dirSuffix = entry.getKey().toLowerCase();\r
730                                         Collection<CategoryLayerPair> categoryLayerPairs = entry.getValue();\r
731                                         if (dir.endsWith(dirSuffix)) {\r
732                                                 return categoryLayerPairs;\r
733                                         }\r
734                                 }\r
735                                 return null;\r
736                         }\r
737                 };\r
738                 \r
739                 return getPartsImageContents(strategy);\r
740         }\r
741 \r
742         /**\r
743          * ディレクトリ名からカテゴリとレイヤーを取得するためのインターフェイス.<br>\r
744          * @author seraphy\r
745          */\r
746         protected interface CategoryLayerPairResolveStrategy {\r
747                 \r
748                 /**\r
749                  * ディレクトリを指定して、それに該当するカテゴリとレイヤーペアのコレクションを返します.<br>\r
750                  * 同一のディレクトリに対して複数のレイヤーが割り当てられている可能性があるためコレクションで返されます.<br>\r
751                  * 空のコレクションにはなりません.<br>\r
752                  * レイヤーとして認識されていないディレクトリの場合はnullを返します.<br>\r
753                  * @param dir ディレクトリ\r
754                  * @return カテゴリ・レイヤーのペアのコレクション、またはnull (空のコレクションにはならない。)\r
755                  */\r
756                 Collection<CategoryLayerPair> resolveCategoryLayerPairs(String dir);\r
757                 \r
758         }\r
759         \r
760         /**\r
761          * アーカイブに含まれるフォルダをもつpngファイルからパーツイメージを取得する。\r
762          * @param strategy ディレクトリが売れ入れ可能であるか判断するストラテジー\r
763          * @return パーツイメージコンテンツのコレクション、なければ空\r
764          */\r
765         protected Collection<PartsImageContent> getPartsImageContents(CategoryLayerPairResolveStrategy strategy) {\r
766                 if (strategy == null) {\r
767                         throw new IllegalArgumentException();\r
768                 }\r
769 \r
770                 ArrayList<PartsImageContent> results = new ArrayList<PartsImageContent>();\r
771                 for (Map.Entry<String, FileContent> entry : entries.entrySet()) {\r
772                         String name = entry.getKey();\r
773                         FileContent fileContent = entry.getValue();\r
774 \r
775                         String[] split = name.split("/");\r
776                         if (split.length < 2) {\r
777                                 // 最低でもフォルダ下になければならない\r
778                                 continue;\r
779                         }\r
780                         String lastName = split[split.length - 1];\r
781                         if (!lastName.toLowerCase().endsWith(".png")) {\r
782                                 // png拡張子でなければならない\r
783                                 continue;\r
784                         }\r
785 \r
786                         // ディレクトリ名\r
787                         String dir = name.substring(0, name.length() - lastName.length());\r
788 \r
789                         // ディレクトリ名から対応するレイヤーを取得します.\r
790                         Collection<CategoryLayerPair> categoryLayerPairs = strategy.resolveCategoryLayerPairs(dir);\r
791                         if (categoryLayerPairs == null) {\r
792                                 // パーツイメージのディレクトリとして定義されていない場合は、この画像は無視される.\r
793                                 continue;\r
794                         }\r
795                         \r
796                         // PNGファイルヘッダの取得と確認\r
797                         PNGFileImageHeader pngFileHeader = readPNGFileHeader(fileContent);\r
798                         if (pngFileHeader == null) {\r
799                                 // PNGファイルとして不正なものは無視する.\r
800                                 logger.log(Level.WARNING, "invalid png: " + name);\r
801                                 continue;\r
802                         }\r
803 \r
804                         // パーツ名(拡張子を除いたもの)\r
805                         String partsName;\r
806                         int extpos = lastName.lastIndexOf('.');\r
807                         partsName = lastName.substring(0, extpos);\r
808                         \r
809                         PartsImageContent partsImageContent = new PartsImageContent(\r
810                                         fileContent, categoryLayerPairs, lastName, partsName, pngFileHeader);\r
811 \r
812                         results.add(partsImageContent);\r
813                 }\r
814                 return results;\r
815         }\r
816         \r
817         /**\r
818          * PNGファイルとしてファイルを読み込みPNGヘッダ情報を返します.<br>\r
819          * PNGでないか、ファイルの読み込みに失敗した場合はnullを返します.<br>\r
820          * @param fileContent 画像ファイル\r
821          * @return PNGヘッダ情報、またはnull\r
822          */\r
823         protected PNGFileImageHeader readPNGFileHeader(FileContent fileContent) {\r
824                 PNGFileImageHeaderReader pngHeaderReader = PNGFileImageHeaderReader.getInstance();\r
825                 PNGFileImageHeader pngFileHeader = null;\r
826                 try {\r
827                         InputStream is = fileContent.openStream();\r
828                         try {\r
829                                 pngFileHeader = pngHeaderReader.readHeader(is);\r
830                         } finally {\r
831                                 is.close();\r
832                         }\r
833                 } catch (IOException ex) {\r
834                         logger.log(Level.WARNING, "not png header. " + fileContent, ex);\r
835                         pngFileHeader = null;\r
836                         // 無視する.\r
837                 }\r
838                 return pngFileHeader;\r
839         }\r
840         \r
841         /**\r
842          * アーカイブに含まれるparts-info.xmlを読み込み返す.<br>\r
843          * 存在しなければ空のインスタンスを返す.<br>\r
844          * @return パーツ管理データ\r
845          * @throws IOException\r
846          */\r
847         public PartsManageData getPartsManageData() throws IOException {\r
848                 FileContent content = entries.get(rootPrefix + "parts-info.xml");\r
849                 if (content == null) {\r
850                         return new PartsManageData();\r
851                 }\r
852                 \r
853                 PartsManageData partsManageData;\r
854 \r
855                 InputStream is = content.openStream();\r
856                 try {\r
857                         CharacterDataPersistent persist = CharacterDataPersistent.getInstance();\r
858                         partsManageData = persist.loadPartsManageData(is);\r
859                 } finally {\r
860                         is.close();\r
861                 }\r
862                 \r
863                 return partsManageData;\r
864         }\r
865 }\r