1 package be
.nikiroo
.fanfix
.library
;
3 import java
.awt
.image
.BufferedImage
;
5 import java
.io
.FileFilter
;
6 import java
.io
.FileInputStream
;
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 javax
.imageio
.ImageIO
;
16 import be
.nikiroo
.fanfix
.Instance
;
17 import be
.nikiroo
.fanfix
.bundles
.Config
;
18 import be
.nikiroo
.fanfix
.data
.MetaData
;
19 import be
.nikiroo
.fanfix
.data
.Story
;
20 import be
.nikiroo
.fanfix
.output
.BasicOutput
;
21 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
22 import be
.nikiroo
.fanfix
.output
.InfoCover
;
23 import be
.nikiroo
.fanfix
.supported
.InfoReader
;
24 import be
.nikiroo
.utils
.IOUtils
;
25 import be
.nikiroo
.utils
.ImageUtils
;
26 import be
.nikiroo
.utils
.MarkableFileInputStream
;
27 import be
.nikiroo
.utils
.Progress
;
30 * This {@link BasicLibrary} will store the stories locally on disk.
34 public class LocalLibrary
extends BasicLibrary
{
36 private Map
<MetaData
, File
[]> stories
; // Files: [ infoFile, TargetFile ]
37 private Map
<String
, BufferedImage
> sourceCovers
;
40 private OutputType text
;
41 private OutputType image
;
44 * Create a new {@link LocalLibrary} with the given back-end directory.
47 * the directory where to find the {@link Story} objects
49 public LocalLibrary(File baseDir
) {
50 this(baseDir
, Instance
.getConfig().getString(
51 Config
.NON_IMAGES_DOCUMENT_TYPE
), Instance
.getConfig()
52 .getString(Config
.IMAGES_DOCUMENT_TYPE
), false);
56 * Create a new {@link LocalLibrary} with the given back-end directory.
59 * the directory where to find the {@link Story} objects
61 * the {@link OutputType} to use for non-image documents
63 * the {@link OutputType} to use for image documents
64 * @param defaultIsHtml
65 * if the given text or image is invalid, use HTML by default (if
66 * not, it will be INFO_TEXT/CBZ by default)
68 public LocalLibrary(File baseDir
, String text
, String image
,
69 boolean defaultIsHtml
) {
70 this(baseDir
, OutputType
.valueOfAllOkUC(text
,
71 defaultIsHtml ? OutputType
.HTML
: OutputType
.INFO_TEXT
),
72 OutputType
.valueOfAllOkUC(image
,
73 defaultIsHtml ? OutputType
.HTML
: OutputType
.CBZ
));
77 * Create a new {@link LocalLibrary} with the given back-end directory.
80 * the directory where to find the {@link Story} objects
82 * the {@link OutputType} to use for non-image documents
84 * the {@link OutputType} to use for image documents
86 public LocalLibrary(File baseDir
, OutputType text
, OutputType image
) {
87 this.baseDir
= baseDir
;
93 this.sourceCovers
= new HashMap
<String
, BufferedImage
>();
99 protected List
<MetaData
> getMetas(Progress pg
) {
100 return new ArrayList
<MetaData
>(getStories(pg
).keySet());
104 public File
getFile(String luid
, Progress pg
) {
105 File
[] files
= getStories(pg
).get(getInfo(luid
));
114 public BufferedImage
getCover(String luid
) {
115 MetaData meta
= getInfo(luid
);
117 File
[] files
= getStories(null).get(meta
);
119 File infoFile
= files
[0];
122 meta
= InfoReader
.readMeta(infoFile
, true);
123 return meta
.getCover();
124 } catch (IOException e
) {
125 Instance
.getTraceHandler().error(e
);
134 protected void invalidateInfo(String luid
) {
136 sourceCovers
= new HashMap
<String
, BufferedImage
>();
140 protected synchronized int getNextId() {
141 getStories(null); // make sure lastId is set
146 protected void doDelete(String luid
) throws IOException
{
147 for (File file
: getRelatedFiles(luid
)) {
148 // TODO: throw an IOException if we cannot delete the files?
149 IOUtils
.deltree(file
);
150 file
.getParentFile().delete();
155 protected Story
doSave(Story story
, Progress pg
) throws IOException
{
156 MetaData meta
= story
.getMeta();
158 File expectedTarget
= getExpectedFile(meta
);
159 expectedTarget
.getParentFile().mkdirs();
161 BasicOutput it
= BasicOutput
.getOutput(getOutputType(meta
), true, true);
162 it
.process(story
, expectedTarget
.getPath(), pg
);
168 protected synchronized void saveMeta(MetaData meta
, Progress pg
)
170 File newDir
= getExpectedDir(meta
.getSource());
171 if (!newDir
.exists()) {
175 List
<File
> relatedFiles
= getRelatedFiles(meta
.getLuid());
176 for (File relatedFile
: relatedFiles
) {
177 // TODO: this is not safe at all.
178 // We should copy all the files THEN delete them
179 // Maybe also adding some rollback cleanup if possible
180 if (relatedFile
.getName().endsWith(".info")) {
182 String name
= relatedFile
.getName().replaceFirst(
184 InfoCover
.writeInfo(newDir
, name
, meta
);
185 relatedFile
.delete();
186 relatedFile
.getParentFile().delete();
187 } catch (IOException e
) {
188 Instance
.getTraceHandler().error(e
);
191 relatedFile
.renameTo(new File(newDir
, relatedFile
.getName()));
192 relatedFile
.getParentFile().delete();
200 public BufferedImage
getSourceCover(String source
) {
201 if (!sourceCovers
.containsKey(source
)) {
202 sourceCovers
.put(source
, super.getSourceCover(source
));
205 return sourceCovers
.get(source
);
209 public void setSourceCover(String source
, String luid
) {
210 sourceCovers
.put(source
, getCover(luid
));
211 File cover
= new File(getExpectedDir(source
), ".cover.png");
213 ImageIO
.write(sourceCovers
.get(source
), "png", cover
);
214 } catch (IOException e
) {
215 Instance
.getTraceHandler().error(e
);
216 sourceCovers
.remove(source
);
221 public void imprt(BasicLibrary other
, String luid
, Progress pg
)
227 // Check if we can simply copy the files instead of the whole process
228 if (other
instanceof LocalLibrary
) {
229 LocalLibrary otherLocalLibrary
= (LocalLibrary
) other
;
231 MetaData meta
= otherLocalLibrary
.getInfo(luid
);
232 String expectedType
= ""
233 + (meta
!= null && meta
.isImageDocument() ? image
: text
);
234 if (meta
!= null && meta
.getType().equals(expectedType
)) {
235 File from
= otherLocalLibrary
.getExpectedDir(meta
.getSource());
236 File to
= this.getExpectedDir(meta
.getSource());
237 List
<File
> sources
= otherLocalLibrary
.getRelatedFiles(luid
);
238 if (!sources
.isEmpty()) {
239 pg
.setMinMax(0, sources
.size());
242 for (File source
: sources
) {
243 File target
= new File(source
.getAbsolutePath().replace(
244 from
.getAbsolutePath(), to
.getAbsolutePath()));
245 if (!source
.equals(target
)) {
246 target
.getParentFile().mkdirs();
247 InputStream in
= null;
249 in
= new FileInputStream(source
);
250 IOUtils
.write(in
, target
);
251 } catch (IOException e
) {
255 } catch (Exception ee
) {
273 super.imprt(other
, luid
, pg
);
279 * Return the {@link OutputType} for this {@link Story}.
282 * the {@link Story} {@link MetaData}
286 private OutputType
getOutputType(MetaData meta
) {
287 if (meta
!= null && meta
.isImageDocument()) {
295 * Get the target {@link File} related to the given <tt>.info</tt>
296 * {@link File} and {@link MetaData}.
301 * the <tt>.info</tt> {@link File}
303 * @return the target {@link File}
305 private File
getTargetFile(MetaData meta
, File infoFile
) {
306 // Replace .info with whatever is needed:
307 String path
= infoFile
.getPath();
308 path
= path
.substring(0, path
.length() - ".info".length());
309 String newExt
= getOutputType(meta
).getDefaultExtension(true);
311 return new File(path
+ newExt
);
315 * The target (full path) where the {@link Story} related to this
316 * {@link MetaData} should be located on disk for a new {@link Story}.
319 * the {@link Story} {@link MetaData}
323 private File
getExpectedFile(MetaData key
) {
324 String title
= key
.getTitle();
328 title
= title
.replaceAll("[^a-zA-Z0-9._+-]", "_");
329 return new File(getExpectedDir(key
.getSource()), key
.getLuid() + "_"
334 * The directory (full path) where the new {@link Story} related to this
335 * {@link MetaData} should be located on disk.
340 * @return the target directory
342 private File
getExpectedDir(String source
) {
343 String sanitizedSource
= source
.replaceAll("[^a-zA-Z0-9._+-]", "_");
344 return new File(baseDir
, sanitizedSource
);
348 * Return the list of files/directories on disk for this {@link Story}.
350 * If the {@link Story} is not found, and empty list is returned.
353 * the {@link Story} LUID
355 * @return the list of {@link File}s
357 * @throws IOException
358 * if the {@link Story} was not found
360 private List
<File
> getRelatedFiles(String luid
) throws IOException
{
361 List
<File
> files
= new ArrayList
<File
>();
363 MetaData meta
= getInfo(luid
);
365 throw new IOException("Story not found: " + luid
);
368 File infoFile
= getStories(null).get(meta
)[0];
369 File targetFile
= getStories(null).get(meta
)[1];
372 files
.add(targetFile
);
374 String readerExt
= getOutputType(meta
).getDefaultExtension(true);
375 String fileExt
= getOutputType(meta
).getDefaultExtension(false);
377 String path
= targetFile
.getAbsolutePath();
378 if (readerExt
!= null && !readerExt
.equals(fileExt
)) {
379 path
= path
.substring(0, path
.length() - readerExt
.length())
381 File relatedFile
= new File(path
);
383 if (relatedFile
.exists()) {
384 files
.add(relatedFile
);
388 String coverExt
= "."
389 + Instance
.getConfig().getString(Config
.IMAGE_FORMAT_COVER
)
391 File coverFile
= new File(path
+ coverExt
);
392 if (!coverFile
.exists()) {
393 coverFile
= new File(path
.substring(0,
394 path
.length() - fileExt
.length())
398 if (coverFile
.exists()) {
399 files
.add(coverFile
);
406 * Fill the list of stories by reading the content of the local directory
407 * {@link LocalLibrary#baseDir}.
409 * Will use a cached list when possible (see
410 * {@link BasicLibrary#invalidateInfo()}).
413 * the optional {@link Progress}
415 * @return the list of stories
417 private synchronized Map
<MetaData
, File
[]> getStories(Progress pg
) {
421 pg
.setMinMax(0, 100);
424 if (stories
== null) {
425 stories
= new HashMap
<MetaData
, File
[]>();
429 File
[] dirs
= baseDir
.listFiles(new FileFilter() {
431 public boolean accept(File file
) {
432 return file
!= null && file
.isDirectory();
436 Progress pgDirs
= new Progress(0, 100 * dirs
.length
);
437 pg
.addProgress(pgDirs
, 100);
439 for (File dir
: dirs
) {
440 File
[] infoFiles
= dir
.listFiles(new FileFilter() {
442 public boolean accept(File file
) {
444 && file
.getPath().toLowerCase()
449 Progress pgFiles
= new Progress(0, infoFiles
.length
);
450 pgDirs
.addProgress(pgFiles
, 100);
451 pgDirs
.setName("Loading from: " + dir
.getName());
453 String source
= null;
454 for (File infoFile
: infoFiles
) {
455 pgFiles
.setName(infoFile
.getName());
457 MetaData meta
= InfoReader
.readMeta(infoFile
, false);
458 source
= meta
.getSource();
460 int id
= Integer
.parseInt(meta
.getLuid());
465 stories
.put(meta
, new File
[] { infoFile
,
466 getTargetFile(meta
, infoFile
) });
467 } catch (Exception e
) {
469 throw new IOException(
470 "Cannot understand the LUID of "
473 + (meta
== null ?
"[meta is NULL]"
474 : meta
.getLuid()), e
);
476 } catch (IOException e
) {
477 // We should not have not-supported files in the
479 Instance
.getTraceHandler().error(
481 "Cannot load file from library: "
487 File cover
= new File(dir
, ".cover.png");
488 if (cover
.exists()) {
490 InputStream in
= new MarkableFileInputStream(
491 new FileInputStream(cover
));
493 sourceCovers
.put(source
, ImageUtils
.fromStream(in
));
497 } catch (IOException e
) {
498 Instance
.getTraceHandler().error(e
);
502 pgFiles
.setName(null);
505 pgDirs
.setName("Loading directories");
512 * Fix the source cover to the given story cover.
515 * the source to change
519 void setSourceCover(String source
, BufferedImage coverImage
) {
520 sourceCovers
.put(source
, coverImage
);
521 File cover
= new File(getExpectedDir(source
), ".cover.png");
523 ImageIO
.write(sourceCovers
.get(source
), "png", cover
);
524 } catch (IOException e
) {
525 Instance
.getTraceHandler().error(e
);
526 sourceCovers
.remove(source
);