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
{
39 protected File baseDir
;
40 protected boolean localSpeed
;
42 private Map
<MetaData
, File
> stories
;
44 private OutputType text
;
45 private OutputType image
;
48 * Create a new {@link Library} with the given backend directory.
51 * the directory where to find the {@link Story} objects
53 * the {@link OutputType} to save the text-focused stories into
55 * the {@link OutputType} to save the images-focused stories into
57 public Library(File dir
, OutputType text
, OutputType image
) {
70 * Create a new {@link Library} with no link to the local machine.
72 * Reserved for extensions.
75 this.stories
= new HashMap
<MetaData
, File
>();
79 * Refresh the {@link Library}, that is, make sure all stories are loaded.
82 * the optional progress reporter
84 public void refresh(Progress pg
) {
89 * List all the known types (sources) of stories.
93 public synchronized List
<String
> getTypes() {
94 List
<String
> list
= new ArrayList
<String
>();
95 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
96 String storyType
= entry
.getKey().getSource();
97 if (!list
.contains(storyType
)) {
102 Collections
.sort(list
);
107 * List all the known authors of stories.
109 * @return the authors
111 public synchronized List
<String
> getAuthors() {
112 List
<String
> list
= new ArrayList
<String
>();
113 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
114 String storyAuthor
= entry
.getKey().getAuthor();
115 if (!list
.contains(storyAuthor
)) {
116 list
.add(storyAuthor
);
120 Collections
.sort(list
);
125 * List all the stories of the given author in the {@link Library}, or all
126 * the stories if NULL is passed as an author.
128 * Cover images not included.
131 * the author of the stories to retrieve, or NULL for all
133 * @return the stories
135 public synchronized List
<MetaData
> getListByAuthor(String author
) {
136 List
<MetaData
> list
= new ArrayList
<MetaData
>();
137 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
138 String storyAuthor
= entry
.getKey().getAuthor();
139 if (author
== null || author
.equalsIgnoreCase(storyAuthor
)) {
140 list
.add(entry
.getKey());
144 Collections
.sort(list
);
149 * List all the stories of the given source type in the {@link Library}, or
150 * all the stories if NULL is passed as a type.
152 * Cover images not included.
155 * the type of story to retrieve, or NULL for all
157 * @return the stories
159 public synchronized List
<MetaData
> getListByType(String type
) {
161 // convert the type to dir name
162 type
= getExpectedDir(type
).getName();
165 List
<MetaData
> list
= new ArrayList
<MetaData
>();
166 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
167 String storyType
= entry
.getValue().getParentFile().getName();
168 if (type
== null || type
.equalsIgnoreCase(storyType
)) {
169 list
.add(entry
.getKey());
173 Collections
.sort(list
);
178 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
179 * cover image <b>MAY</b> not be included.
182 * the Library UID of the story
184 * @return the corresponding {@link Story}
186 public synchronized MetaData
getInfo(String luid
) {
188 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
189 if (luid
.equals(entry
.getKey().getLuid())) {
190 return entry
.getKey();
199 * Retrieve a {@link File} corresponding to the given {@link Story}.
202 * the Library UID of the story
204 * @return the corresponding {@link Story}
206 public synchronized File
getFile(String luid
) {
208 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
209 if (luid
.equals(entry
.getKey().getLuid())) {
210 return entry
.getValue();
219 * Return the cover image associated to this story.
222 * the Library UID of the story
224 * @return the cover image
226 public synchronized BufferedImage
getCover(String luid
) {
227 MetaData meta
= getInfo(luid
);
229 getFile(luid
); // to help remote implementation
231 File infoFile
= new File(getExpectedFile(meta
).getPath()
233 meta
= readMeta(infoFile
, true).getKey();
234 return meta
.getCover();
235 } catch (IOException e
) {
244 * Retrieve a specific {@link Story}.
247 * the Library UID of the story
249 * the optional progress reporter
251 * @return the corresponding {@link Story} or NULL if not found
253 public synchronized Story
getStory(String luid
, Progress pg
) {
255 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
256 if (luid
.equals(entry
.getKey().getLuid())) {
257 MetaData meta
= entry
.getKey();
258 File file
= getFile(luid
); // to help remote implementation
260 SupportType type
= SupportType
.valueOfAllOkUC(meta
262 URL url
= file
.toURI().toURL();
264 return BasicSupport
.getSupport(type
).process(url
,
267 throw new IOException("Unknown type: "
270 } catch (IOException e
) {
271 // We should not have not-supported files in the
273 Instance
.syserr(new IOException(
274 "Cannot load file from library: " + file
, e
));
289 * Import the {@link Story} at the given {@link URL} into the
293 * the {@link URL} to import
295 * the optional progress reporter
297 * @return the imported {@link Story}
299 * @throws IOException
300 * in case of I/O error
302 public Story
imprt(URL url
, Progress pg
) throws IOException
{
303 BasicSupport support
= BasicSupport
.getSupport(url
);
304 if (support
== null) {
305 throw new IOException("URL not supported: " + url
.toString());
308 return save(support
.process(url
, pg
), null);
312 * Export the {@link Story} to the given target in the given format.
315 * the {@link Story} ID
317 * the {@link OutputType} to transform it to
319 * the target to save to
321 * the optional progress reporter
323 * @return the saved resource (the main saved {@link File})
325 * @throws IOException
326 * in case of I/O error
328 public File
export(String luid
, OutputType type
, String target
, Progress pg
)
330 Progress pgGetStory
= new Progress();
331 Progress pgOut
= new Progress();
334 pg
.addProgress(pgGetStory
, 1);
335 pg
.addProgress(pgOut
, 1);
338 BasicOutput out
= BasicOutput
.getOutput(type
, true);
340 throw new IOException("Output type not supported: " + type
);
343 Story story
= getStory(luid
, pgGetStory
);
345 throw new IOException("Cannot find story to export: " + luid
);
348 return out
.process(story
, target
, pgOut
);
352 * Save a {@link Story} to the {@link Library}.
355 * the {@link Story} to save
357 * the optional progress reporter
359 * @return the same {@link Story}, whose LUID may have changed
361 * @throws IOException
362 * in case of I/O error
364 public Story
save(Story story
, Progress pg
) throws IOException
{
365 return save(story
, null, pg
);
369 * Save a {@link Story} to the {@link Library} -- the LUID <b>must</b> be
370 * correct, or NULL to get the next free one.
373 * the {@link Story} to save
375 * the <b>correct</b> LUID or NULL to get the next free one
377 * the optional progress reporter
379 * @return the same {@link Story}, whose LUID may have changed
381 * @throws IOException
382 * in case of I/O error
384 public synchronized Story
save(Story story
, String luid
, Progress pg
)
386 // Do not change the original metadata, but change the original story
387 MetaData key
= story
.getMeta().clone();
390 if (luid
== null || luid
.isEmpty()) {
391 getStories(null); // refresh lastId if needed
392 key
.setLuid(String
.format("%03d", (++lastId
)));
397 getExpectedDir(key
.getSource()).mkdirs();
398 if (!getExpectedDir(key
.getSource()).exists()) {
399 throw new IOException("Cannot create library dir");
403 if (key
!= null && key
.isImageDocument()) {
409 BasicOutput it
= BasicOutput
.getOutput(out
, true);
410 it
.process(story
, getExpectedFile(key
).getPath(), pg
);
419 * Delete the given {@link Story} from this {@link Library}.
422 * the LUID of the target {@link Story}
424 * @return TRUE if it was deleted
426 public synchronized boolean delete(String luid
) {
429 List
<File
> files
= getFiles(luid
);
430 if (!files
.isEmpty()) {
431 for (File file
: files
) {
432 IOUtils
.deltree(file
);
445 * Change the type (source) of the given {@link Story}.
448 * the {@link Story} LUID
452 * @return TRUE if the {@link Story} was found
454 public synchronized boolean changeType(String luid
, String newType
) {
455 MetaData meta
= getInfo(luid
);
457 meta
.setSource(newType
);
458 File newDir
= getExpectedDir(meta
.getSource());
459 if (!newDir
.exists()) {
463 List
<File
> files
= getFiles(luid
);
464 for (File file
: files
) {
465 if (file
.getName().endsWith(".info")) {
467 String name
= file
.getName().replaceFirst("\\.info$",
469 InfoCover
.writeInfo(newDir
, name
, meta
);
471 } catch (IOException e
) {
475 file
.renameTo(new File(newDir
, file
.getName()));
489 * The library is accessed locally or at local speed (for operations like
490 * {@link Library#getFile(String)}).
492 * It could be cached, too, it is only about the access speed.
494 * @return TRUE if it is accessed locally
496 public boolean isLocalSpeed() {
501 * Return the list of files/dirs on disk for this {@link Story}.
503 * If the {@link Story} is not found, and empty list is returned.
506 * the {@link Story} LUID
508 * @return the list of {@link File}s
510 private List
<File
> getFiles(String luid
) {
511 List
<File
> files
= new ArrayList
<File
>();
513 MetaData meta
= getInfo(luid
);
514 File file
= getFile(luid
); // to help remote implementation
519 String readerExt
= getOutputType(meta
).getDefaultExtension(true);
520 String fileExt
= getOutputType(meta
).getDefaultExtension(false);
522 String path
= file
.getAbsolutePath();
523 if (readerExt
!= null && !readerExt
.equals(fileExt
)) {
524 path
= path
.substring(0, path
.length() - readerExt
.length())
526 file
= new File(path
);
533 File infoFile
= new File(path
+ ".info");
534 if (!infoFile
.exists()) {
535 infoFile
= new File(path
.substring(0,
536 path
.length() - fileExt
.length())
540 if (infoFile
.exists()) {
544 String coverExt
= "."
545 + Instance
.getConfig().getString(Config
.IMAGE_FORMAT_COVER
);
546 File coverFile
= new File(path
+ coverExt
);
547 if (!coverFile
.exists()) {
548 coverFile
= new File(path
.substring(0,
549 path
.length() - fileExt
.length())
553 if (coverFile
.exists()) {
554 files
.add(coverFile
);
562 * The directory (full path) where the {@link Story} related to this
563 * {@link MetaData} should be located on disk.
568 * @return the target directory
570 private File
getExpectedDir(String type
) {
571 String source
= type
.replaceAll("[^a-zA-Z0-9._+-]", "_");
572 return new File(baseDir
, source
);
576 * The target (full path) where the {@link Story} related to this
577 * {@link MetaData} should be located on disk.
580 * the {@link Story} {@link MetaData}
584 private File
getExpectedFile(MetaData key
) {
585 String title
= key
.getTitle();
589 title
= title
.replaceAll("[^a-zA-Z0-9._+-]", "_");
590 return new File(getExpectedDir(key
.getSource()), key
.getLuid() + "_"
595 * Return all the known stories in this {@link Library} object.
598 * the optional progress reporter
600 * @return the stories
602 protected synchronized Map
<MetaData
, File
> getStories(Progress pg
) {
606 pg
.setMinMax(0, 100);
609 if (stories
.isEmpty()) {
612 File
[] dirs
= baseDir
.listFiles(new FileFilter() {
613 public boolean accept(File file
) {
614 return file
!= null && file
.isDirectory();
618 Progress pgDirs
= new Progress(0, 100 * dirs
.length
);
619 pg
.addProgress(pgDirs
, 100);
621 for (File dir
: dirs
) {
622 File
[] files
= dir
.listFiles(new FileFilter() {
623 public boolean accept(File file
) {
625 && file
.getPath().toLowerCase()
630 Progress pgFiles
= new Progress(0, files
.length
);
631 pgDirs
.addProgress(pgFiles
, 100);
632 pgDirs
.setName("Loading from: " + dir
.getName());
634 for (File file
: files
) {
635 pgFiles
.setName(file
.getName());
637 Entry
<MetaData
, File
> entry
= readMeta(file
, false);
639 int id
= Integer
.parseInt(entry
.getKey().getLuid());
644 stories
.put(entry
.getKey(), entry
.getValue());
645 } catch (Exception e
) {
647 throw new IOException(
648 "Cannot understand the LUID of "
649 + file
.getPath() + ": "
650 + entry
.getKey().getLuid(), e
);
652 } catch (IOException e
) {
653 // We should not have not-supported files in the
655 Instance
.syserr(new IOException(
656 "Cannot load file from library: "
657 + file
.getPath(), e
));
662 pgFiles
.setName(null);
665 pgDirs
.setName("Loading directories");
671 private Entry
<MetaData
, File
> readMeta(File infoFile
, boolean withCover
)
674 final MetaData meta
= InfoReader
.readMeta(infoFile
, withCover
);
676 // Replace .info with whatever is needed:
677 String path
= infoFile
.getPath();
678 path
= path
.substring(0, path
.length() - ".info".length());
680 String newExt
= getOutputType(meta
).getDefaultExtension(true);
682 File targetFile
= new File(path
+ newExt
);
684 final File ffile
= targetFile
;
685 return new Entry
<MetaData
, File
>() {
686 public File
setValue(File value
) {
690 public File
getValue() {
694 public MetaData
getKey() {
701 * Return the {@link OutputType} for this {@link Story}.
704 * the {@link Story} {@link MetaData}
708 private OutputType
getOutputType(MetaData meta
) {
709 if (meta
!= null && meta
.isImageDocument()) {