1 package be
.nikiroo
.fanfix
;
3 import java
.awt
.image
.BufferedImage
;
5 import java
.io
.FileFilter
;
6 import java
.io
.IOException
;
8 import java
.util
.ArrayList
;
9 import java
.util
.Collections
;
10 import java
.util
.HashMap
;
11 import java
.util
.List
;
13 import java
.util
.Map
.Entry
;
15 import be
.nikiroo
.fanfix
.bundles
.Config
;
16 import be
.nikiroo
.fanfix
.data
.MetaData
;
17 import be
.nikiroo
.fanfix
.data
.Story
;
18 import be
.nikiroo
.fanfix
.output
.BasicOutput
;
19 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
20 import be
.nikiroo
.fanfix
.supported
.BasicSupport
;
21 import be
.nikiroo
.fanfix
.supported
.BasicSupport
.SupportType
;
22 import be
.nikiroo
.fanfix
.supported
.InfoReader
;
23 import be
.nikiroo
.utils
.IOUtils
;
24 import be
.nikiroo
.utils
.Progress
;
27 * Manage a library of Stories: import, export, list.
29 * Each {@link Story} object will be associated with a (local to the library)
30 * unique ID, the LUID, which will be used to identify the {@link Story}.
32 * Most of the {@link Library} functions work on either the LUID or a partial
33 * (cover not included) {@link MetaData} object.
37 public class Library
{
39 private Map
<MetaData
, File
> stories
;
41 private OutputType text
;
42 private OutputType image
;
45 * Create a new {@link Library} with the given backend directory.
48 * the directory where to find the {@link Story} objects
50 * the {@link OutputType} to save the text-focused stories into
52 * the {@link OutputType} to save the images-focused stories into
54 public Library(File dir
, OutputType text
, OutputType image
) {
56 this.stories
= new HashMap
<MetaData
, File
>();
65 * Refresh the {@link Library}, that is, make sure all stories are loaded.
68 * the optional progress reporter
70 public void refresh(Progress pg
) {
75 * List all the known types of stories.
79 public synchronized List
<String
> getTypes() {
80 List
<String
> list
= new ArrayList
<String
>();
81 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
82 String storyType
= entry
.getKey().getSource();
83 if (!list
.contains(storyType
)) {
88 Collections
.sort(list
);
93 * List all the known authors of stories.
97 public synchronized List
<String
> getAuthors() {
98 List
<String
> list
= new ArrayList
<String
>();
99 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
100 String storyAuthor
= entry
.getKey().getAuthor();
101 if (!list
.contains(storyAuthor
)) {
102 list
.add(storyAuthor
);
106 Collections
.sort(list
);
111 * List all the stories of the given author in the {@link Library}, or all
112 * the stories if NULL is passed as an author.
114 * Cover images not included.
117 * the author of the stories to retrieve, or NULL for all
119 * @return the stories
121 public synchronized List
<MetaData
> getListByAuthor(String author
) {
122 List
<MetaData
> list
= new ArrayList
<MetaData
>();
123 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
124 String storyAuthor
= entry
.getKey().getAuthor();
125 if (author
== null || author
.equalsIgnoreCase(storyAuthor
)) {
126 list
.add(entry
.getKey());
130 Collections
.sort(list
);
135 * List all the stories of the given source type in the {@link Library}, or
136 * all the stories if NULL is passed as a type.
138 * Cover images not included.
141 * the type of story to retrieve, or NULL for all
143 * @return the stories
145 public synchronized List
<MetaData
> getListByType(String type
) {
146 List
<MetaData
> list
= new ArrayList
<MetaData
>();
147 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
148 String storyType
= entry
.getValue().getParentFile().getName();
149 if (type
== null || type
.equalsIgnoreCase(storyType
)) {
150 list
.add(entry
.getKey());
154 Collections
.sort(list
);
159 * Retrieve a {@link File} corresponding to the given {@link Story}, cover
160 * image not included.
163 * the Library UID of the story
165 * @return the corresponding {@link Story}
167 public synchronized MetaData
getInfo(String luid
) {
169 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
170 if (luid
.equals(entry
.getKey().getLuid())) {
171 return entry
.getKey();
180 * Retrieve a {@link File} corresponding to the given {@link Story}.
183 * the Library UID of the story
185 * @return the corresponding {@link Story}
187 public synchronized File
getFile(String luid
) {
189 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
190 if (luid
.equals(entry
.getKey().getLuid())) {
191 return entry
.getValue();
200 * Return the cover image associated to this story.
203 * the Library UID of the story
205 * @return the cover image
207 public synchronized BufferedImage
getCover(String luid
) {
208 MetaData meta
= getInfo(luid
);
211 File infoFile
= new File(getFile(meta
).getPath() + ".info");
212 meta
= readMeta(infoFile
, true).getKey();
213 return meta
.getCover();
214 } catch (IOException e
) {
223 * Retrieve a specific {@link Story}.
226 * the Library UID of the story
228 * the optional progress reporter
230 * @return the corresponding {@link Story} or NULL if not found
232 public synchronized Story
getStory(String luid
, Progress pg
) {
234 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
235 if (luid
.equals(entry
.getKey().getLuid())) {
237 SupportType type
= SupportType
.valueOfAllOkUC(entry
238 .getKey().getType());
239 URL url
= entry
.getValue().toURI().toURL();
241 return BasicSupport
.getSupport(type
).process(url
,
244 throw new IOException("Unknown type: "
245 + entry
.getKey().getType());
247 } catch (IOException e
) {
248 // We should not have not-supported files in the
250 Instance
.syserr(new IOException(
251 "Cannot load file from library: "
252 + entry
.getValue().getPath(), e
));
267 * Import the {@link Story} at the given {@link URL} into the
271 * the {@link URL} to import
273 * the optional progress reporter
275 * @return the imported {@link Story}
277 * @throws IOException
278 * in case of I/O error
280 public Story
imprt(URL url
, Progress pg
) throws IOException
{
281 BasicSupport support
= BasicSupport
.getSupport(url
);
282 if (support
== null) {
283 throw new IOException("URL not supported: " + url
.toString());
286 return save(support
.process(url
, pg
), null);
290 * Export the {@link Story} to the given target in the given format.
293 * the {@link Story} ID
295 * the {@link OutputType} to transform it to
297 * the target to save to
299 * the optional progress reporter
301 * @return the saved resource (the main saved {@link File})
303 * @throws IOException
304 * in case of I/O error
306 public File
export(String luid
, OutputType type
, String target
, Progress pg
)
308 Progress pgGetStory
= new Progress();
309 Progress pgOut
= new Progress();
312 pg
.addProgress(pgGetStory
, 1);
313 pg
.addProgress(pgOut
, 1);
316 BasicOutput out
= BasicOutput
.getOutput(type
, true);
318 throw new IOException("Output type not supported: " + type
);
321 Story story
= getStory(luid
, pgGetStory
);
323 throw new IOException("Cannot find story to export: " + luid
);
326 return out
.process(story
, target
, pgOut
);
330 * Save a {@link Story} to the {@link Library}.
333 * the {@link Story} to save
335 * the optional progress reporter
337 * @return the same {@link Story}, whose LUID may have changed
339 * @throws IOException
340 * in case of I/O error
342 public Story
save(Story story
, Progress pg
) throws IOException
{
343 return save(story
, null, pg
);
347 * Save a {@link Story} to the {@link Library} -- the LUID <b>must</b> be
348 * correct, or NULL to get the next free one.
351 * the {@link Story} to save
353 * the <b>correct</b> LUID or NULL to get the next free one
355 * the optional progress reporter
357 * @return the same {@link Story}, whose LUID may have changed
359 * @throws IOException
360 * in case of I/O error
362 public synchronized Story
save(Story story
, String luid
, Progress pg
)
364 // Do not change the original metadata, but change the original story
365 MetaData key
= story
.getMeta().clone();
368 if (luid
== null || luid
.isEmpty()) {
369 getStories(null); // refresh lastId if needed
370 key
.setLuid(String
.format("%03d", (++lastId
)));
375 getDir(key
).mkdirs();
376 if (!getDir(key
).exists()) {
377 throw new IOException("Cannot create library dir");
381 if (key
!= null && key
.isImageDocument()) {
387 BasicOutput it
= BasicOutput
.getOutput(out
, true);
388 it
.process(story
, getFile(key
).getPath(), pg
);
397 * Delete the given {@link Story} from this {@link Library}.
400 * the LUID of the target {@link Story}
402 * @return TRUE if it was deleted
404 public synchronized boolean delete(String luid
) {
407 MetaData meta
= getInfo(luid
);
408 File file
= getStories(null).get(meta
);
412 String readerExt
= getOutputType(meta
)
413 .getDefaultExtension(true);
414 String fileExt
= getOutputType(meta
).getDefaultExtension(false);
416 String path
= file
.getAbsolutePath();
417 if (readerExt
!= null && !readerExt
.equals(fileExt
)) {
419 .substring(0, path
.length() - readerExt
.length())
421 file
= new File(path
);
422 IOUtils
.deltree(file
);
425 File infoFile
= new File(path
+ ".info");
426 if (!infoFile
.exists()) {
427 infoFile
= new File(path
.substring(0, path
.length()
433 String coverExt
= "."
434 + Instance
.getConfig().getString(
435 Config
.IMAGE_FORMAT_COVER
);
436 File coverFile
= new File(path
+ coverExt
);
437 if (!coverFile
.exists()) {
438 coverFile
= new File(path
.substring(0, path
.length()
439 - fileExt
.length()));
454 * The directory (full path) where the {@link Story} related to this
455 * {@link MetaData} should be located on disk.
458 * the {@link Story} {@link MetaData}
460 * @return the target directory
462 private File
getDir(MetaData key
) {
463 String source
= key
.getSource().replaceAll("[^a-zA-Z0-9._+-]", "_");
464 return new File(baseDir
, source
);
468 * The target (full path) where the {@link Story} related to this
469 * {@link MetaData} should be located on disk.
472 * the {@link Story} {@link MetaData}
476 private File
getFile(MetaData key
) {
477 String title
= key
.getTitle();
481 title
= title
.replaceAll("[^a-zA-Z0-9._+-]", "_");
482 return new File(getDir(key
), key
.getLuid() + "_" + title
);
486 * Return all the known stories in this {@link Library} object.
489 * the optional progress reporter
491 * @return the stories
493 private synchronized Map
<MetaData
, File
> getStories(Progress pg
) {
497 pg
.setMinMax(0, 100);
500 if (stories
.isEmpty()) {
503 File
[] dirs
= baseDir
.listFiles(new FileFilter() {
504 public boolean accept(File file
) {
505 return file
!= null && file
.isDirectory();
509 Progress pgDirs
= new Progress(0, 100 * dirs
.length
);
510 pg
.addProgress(pgDirs
, 100);
512 for (File dir
: dirs
) {
513 File
[] files
= dir
.listFiles(new FileFilter() {
514 public boolean accept(File file
) {
516 && file
.getPath().toLowerCase()
521 Progress pgFiles
= new Progress(0, files
.length
);
522 pgDirs
.addProgress(pgFiles
, 100);
523 pgDirs
.setName("Loading from: " + dir
.getName());
525 for (File file
: files
) {
526 pgFiles
.setName(file
.getName());
528 Entry
<MetaData
, File
> entry
= readMeta(file
, false);
530 int id
= Integer
.parseInt(entry
.getKey().getLuid());
535 stories
.put(entry
.getKey(), entry
.getValue());
536 } catch (Exception e
) {
538 throw new IOException(
539 "Cannot understand the LUID of "
540 + file
.getPath() + ": "
541 + entry
.getKey().getLuid(), e
);
543 } catch (IOException e
) {
544 // We should not have not-supported files in the
546 Instance
.syserr(new IOException(
547 "Cannot load file from library: "
548 + file
.getPath(), e
));
553 pgFiles
.setName(null);
556 pgDirs
.setName("Loading directories");
562 private Entry
<MetaData
, File
> readMeta(File infoFile
, boolean withCover
)
565 final MetaData meta
= InfoReader
.readMeta(infoFile
, withCover
);
567 // Replace .info with whatever is needed:
568 String path
= infoFile
.getPath();
569 path
= path
.substring(0, path
.length() - ".info".length());
571 String newExt
= getOutputType(meta
).getDefaultExtension(true);
573 File targetFile
= new File(path
+ newExt
);
575 final File ffile
= targetFile
;
576 return new Entry
<MetaData
, File
>() {
577 public File
setValue(File value
) {
581 public File
getValue() {
585 public MetaData
getKey() {
592 * Return the {@link OutputType} for this {@link Story}.
595 * the {@link Story} {@link MetaData}
599 private OutputType
getOutputType(MetaData meta
) {
600 if (meta
!= null && meta
.isImageDocument()) {