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
;
41 protected 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
> getListBySource(String type
) {
160 List
<MetaData
> list
= new ArrayList
<MetaData
>();
161 for (MetaData meta
: getStories(null).keySet()) {
162 String storyType
= meta
.getSource();
163 if (type
== null || type
.equalsIgnoreCase(storyType
)) {
168 Collections
.sort(list
);
173 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
174 * cover image <b>MAY</b> not be included.
177 * the Library UID of the story
179 * @return the corresponding {@link Story}
181 public synchronized MetaData
getInfo(String luid
) {
183 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
184 if (luid
.equals(entry
.getKey().getLuid())) {
185 return entry
.getKey();
194 * Retrieve a {@link File} corresponding to the given {@link Story}.
197 * the Library UID of the story
199 * @return the corresponding {@link Story}
201 public synchronized File
getFile(String luid
) {
203 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
204 if (luid
.equals(entry
.getKey().getLuid())) {
205 return entry
.getValue();
214 * Return the cover image associated to this story.
217 * the Library UID of the story
219 * @return the cover image
221 public synchronized BufferedImage
getCover(String luid
) {
222 MetaData meta
= getInfo(luid
);
224 getFile(luid
); // to help remote implementation
226 File infoFile
= new File(getExpectedFile(meta
).getPath()
228 meta
= readMeta(infoFile
, true).getKey();
229 return meta
.getCover();
230 } catch (IOException e
) {
239 * Retrieve a specific {@link Story}.
242 * the Library UID of the story
244 * the optional progress reporter
246 * @return the corresponding {@link Story} or NULL if not found
248 public synchronized Story
getStory(String luid
, Progress pg
) {
250 for (Entry
<MetaData
, File
> entry
: getStories(null).entrySet()) {
251 if (luid
.equals(entry
.getKey().getLuid())) {
252 MetaData meta
= entry
.getKey();
253 File file
= getFile(luid
); // to help remote implementation
255 SupportType type
= SupportType
.valueOfAllOkUC(meta
257 URL url
= file
.toURI().toURL();
259 return BasicSupport
.getSupport(type
).process(url
,
262 throw new IOException("Unknown type: "
265 } catch (IOException e
) {
266 // We should not have not-supported files in the
268 Instance
.syserr(new IOException(
269 "Cannot load file from library: " + file
, e
));
284 * Import the {@link Story} at the given {@link URL} into the
288 * the {@link URL} to import
290 * the optional progress reporter
292 * @return the imported {@link Story}
294 * @throws IOException
295 * in case of I/O error
297 public Story
imprt(URL url
, Progress pg
) throws IOException
{
298 BasicSupport support
= BasicSupport
.getSupport(url
);
299 if (support
== null) {
300 throw new IOException("URL not supported: " + url
.toString());
303 return save(support
.process(url
, pg
), null);
307 * Export the {@link Story} to the given target in the given format.
310 * the {@link Story} ID
312 * the {@link OutputType} to transform it to
314 * the target to save to
316 * the optional progress reporter
318 * @return the saved resource (the main saved {@link File})
320 * @throws IOException
321 * in case of I/O error
323 public File
export(String luid
, OutputType type
, String target
, Progress pg
)
325 Progress pgGetStory
= new Progress();
326 Progress pgOut
= new Progress();
329 pg
.addProgress(pgGetStory
, 1);
330 pg
.addProgress(pgOut
, 1);
333 BasicOutput out
= BasicOutput
.getOutput(type
, true);
335 throw new IOException("Output type not supported: " + type
);
338 Story story
= getStory(luid
, pgGetStory
);
340 throw new IOException("Cannot find story to export: " + luid
);
343 return out
.process(story
, target
, pgOut
);
347 * Save a {@link Story} to the {@link Library}.
350 * the {@link Story} to save
352 * the optional progress reporter
354 * @return the same {@link Story}, whose LUID may have changed
356 * @throws IOException
357 * in case of I/O error
359 public Story
save(Story story
, Progress pg
) throws IOException
{
360 return save(story
, null, pg
);
364 * Save a {@link Story} to the {@link Library} -- the LUID <b>must</b> be
365 * correct, or NULL to get the next free one.
368 * the {@link Story} to save
370 * the <b>correct</b> LUID or NULL to get the next free one
372 * the optional progress reporter
374 * @return the same {@link Story}, whose LUID may have changed
376 * @throws IOException
377 * in case of I/O error
379 public synchronized Story
save(Story story
, String luid
, Progress pg
)
381 // Do not change the original metadata, but change the original story
382 MetaData key
= story
.getMeta().clone();
385 if (luid
== null || luid
.isEmpty()) {
386 getStories(null); // refresh lastId if needed
387 key
.setLuid(String
.format("%03d", (++lastId
)));
392 getExpectedDir(key
.getSource()).mkdirs();
393 if (!getExpectedDir(key
.getSource()).exists()) {
394 throw new IOException("Cannot create library dir");
398 if (key
!= null && key
.isImageDocument()) {
404 BasicOutput it
= BasicOutput
.getOutput(out
, true);
405 it
.process(story
, getExpectedFile(key
).getPath(), pg
);
414 * Delete the given {@link Story} from this {@link Library}.
417 * the LUID of the target {@link Story}
419 * @return TRUE if it was deleted
421 public synchronized boolean delete(String luid
) {
424 List
<File
> files
= getFiles(luid
);
425 if (!files
.isEmpty()) {
426 for (File file
: files
) {
427 IOUtils
.deltree(file
);
440 * Change the type (source) of the given {@link Story}.
443 * the {@link Story} LUID
447 * @return TRUE if the {@link Story} was found
449 public synchronized boolean changeType(String luid
, String newType
) {
450 MetaData meta
= getInfo(luid
);
452 meta
.setSource(newType
);
453 File newDir
= getExpectedDir(meta
.getSource());
454 if (!newDir
.exists()) {
458 List
<File
> files
= getFiles(luid
);
459 for (File file
: files
) {
460 if (file
.getName().endsWith(".info")) {
462 String name
= file
.getName().replaceFirst("\\.info$",
464 InfoCover
.writeInfo(newDir
, name
, meta
);
466 } catch (IOException e
) {
470 file
.renameTo(new File(newDir
, file
.getName()));
484 * The library is accessed locally or at local speed (for operations like
485 * {@link Library#getFile(String)}).
487 * It could be cached, too, it is only about the access speed.
489 * @return TRUE if it is accessed locally
491 public boolean isLocalSpeed() {
496 * Return the list of files/dirs on disk for this {@link Story}.
498 * If the {@link Story} is not found, and empty list is returned.
501 * the {@link Story} LUID
503 * @return the list of {@link File}s
505 private List
<File
> getFiles(String luid
) {
506 List
<File
> files
= new ArrayList
<File
>();
508 MetaData meta
= getInfo(luid
);
509 File file
= getFile(luid
); // to help remote implementation
514 String readerExt
= getOutputType(meta
).getDefaultExtension(true);
515 String fileExt
= getOutputType(meta
).getDefaultExtension(false);
517 String path
= file
.getAbsolutePath();
518 if (readerExt
!= null && !readerExt
.equals(fileExt
)) {
519 path
= path
.substring(0, path
.length() - readerExt
.length())
521 file
= new File(path
);
528 File infoFile
= new File(path
+ ".info");
529 if (!infoFile
.exists()) {
530 infoFile
= new File(path
.substring(0,
531 path
.length() - fileExt
.length())
535 if (infoFile
.exists()) {
539 String coverExt
= "."
540 + Instance
.getConfig().getString(Config
.IMAGE_FORMAT_COVER
);
541 File coverFile
= new File(path
+ coverExt
);
542 if (!coverFile
.exists()) {
543 coverFile
= new File(path
.substring(0,
544 path
.length() - fileExt
.length())
548 if (coverFile
.exists()) {
549 files
.add(coverFile
);
557 * The directory (full path) where the {@link Story} related to this
558 * {@link MetaData} should be located on disk.
563 * @return the target directory
565 private File
getExpectedDir(String type
) {
566 String source
= type
.replaceAll("[^a-zA-Z0-9._+-]", "_");
567 return new File(baseDir
, source
);
571 * The target (full path) where the {@link Story} related to this
572 * {@link MetaData} should be located on disk.
575 * the {@link Story} {@link MetaData}
579 private File
getExpectedFile(MetaData key
) {
580 String title
= key
.getTitle();
584 title
= title
.replaceAll("[^a-zA-Z0-9._+-]", "_");
585 return new File(getExpectedDir(key
.getSource()), key
.getLuid() + "_"
590 * Return all the known stories in this {@link Library} object.
593 * the optional progress reporter
595 * @return the stories
597 protected synchronized Map
<MetaData
, File
> getStories(Progress pg
) {
601 pg
.setMinMax(0, 100);
604 if (stories
.isEmpty()) {
607 File
[] dirs
= baseDir
.listFiles(new FileFilter() {
608 public boolean accept(File file
) {
609 return file
!= null && file
.isDirectory();
613 Progress pgDirs
= new Progress(0, 100 * dirs
.length
);
614 pg
.addProgress(pgDirs
, 100);
616 for (File dir
: dirs
) {
617 File
[] files
= dir
.listFiles(new FileFilter() {
618 public boolean accept(File file
) {
620 && file
.getPath().toLowerCase()
625 Progress pgFiles
= new Progress(0, files
.length
);
626 pgDirs
.addProgress(pgFiles
, 100);
627 pgDirs
.setName("Loading from: " + dir
.getName());
629 for (File file
: files
) {
630 pgFiles
.setName(file
.getName());
632 Entry
<MetaData
, File
> entry
= readMeta(file
, false);
634 int id
= Integer
.parseInt(entry
.getKey().getLuid());
639 stories
.put(entry
.getKey(), entry
.getValue());
640 } catch (Exception e
) {
642 throw new IOException(
643 "Cannot understand the LUID of "
644 + file
.getPath() + ": "
645 + entry
.getKey().getLuid(), e
);
647 } catch (IOException e
) {
648 // We should not have not-supported files in the
650 Instance
.syserr(new IOException(
651 "Cannot load file from library: "
652 + file
.getPath(), e
));
657 pgFiles
.setName(null);
660 pgDirs
.setName("Loading directories");
666 private Entry
<MetaData
, File
> readMeta(File infoFile
, boolean withCover
)
669 final MetaData meta
= InfoReader
.readMeta(infoFile
, withCover
);
671 // Replace .info with whatever is needed:
672 String path
= infoFile
.getPath();
673 path
= path
.substring(0, path
.length() - ".info".length());
675 String newExt
= getOutputType(meta
).getDefaultExtension(true);
677 File targetFile
= new File(path
+ newExt
);
679 final File ffile
= targetFile
;
680 return new Entry
<MetaData
, File
>() {
681 public File
setValue(File value
) {
685 public File
getValue() {
689 public MetaData
getKey() {
696 * Return the {@link OutputType} for this {@link Story}.
699 * the {@link Story} {@link MetaData}
703 private OutputType
getOutputType(MetaData meta
) {
704 if (meta
!= null && meta
.isImageDocument()) {