1 package be
.nikiroo
.fanfix
.library
;
4 import java
.io
.FileFilter
;
5 import java
.io
.FileInputStream
;
6 import java
.io
.FileNotFoundException
;
7 import java
.io
.IOException
;
8 import java
.io
.InputStream
;
9 import java
.util
.ArrayList
;
10 import java
.util
.HashMap
;
11 import java
.util
.List
;
14 import be
.nikiroo
.fanfix
.Instance
;
15 import be
.nikiroo
.fanfix
.bundles
.Config
;
16 import be
.nikiroo
.fanfix
.bundles
.ConfigBundle
;
17 import be
.nikiroo
.fanfix
.data
.MetaData
;
18 import be
.nikiroo
.fanfix
.data
.Story
;
19 import be
.nikiroo
.fanfix
.output
.BasicOutput
;
20 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
21 import be
.nikiroo
.fanfix
.output
.InfoCover
;
22 import be
.nikiroo
.fanfix
.supported
.InfoReader
;
23 import be
.nikiroo
.utils
.HashUtils
;
24 import be
.nikiroo
.utils
.IOUtils
;
25 import be
.nikiroo
.utils
.Image
;
26 import be
.nikiroo
.utils
.Progress
;
29 * This {@link BasicLibrary} will store the stories locally on disk.
33 public class LocalLibrary
extends BasicLibrary
{
35 private Object lock
= new Object();
36 private Map
<MetaData
, File
[]> stories
; // Files: [ infoFile, TargetFile ]
37 private Map
<String
, Image
> sourceCovers
;
38 private Map
<String
, Image
> authorCovers
;
41 private OutputType text
;
42 private OutputType image
;
45 * Create a new {@link LocalLibrary} with the given back-end directory.
48 * the directory where to find the {@link Story} objects
50 * the configuration used to know which kind of default
51 * {@link OutputType} to use for images and non-images stories
53 public LocalLibrary(File baseDir
, ConfigBundle config
) {
55 config
.getString(Config
.FILE_FORMAT_NON_IMAGES_DOCUMENT_TYPE
),
56 config
.getString(Config
.FILE_FORMAT_IMAGES_DOCUMENT_TYPE
),
61 * Create a new {@link LocalLibrary} with the given back-end directory.
64 * the directory where to find the {@link Story} objects
66 * the {@link OutputType} to use for non-image documents
68 * the {@link OutputType} to use for image documents
69 * @param defaultIsHtml
70 * if the given text or image is invalid, use HTML by default (if
71 * not, it will be INFO_TEXT/CBZ by default)
73 public LocalLibrary(File baseDir
, String text
, String image
,
74 boolean defaultIsHtml
) {
76 OutputType
.valueOfAllOkUC(text
,
77 defaultIsHtml ? OutputType
.HTML
: OutputType
.INFO_TEXT
),
78 OutputType
.valueOfAllOkUC(image
,
79 defaultIsHtml ? OutputType
.HTML
: OutputType
.CBZ
));
83 * Create a new {@link LocalLibrary} with the given back-end directory.
86 * the directory where to find the {@link Story} objects
88 * the {@link OutputType} to use for non-image documents
90 * the {@link OutputType} to use for image documents
92 public LocalLibrary(File baseDir
, OutputType text
, OutputType image
) {
93 this.baseDir
= baseDir
;
99 this.sourceCovers
= null;
105 protected List
<MetaData
> getMetas(Progress pg
) {
106 return new ArrayList
<MetaData
>(getStories(pg
).keySet());
110 public File
getFile(String luid
, Progress pg
) throws IOException
{
111 Instance
.getInstance().getTraceHandler().trace(
112 this.getClass().getSimpleName() + ": get file for " + luid
);
115 String mess
= "no file found for ";
117 MetaData meta
= getInfo(luid
);
119 File
[] files
= getStories(pg
).get(meta
);
121 mess
= "file retrieved for ";
126 Instance
.getInstance().getTraceHandler()
127 .trace(this.getClass().getSimpleName() + ": " + mess
+ luid
128 + " (" + meta
.getTitle() + ")");
134 public Image
getCover(String luid
) throws IOException
{
135 MetaData meta
= getInfo(luid
);
137 if (meta
.getCover() != null) {
138 return meta
.getCover();
141 File
[] files
= getStories(null).get(meta
);
143 File infoFile
= files
[0];
146 meta
= InfoReader
.readMeta(infoFile
, true);
147 return meta
.getCover();
148 } catch (IOException e
) {
149 Instance
.getInstance().getTraceHandler().error(e
);
158 protected void updateInfo(MetaData meta
) {
163 protected void invalidateInfo(String luid
) {
164 synchronized (lock
) {
171 protected String
getNextId() {
172 getStories(null); // make sure lastId is set
174 synchronized (lock
) {
175 return String
.format("%03d", ++lastId
);
180 protected void doDelete(String luid
) throws IOException
{
181 for (File file
: getRelatedFiles(luid
)) {
182 // TODO: throw an IOException if we cannot delete the files?
183 IOUtils
.deltree(file
);
184 file
.getParentFile().delete();
189 protected Story
doSave(Story story
, Progress pg
) throws IOException
{
190 MetaData meta
= story
.getMeta();
192 File expectedTarget
= getExpectedFile(meta
);
193 expectedTarget
.getParentFile().mkdirs();
195 BasicOutput it
= BasicOutput
.getOutput(getOutputType(meta
), true, true);
196 it
.process(story
, expectedTarget
.getPath(), pg
);
202 protected synchronized void saveMeta(MetaData meta
, Progress pg
)
204 File newDir
= getExpectedDir(meta
.getSource());
205 if (!newDir
.exists()) {
209 List
<File
> relatedFiles
= getRelatedFiles(meta
.getLuid());
210 for (File relatedFile
: relatedFiles
) {
211 // TODO: this is not safe at all.
212 // We should copy all the files THEN delete them
213 // Maybe also adding some rollback cleanup if possible
214 if (relatedFile
.getName().endsWith(".info")) {
216 String name
= relatedFile
.getName().replaceFirst("\\.info$",
218 relatedFile
.delete();
219 InfoCover
.writeInfo(newDir
, name
, meta
);
220 relatedFile
.getParentFile().delete();
221 } catch (IOException e
) {
222 Instance
.getInstance().getTraceHandler().error(e
);
225 relatedFile
.renameTo(new File(newDir
, relatedFile
.getName()));
226 relatedFile
.getParentFile().delete();
234 public Image
getCustomSourceCover(String source
) {
235 synchronized (lock
) {
236 if (sourceCovers
== null) {
237 sourceCovers
= new HashMap
<String
, Image
>();
241 synchronized (lock
) {
242 Image img
= sourceCovers
.get(source
);
248 File coverDir
= getExpectedDir(source
);
249 if (coverDir
.isDirectory()) {
250 File cover
= new File(coverDir
, ".cover.png");
251 if (cover
.exists()) {
254 in
= new FileInputStream(cover
);
256 synchronized (lock
) {
257 Image img
= new Image(in
);
258 if (img
.getSize() == 0) {
260 throw new IOException(
261 "Empty image not accepted");
263 sourceCovers
.put(source
, img
);
268 } catch (FileNotFoundException e
) {
270 } catch (IOException e
) {
271 Instance
.getInstance().getTraceHandler()
272 .error(new IOException(
273 "Cannot load the existing custom source cover: "
280 synchronized (lock
) {
281 return sourceCovers
.get(source
);
286 public Image
getCustomAuthorCover(String author
) {
287 synchronized (lock
) {
288 if (authorCovers
== null) {
289 authorCovers
= new HashMap
<String
, Image
>();
293 synchronized (lock
) {
294 Image img
= authorCovers
.get(author
);
300 File cover
= getAuthorCoverFile(author
);
301 if (cover
.exists()) {
304 in
= new FileInputStream(cover
);
306 synchronized (lock
) {
307 Image img
= new Image(in
);
308 if (img
.getSize() == 0) {
310 throw new IOException(
311 "Empty image not accepted");
313 authorCovers
.put(author
, img
);
318 } catch (FileNotFoundException e
) {
320 } catch (IOException e
) {
321 Instance
.getInstance().getTraceHandler()
322 .error(new IOException(
323 "Cannot load the existing custom author cover: "
329 synchronized (lock
) {
330 return authorCovers
.get(author
);
335 public void setSourceCover(String source
, String luid
) throws IOException
{
336 setSourceCover(source
, getCover(luid
));
340 public void setAuthorCover(String author
, String luid
) throws IOException
{
341 setAuthorCover(author
, getCover(luid
));
345 * Set the source cover to the given story cover.
348 * the source to change
352 void setSourceCover(String source
, Image coverImage
) {
353 File dir
= getExpectedDir(source
);
355 File cover
= new File(dir
, ".cover");
357 Instance
.getInstance().getCache().saveAsImage(coverImage
, cover
,
359 synchronized (lock
) {
360 if (sourceCovers
!= null) {
361 sourceCovers
.put(source
, coverImage
);
364 } catch (IOException e
) {
365 Instance
.getInstance().getTraceHandler().error(e
);
370 * Set the author cover to the given story cover.
373 * the author to change
377 void setAuthorCover(String author
, Image coverImage
) {
378 File cover
= getAuthorCoverFile(author
);
379 cover
.getParentFile().mkdirs();
381 Instance
.getInstance().getCache().saveAsImage(coverImage
, cover
,
383 synchronized (lock
) {
384 if (authorCovers
!= null) {
385 authorCovers
.put(author
, coverImage
);
388 } catch (IOException e
) {
389 Instance
.getInstance().getTraceHandler().error(e
);
394 public void imprt(BasicLibrary other
, String luid
, Progress pg
)
400 // Check if we can simply copy the files instead of the whole process
401 if (other
instanceof LocalLibrary
) {
402 LocalLibrary otherLocalLibrary
= (LocalLibrary
) other
;
404 MetaData meta
= otherLocalLibrary
.getInfo(luid
);
405 String expectedType
= ""
406 + (meta
!= null && meta
.isImageDocument() ? image
: text
);
407 if (meta
!= null && meta
.getType().equals(expectedType
)) {
408 File from
= otherLocalLibrary
.getExpectedDir(meta
.getSource());
409 File to
= this.getExpectedDir(meta
.getSource());
410 List
<File
> relatedFiles
= otherLocalLibrary
411 .getRelatedFiles(luid
);
412 if (!relatedFiles
.isEmpty()) {
413 pg
.setMinMax(0, relatedFiles
.size());
416 for (File relatedFile
: relatedFiles
) {
417 File target
= new File(relatedFile
.getAbsolutePath()
418 .replace(from
.getAbsolutePath(),
419 to
.getAbsolutePath()));
420 if (!relatedFile
.equals(target
)) {
421 target
.getParentFile().mkdirs();
422 InputStream in
= null;
424 in
= new FileInputStream(relatedFile
);
425 IOUtils
.write(in
, target
);
426 } catch (IOException e
) {
430 } catch (Exception ee
) {
448 super.imprt(other
, luid
, pg
);
452 * Return the {@link OutputType} for this {@link Story}.
455 * the {@link Story} {@link MetaData}
459 private OutputType
getOutputType(MetaData meta
) {
460 if (meta
!= null && meta
.isImageDocument()) {
468 * Return the default {@link OutputType} for this kind of {@link Story}.
470 * @param imageDocument
471 * TRUE for images document, FALSE for text documents
475 public String
getOutputType(boolean imageDocument
) {
477 return image
.toString();
480 return text
.toString();
484 * Get the target {@link File} related to the given <tt>.info</tt>
485 * {@link File} and {@link MetaData}.
490 * the <tt>.info</tt> {@link File}
492 * @return the target {@link File}
494 private File
getTargetFile(MetaData meta
, File infoFile
) {
495 // Replace .info with whatever is needed:
496 String path
= infoFile
.getPath();
497 path
= path
.substring(0, path
.length() - ".info".length());
498 String newExt
= getOutputType(meta
).getDefaultExtension(true);
500 return new File(path
+ newExt
);
504 * The target (full path) where the {@link Story} related to this
505 * {@link MetaData} should be located on disk for a new {@link Story}.
508 * the {@link Story} {@link MetaData}
512 private File
getExpectedFile(MetaData key
) {
513 String title
= key
.getTitle();
517 title
= title
.replaceAll("[^a-zA-Z0-9._+-]", "_");
518 if (title
.length() > 40) {
519 title
= title
.substring(0, 40);
521 return new File(getExpectedDir(key
.getSource()),
522 key
.getLuid() + "_" + title
);
526 * The directory (full path) where the new {@link Story} related to this
527 * {@link MetaData} should be located on disk.
532 * @return the target directory
534 private File
getExpectedDir(String source
) {
535 String sanitizedSource
= source
.replaceAll("[^a-zA-Z0-9._+/-]", "_");
537 while (sanitizedSource
.startsWith("/")
538 || sanitizedSource
.startsWith("_")) {
539 if (sanitizedSource
.length() > 1) {
540 sanitizedSource
= sanitizedSource
.substring(1);
542 sanitizedSource
= "";
546 sanitizedSource
= sanitizedSource
.replace("/", File
.separator
);
548 if (sanitizedSource
.isEmpty()) {
549 sanitizedSource
= "_EMPTY";
552 return new File(baseDir
, sanitizedSource
);
556 * Return the full path to the file to use for the custom cover of this
559 * One or more of the parent directories <b>MAY</b> not exist.
564 * @return the custom cover file
566 private File
getAuthorCoverFile(String author
) {
567 File aDir
= new File(baseDir
, "_AUTHORS");
568 String hash
= HashUtils
.md5(author
);
569 String ext
= Instance
.getInstance().getConfig()
570 .getString(Config
.FILE_FORMAT_IMAGE_FORMAT_COVER
);
571 return new File(aDir
, hash
+ "." + ext
.toLowerCase());
575 * Return the list of files/directories on disk for this {@link Story}.
577 * If the {@link Story} is not found, and empty list is returned.
580 * the {@link Story} LUID
582 * @return the list of {@link File}s
584 * @throws IOException
585 * if the {@link Story} was not found
587 private List
<File
> getRelatedFiles(String luid
) throws IOException
{
588 List
<File
> files
= new ArrayList
<File
>();
590 MetaData meta
= getInfo(luid
);
592 throw new IOException("Story not found: " + luid
);
595 File infoFile
= getStories(null).get(meta
)[0];
596 File targetFile
= getStories(null).get(meta
)[1];
599 files
.add(targetFile
);
601 String readerExt
= getOutputType(meta
).getDefaultExtension(true);
602 String fileExt
= getOutputType(meta
).getDefaultExtension(false);
604 String path
= targetFile
.getAbsolutePath();
605 if (readerExt
!= null && !readerExt
.equals(fileExt
)) {
606 path
= path
.substring(0, path
.length() - readerExt
.length())
608 File relatedFile
= new File(path
);
610 if (relatedFile
.exists()) {
611 files
.add(relatedFile
);
615 String coverExt
= "." + Instance
.getInstance().getConfig()
616 .getString(Config
.FILE_FORMAT_IMAGE_FORMAT_COVER
).toLowerCase();
617 File coverFile
= new File(path
+ coverExt
);
618 if (!coverFile
.exists()) {
619 coverFile
= new File(
620 path
.substring(0, path
.length() - fileExt
.length())
624 if (coverFile
.exists()) {
625 files
.add(coverFile
);
628 String summaryExt
= ".summary";
629 File summaryFile
= new File(path
+ summaryExt
);
630 if (!summaryFile
.exists()) {
631 summaryFile
= new File(
632 path
.substring(0, path
.length() - fileExt
.length())
636 if (summaryFile
.exists()) {
637 files
.add(summaryFile
);
644 * Fill the list of stories by reading the content of the local directory
645 * {@link LocalLibrary#baseDir}.
647 * Will use a cached list when possible (see
648 * {@link BasicLibrary#invalidateInfo()}).
651 * the optional {@link Progress}
653 * @return the list of stories (for each item, the first {@link File} is the
654 * info file, the second file is the target {@link File})
656 private Map
<MetaData
, File
[]> getStories(Progress pg
) {
660 pg
.setMinMax(0, 100);
663 Map
<MetaData
, File
[]> stories
= this.stories
;
664 if (stories
== null) {
665 stories
= getStoriesDo(pg
);
666 synchronized (lock
) {
667 if (this.stories
== null)
668 this.stories
= stories
;
670 stories
= this.stories
;
680 * Actually do the work of {@link LocalLibrary#getStories(Progress)} (i.e.,
681 * do not retrieve the cache).
684 * the optional {@link Progress}
686 * @return the list of stories (for each item, the first {@link File} is the
687 * info file, the second file is the target {@link File})
689 private synchronized Map
<MetaData
, File
[]> getStoriesDo(Progress pg
) {
693 pg
.setMinMax(0, 100);
696 Map
<MetaData
, File
[]> stories
= new HashMap
<MetaData
, File
[]>();
698 File
[] dirs
= baseDir
.listFiles(new FileFilter() {
700 public boolean accept(File file
) {
701 return file
!= null && file
.isDirectory();
706 Progress pgDirs
= new Progress(0, 100 * dirs
.length
);
707 pg
.addProgress(pgDirs
, 100);
709 for (File dir
: dirs
) {
710 Progress pgFiles
= new Progress();
711 pgDirs
.addProgress(pgFiles
, 100);
712 pgDirs
.setName("Loading from: " + dir
.getName());
714 addToStories(stories
, pgFiles
, dir
);
716 pgFiles
.setName(null);
719 pgDirs
.setName("Loading directories");
727 private void addToStories(Map
<MetaData
, File
[]> stories
, Progress pgFiles
,
729 File
[] infoFilesAndSubdirs
= dir
.listFiles(new FileFilter() {
731 public boolean accept(File file
) {
732 boolean info
= file
!= null && file
.isFile()
733 && file
.getPath().toLowerCase().endsWith(".info");
734 boolean dir
= file
!= null && file
.isDirectory();
735 boolean isExpandedHtml
= new File(file
, "index.html").isFile();
736 return info
|| (dir
&& !isExpandedHtml
);
740 if (pgFiles
!= null) {
741 pgFiles
.setMinMax(0, infoFilesAndSubdirs
.length
);
744 for (File infoFileOrSubdir
: infoFilesAndSubdirs
) {
745 if (infoFileOrSubdir
.isDirectory()) {
746 addToStories(stories
, null, infoFileOrSubdir
);
749 MetaData meta
= InfoReader
.readMeta(infoFileOrSubdir
,
752 int id
= Integer
.parseInt(meta
.getLuid());
757 stories
.put(meta
, new File
[] { infoFileOrSubdir
,
758 getTargetFile(meta
, infoFileOrSubdir
) });
759 } catch (Exception e
) {
761 throw new IOException("Cannot understand the LUID of "
762 + infoFileOrSubdir
+ ": " + meta
.getLuid(), e
);
764 } catch (IOException e
) {
765 // We should not have not-supported files in the
767 Instance
.getInstance().getTraceHandler().error(
768 new IOException("Cannot load file from library: "
769 + infoFileOrSubdir
, e
));
773 if (pgFiles
!= null) {