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
.output
.InfoCover
;
21 import be
.nikiroo
.fanfix
.supported
.BasicSupport
;
22 import be
.nikiroo
.fanfix
.supported
.BasicSupport
.SupportType
;
23 import be
.nikiroo
.fanfix
.supported
.InfoReader
;
24 import be
.nikiroo
.utils
.IOUtils
;
25 import be
.nikiroo
.utils
.Progress
;
28 * Manage a library of Stories: import, export, list.
30 * Each {@link Story} object will be associated with a (local to the library)
31 * unique ID, the LUID, which will be used to identify the {@link Story}.
33 * Most of the {@link Library} functions work on either the LUID or a partial
34 * (cover not included) {@link MetaData} object.
38 public class Library
{
40 private Map
<MetaData
, File
> stories
;
42 private OutputType text
;
43 private OutputType image
;
46 * Create a new {@link Library} with the given backend directory.
49 * the directory where to find the {@link Story} objects
51 * the {@link OutputType} to save the text-focused stories into
53 * the {@link OutputType} to save the images-focused stories into
55 public Library(File dir
, OutputType text
, OutputType image
) {
57 this.stories
= new HashMap
<MetaData
, File
>();
66 * Refresh the {@link Library}, that is, make sure all stories are loaded.
69 * the optional progress reporter
71 public void refresh(Progress pg
) {
76 * List all the known types (sources) of stories.
80 public synchronized List
<String
> getTypes() {
81 List
<String
> list
= new ArrayList
<String
>();
82 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
83 String storyType
= entry
.getKey().getSource();
84 if (!list
.contains(storyType
)) {
89 Collections
.sort(list
);
94 * List all the known authors of stories.
98 public synchronized List
<String
> getAuthors() {
99 List
<String
> list
= new ArrayList
<String
>();
100 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
101 String storyAuthor
= entry
.getKey().getAuthor();
102 if (!list
.contains(storyAuthor
)) {
103 list
.add(storyAuthor
);
107 Collections
.sort(list
);
112 * List all the stories of the given author in the {@link Library}, or all
113 * the stories if NULL is passed as an author.
115 * Cover images not included.
118 * the author of the stories to retrieve, or NULL for all
120 * @return the stories
122 public synchronized List
<MetaData
> getListByAuthor(String author
) {
123 List
<MetaData
> list
= new ArrayList
<MetaData
>();
124 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
125 String storyAuthor
= entry
.getKey().getAuthor();
126 if (author
== null || author
.equalsIgnoreCase(storyAuthor
)) {
127 list
.add(entry
.getKey());
131 Collections
.sort(list
);
136 * List all the stories of the given source type in the {@link Library}, or
137 * all the stories if NULL is passed as a type.
139 * Cover images not included.
142 * the type of story to retrieve, or NULL for all
144 * @return the stories
146 public synchronized List
<MetaData
> getListByType(String type
) {
148 // convert the type to dir name
149 type
= getDir(type
).getName();
152 List
<MetaData
> list
= new ArrayList
<MetaData
>();
153 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
154 String storyType
= entry
.getValue().getParentFile().getName();
155 if (type
== null || type
.equalsIgnoreCase(storyType
)) {
156 list
.add(entry
.getKey());
160 Collections
.sort(list
);
165 * Retrieve a {@link File} corresponding to the given {@link Story}, cover
166 * image not included.
169 * the Library UID of the story
171 * @return the corresponding {@link Story}
173 public synchronized MetaData
getInfo(String luid
) {
175 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
176 if (luid
.equals(entry
.getKey().getLuid())) {
177 return entry
.getKey();
186 * Retrieve a {@link File} corresponding to the given {@link Story}.
189 * the Library UID of the story
191 * @return the corresponding {@link Story}
193 public synchronized File
getFile(String luid
) {
195 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
196 if (luid
.equals(entry
.getKey().getLuid())) {
197 return entry
.getValue();
206 * Return the cover image associated to this story.
209 * the Library UID of the story
211 * @return the cover image
213 public synchronized BufferedImage
getCover(String luid
) {
214 MetaData meta
= getInfo(luid
);
217 File infoFile
= new File(getFile(meta
).getPath() + ".info");
218 meta
= readMeta(infoFile
, true).getKey();
219 return meta
.getCover();
220 } catch (IOException e
) {
229 * Retrieve a specific {@link Story}.
232 * the Library UID of the story
234 * the optional progress reporter
236 * @return the corresponding {@link Story} or NULL if not found
238 public synchronized Story
getStory(String luid
, Progress pg
) {
240 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
241 if (luid
.equals(entry
.getKey().getLuid())) {
243 SupportType type
= SupportType
.valueOfAllOkUC(entry
244 .getKey().getType());
245 URL url
= entry
.getValue().toURI().toURL();
247 return BasicSupport
.getSupport(type
).process(url
,
250 throw new IOException("Unknown type: "
251 + entry
.getKey().getType());
253 } catch (IOException e
) {
254 // We should not have not-supported files in the
256 Instance
.syserr(new IOException(
257 "Cannot load file from library: "
258 + entry
.getValue().getPath(), e
));
273 * Import the {@link Story} at the given {@link URL} into the
277 * the {@link URL} to import
279 * the optional progress reporter
281 * @return the imported {@link Story}
283 * @throws IOException
284 * in case of I/O error
286 public Story
imprt(URL url
, Progress pg
) throws IOException
{
287 BasicSupport support
= BasicSupport
.getSupport(url
);
288 if (support
== null) {
289 throw new IOException("URL not supported: " + url
.toString());
292 return save(support
.process(url
, pg
), null);
296 * Export the {@link Story} to the given target in the given format.
299 * the {@link Story} ID
301 * the {@link OutputType} to transform it to
303 * the target to save to
305 * the optional progress reporter
307 * @return the saved resource (the main saved {@link File})
309 * @throws IOException
310 * in case of I/O error
312 public File
export(String luid
, OutputType type
, String target
, Progress pg
)
314 Progress pgGetStory
= new Progress();
315 Progress pgOut
= new Progress();
318 pg
.addProgress(pgGetStory
, 1);
319 pg
.addProgress(pgOut
, 1);
322 BasicOutput out
= BasicOutput
.getOutput(type
, true);
324 throw new IOException("Output type not supported: " + type
);
327 Story story
= getStory(luid
, pgGetStory
);
329 throw new IOException("Cannot find story to export: " + luid
);
332 return out
.process(story
, target
, pgOut
);
336 * Save a {@link Story} to the {@link Library}.
339 * the {@link Story} to save
341 * the optional progress reporter
343 * @return the same {@link Story}, whose LUID may have changed
345 * @throws IOException
346 * in case of I/O error
348 public Story
save(Story story
, Progress pg
) throws IOException
{
349 return save(story
, null, pg
);
353 * Save a {@link Story} to the {@link Library} -- the LUID <b>must</b> be
354 * correct, or NULL to get the next free one.
357 * the {@link Story} to save
359 * the <b>correct</b> LUID or NULL to get the next free one
361 * the optional progress reporter
363 * @return the same {@link Story}, whose LUID may have changed
365 * @throws IOException
366 * in case of I/O error
368 public synchronized Story
save(Story story
, String luid
, Progress pg
)
370 // Do not change the original metadata, but change the original story
371 MetaData key
= story
.getMeta().clone();
374 if (luid
== null || luid
.isEmpty()) {
375 getStories(null); // refresh lastId if needed
376 key
.setLuid(String
.format("%03d", (++lastId
)));
381 getDir(key
.getSource()).mkdirs();
382 if (!getDir(key
.getSource()).exists()) {
383 throw new IOException("Cannot create library dir");
387 if (key
!= null && key
.isImageDocument()) {
393 BasicOutput it
= BasicOutput
.getOutput(out
, true);
394 it
.process(story
, getFile(key
).getPath(), pg
);
403 * Delete the given {@link Story} from this {@link Library}.
406 * the LUID of the target {@link Story}
408 * @return TRUE if it was deleted
410 public synchronized boolean delete(String luid
) {
413 List
<File
> files
= getFiles(luid
);
414 if (!files
.isEmpty()) {
415 for (File file
: files
) {
416 IOUtils
.deltree(file
);
429 * Change the type (source) of the given {@link Story}.
432 * the {@link Story} LUID
433 * @param newSourcethe
436 * @return TRUE if the {@link Story} was found
438 public synchronized boolean changeType(String luid
, String newType
) {
439 MetaData meta
= getInfo(luid
);
441 meta
.setSource(newType
);
442 File newDir
= getDir(meta
.getSource());
443 if (!newDir
.exists()) {
447 List
<File
> files
= getFiles(luid
);
448 for (File file
: files
) {
449 if (file
.getName().endsWith(".info")) {
451 String name
= file
.getName().replaceFirst("\\.info$",
453 InfoCover
.writeInfo(newDir
, name
, meta
);
455 } catch (IOException e
) {
459 file
.renameTo(new File(newDir
, file
.getName()));
473 * Return the list of files/dirs on disk for this {@link Story}.
475 * If the {@link Story} is not found, and empty list is returned.
478 * the {@link Story} LUID
480 * @return the list of {@link File}s
482 private List
<File
> getFiles(String luid
) {
483 List
<File
> files
= new ArrayList
<File
>();
485 MetaData meta
= getInfo(luid
);
486 File file
= getStories(null).get(meta
);
491 String readerExt
= getOutputType(meta
).getDefaultExtension(true);
492 String fileExt
= getOutputType(meta
).getDefaultExtension(false);
494 String path
= file
.getAbsolutePath();
495 if (readerExt
!= null && !readerExt
.equals(fileExt
)) {
496 path
= path
.substring(0, path
.length() - readerExt
.length())
498 file
= new File(path
);
505 File infoFile
= new File(path
+ ".info");
506 if (!infoFile
.exists()) {
507 infoFile
= new File(path
.substring(0,
508 path
.length() - fileExt
.length())
512 if (infoFile
.exists()) {
516 String coverExt
= "."
517 + Instance
.getConfig().getString(Config
.IMAGE_FORMAT_COVER
);
518 File coverFile
= new File(path
+ coverExt
);
519 if (!coverFile
.exists()) {
520 coverFile
= new File(path
.substring(0,
521 path
.length() - fileExt
.length())
525 if (coverFile
.exists()) {
526 files
.add(coverFile
);
534 * The directory (full path) where the {@link Story} related to this
535 * {@link MetaData} should be located on disk.
540 * @return the target directory
542 private File
getDir(String type
) {
543 String source
= type
.replaceAll("[^a-zA-Z0-9._+-]", "_");
544 return new File(baseDir
, source
);
548 * The target (full path) where the {@link Story} related to this
549 * {@link MetaData} should be located on disk.
552 * the {@link Story} {@link MetaData}
556 private File
getFile(MetaData key
) {
557 String title
= key
.getTitle();
561 title
= title
.replaceAll("[^a-zA-Z0-9._+-]", "_");
562 return new File(getDir(key
.getSource()), key
.getLuid() + "_" + title
);
566 * Return all the known stories in this {@link Library} object.
569 * the optional progress reporter
571 * @return the stories
573 private synchronized Map
<MetaData
, File
> getStories(Progress pg
) {
577 pg
.setMinMax(0, 100);
580 if (stories
.isEmpty()) {
583 File
[] dirs
= baseDir
.listFiles(new FileFilter() {
584 public boolean accept(File file
) {
585 return file
!= null && file
.isDirectory();
589 Progress pgDirs
= new Progress(0, 100 * dirs
.length
);
590 pg
.addProgress(pgDirs
, 100);
592 for (File dir
: dirs
) {
593 File
[] files
= dir
.listFiles(new FileFilter() {
594 public boolean accept(File file
) {
596 && file
.getPath().toLowerCase()
601 Progress pgFiles
= new Progress(0, files
.length
);
602 pgDirs
.addProgress(pgFiles
, 100);
603 pgDirs
.setName("Loading from: " + dir
.getName());
605 for (File file
: files
) {
606 pgFiles
.setName(file
.getName());
608 Entry
<MetaData
, File
> entry
= readMeta(file
, false);
610 int id
= Integer
.parseInt(entry
.getKey().getLuid());
615 stories
.put(entry
.getKey(), entry
.getValue());
616 } catch (Exception e
) {
618 throw new IOException(
619 "Cannot understand the LUID of "
620 + file
.getPath() + ": "
621 + entry
.getKey().getLuid(), e
);
623 } catch (IOException e
) {
624 // We should not have not-supported files in the
626 Instance
.syserr(new IOException(
627 "Cannot load file from library: "
628 + file
.getPath(), e
));
633 pgFiles
.setName(null);
636 pgDirs
.setName("Loading directories");
642 private Entry
<MetaData
, File
> readMeta(File infoFile
, boolean withCover
)
645 final MetaData meta
= InfoReader
.readMeta(infoFile
, withCover
);
647 // Replace .info with whatever is needed:
648 String path
= infoFile
.getPath();
649 path
= path
.substring(0, path
.length() - ".info".length());
651 String newExt
= getOutputType(meta
).getDefaultExtension(true);
653 File targetFile
= new File(path
+ newExt
);
655 final File ffile
= targetFile
;
656 return new Entry
<MetaData
, File
>() {
657 public File
setValue(File value
) {
661 public File
getValue() {
665 public MetaData
getKey() {
672 * Return the {@link OutputType} for this {@link Story}.
675 * the {@link Story} {@link MetaData}
679 private OutputType
getOutputType(MetaData meta
) {
680 if (meta
!= null && meta
.isImageDocument()) {