Fix some warnings
[nikiroo-utils.git] / src / be / nikiroo / fanfix / library / LocalLibrary.java
CommitLineData
e42573a0 1package be.nikiroo.fanfix.library;
68e2c6d2 2
68e2c6d2
NR
3import java.io.File;
4import java.io.FileFilter;
14b57448 5import java.io.FileInputStream;
68e2c6d2 6import java.io.IOException;
14b57448 7import java.io.InputStream;
68e2c6d2
NR
8import java.util.ArrayList;
9import java.util.HashMap;
10import java.util.List;
11import java.util.Map;
12
e42573a0 13import be.nikiroo.fanfix.Instance;
68e2c6d2
NR
14import be.nikiroo.fanfix.bundles.Config;
15import be.nikiroo.fanfix.data.MetaData;
16import be.nikiroo.fanfix.data.Story;
17import be.nikiroo.fanfix.output.BasicOutput;
18import be.nikiroo.fanfix.output.BasicOutput.OutputType;
19import be.nikiroo.fanfix.output.InfoCover;
20import be.nikiroo.fanfix.supported.InfoReader;
21import be.nikiroo.utils.IOUtils;
16a81ef7 22import be.nikiroo.utils.Image;
14b57448 23import be.nikiroo.utils.MarkableFileInputStream;
68e2c6d2
NR
24import be.nikiroo.utils.Progress;
25
26/**
27 * This {@link BasicLibrary} will store the stories locally on disk.
28 *
29 * @author niki
30 */
31public class LocalLibrary extends BasicLibrary {
32 private int lastId;
33 private Map<MetaData, File[]> stories; // Files: [ infoFile, TargetFile ]
16a81ef7 34 private Map<String, Image> sourceCovers;
68e2c6d2
NR
35
36 private File baseDir;
37 private OutputType text;
38 private OutputType image;
39
e604986c
NR
40 /**
41 * Create a new {@link LocalLibrary} with the given back-end directory.
42 *
43 * @param baseDir
44 * the directory where to find the {@link Story} objects
45 */
46 public LocalLibrary(File baseDir) {
47 this(baseDir, Instance.getConfig().getString(
48 Config.NON_IMAGES_DOCUMENT_TYPE), Instance.getConfig()
49 .getString(Config.IMAGES_DOCUMENT_TYPE), false);
50 }
51
52 /**
53 * Create a new {@link LocalLibrary} with the given back-end directory.
54 *
55 * @param baseDir
56 * the directory where to find the {@link Story} objects
ff05b828
NR
57 * @param text
58 * the {@link OutputType} to use for non-image documents
59 * @param image
60 * the {@link OutputType} to use for image documents
61 * @param defaultIsHtml
62 * if the given text or image is invalid, use HTML by default (if
63 * not, it will be INFO_TEXT/CBZ by default)
e604986c
NR
64 */
65 public LocalLibrary(File baseDir, String text, String image,
66 boolean defaultIsHtml) {
ff05b828 67 this(baseDir, OutputType.valueOfAllOkUC(text,
e604986c 68 defaultIsHtml ? OutputType.HTML : OutputType.INFO_TEXT),
ff05b828 69 OutputType.valueOfAllOkUC(image,
e604986c
NR
70 defaultIsHtml ? OutputType.HTML : OutputType.CBZ));
71 }
72
68e2c6d2
NR
73 /**
74 * Create a new {@link LocalLibrary} with the given back-end directory.
75 *
76 * @param baseDir
77 * the directory where to find the {@link Story} objects
78 * @param text
e604986c 79 * the {@link OutputType} to use for non-image documents
68e2c6d2 80 * @param image
e604986c 81 * the {@link OutputType} to use for image documents
68e2c6d2
NR
82 */
83 public LocalLibrary(File baseDir, OutputType text, OutputType image) {
84 this.baseDir = baseDir;
85 this.text = text;
86 this.image = image;
87
88 this.lastId = 0;
89 this.stories = null;
16a81ef7 90 this.sourceCovers = new HashMap<String, Image>();
68e2c6d2
NR
91
92 baseDir.mkdirs();
93 }
94
95 @Override
96 protected List<MetaData> getMetas(Progress pg) {
97 return new ArrayList<MetaData>(getStories(pg).keySet());
98 }
99
100 @Override
ff05b828
NR
101 public File getFile(String luid, Progress pg) {
102 File[] files = getStories(pg).get(getInfo(luid));
68e2c6d2
NR
103 if (files != null) {
104 return files[1];
105 }
106
107 return null;
108 }
109
110 @Override
16a81ef7 111 public Image getCover(String luid) {
68e2c6d2
NR
112 MetaData meta = getInfo(luid);
113 if (meta != null) {
114 File[] files = getStories(null).get(meta);
115 if (files != null) {
116 File infoFile = files[0];
117
118 try {
119 meta = InfoReader.readMeta(infoFile, true);
120 return meta.getCover();
121 } catch (IOException e) {
62c63b07 122 Instance.getTraceHandler().error(e);
68e2c6d2
NR
123 }
124 }
125 }
126
127 return null;
128 }
129
130 @Override
9fe3f177 131 protected synchronized void invalidateInfo(String luid) {
68e2c6d2 132 stories = null;
16a81ef7 133 sourceCovers = new HashMap<String, Image>();
68e2c6d2
NR
134 }
135
136 @Override
137 protected synchronized int getNextId() {
14b57448 138 getStories(null); // make sure lastId is set
68e2c6d2
NR
139 return ++lastId;
140 }
141
142 @Override
143 protected void doDelete(String luid) throws IOException {
144 for (File file : getRelatedFiles(luid)) {
145 // TODO: throw an IOException if we cannot delete the files?
146 IOUtils.deltree(file);
e272f05f 147 file.getParentFile().delete();
68e2c6d2
NR
148 }
149 }
150
151 @Override
152 protected Story doSave(Story story, Progress pg) throws IOException {
153 MetaData meta = story.getMeta();
154
155 File expectedTarget = getExpectedFile(meta);
156 expectedTarget.getParentFile().mkdirs();
157
925298fd 158 BasicOutput it = BasicOutput.getOutput(getOutputType(meta), true, true);
68e2c6d2
NR
159 it.process(story, expectedTarget.getPath(), pg);
160
161 return story;
162 }
163
164 @Override
165 protected synchronized void saveMeta(MetaData meta, Progress pg)
166 throws IOException {
167 File newDir = getExpectedDir(meta.getSource());
168 if (!newDir.exists()) {
169 newDir.mkdir();
170 }
171
172 List<File> relatedFiles = getRelatedFiles(meta.getLuid());
173 for (File relatedFile : relatedFiles) {
174 // TODO: this is not safe at all.
175 // We should copy all the files THEN delete them
176 // Maybe also adding some rollback cleanup if possible
177 if (relatedFile.getName().endsWith(".info")) {
178 try {
179 String name = relatedFile.getName().replaceFirst(
180 "\\.info$", "");
181 InfoCover.writeInfo(newDir, name, meta);
182 relatedFile.delete();
e272f05f 183 relatedFile.getParentFile().delete();
68e2c6d2 184 } catch (IOException e) {
62c63b07 185 Instance.getTraceHandler().error(e);
68e2c6d2
NR
186 }
187 } else {
188 relatedFile.renameTo(new File(newDir, relatedFile.getName()));
e272f05f 189 relatedFile.getParentFile().delete();
68e2c6d2
NR
190 }
191 }
192
e272f05f 193 invalidateInfo();
68e2c6d2
NR
194 }
195
14b57448 196 @Override
16a81ef7 197 public Image getSourceCover(String source) {
14b57448
NR
198 if (!sourceCovers.containsKey(source)) {
199 sourceCovers.put(source, super.getSourceCover(source));
200 }
201
202 return sourceCovers.get(source);
203 }
204
205 @Override
206 public void setSourceCover(String source, String luid) {
207 sourceCovers.put(source, getCover(luid));
208 File cover = new File(getExpectedDir(source), ".cover.png");
209 try {
16a81ef7
NR
210 Instance.getCache().saveAsImage(sourceCovers.get(source), cover,
211 true);
14b57448 212 } catch (IOException e) {
62c63b07 213 Instance.getTraceHandler().error(e);
14b57448
NR
214 sourceCovers.remove(source);
215 }
216 }
217
b89dfb6e
NR
218 @Override
219 public void imprt(BasicLibrary other, String luid, Progress pg)
220 throws IOException {
221 if (pg == null) {
222 pg = new Progress();
223 }
224
ff05b828 225 // Check if we can simply copy the files instead of the whole process
b89dfb6e 226 if (other instanceof LocalLibrary) {
ff05b828 227 LocalLibrary otherLocalLibrary = (LocalLibrary) other;
e604986c 228
e604986c
NR
229 MetaData meta = otherLocalLibrary.getInfo(luid);
230 String expectedType = ""
231 + (meta != null && meta.isImageDocument() ? image : text);
232 if (meta != null && meta.getType().equals(expectedType)) {
233 File from = otherLocalLibrary.getExpectedDir(meta.getSource());
b89dfb6e 234 File to = this.getExpectedDir(meta.getSource());
e604986c 235 List<File> sources = otherLocalLibrary.getRelatedFiles(luid);
b89dfb6e
NR
236 if (!sources.isEmpty()) {
237 pg.setMinMax(0, sources.size());
238 }
239
240 for (File source : sources) {
241 File target = new File(source.getAbsolutePath().replace(
242 from.getAbsolutePath(), to.getAbsolutePath()));
243 if (!source.equals(target)) {
e604986c 244 target.getParentFile().mkdirs();
b89dfb6e
NR
245 InputStream in = null;
246 try {
247 in = new FileInputStream(source);
248 IOUtils.write(in, target);
249 } catch (IOException e) {
250 if (in != null) {
251 try {
252 in.close();
253 } catch (Exception ee) {
254 }
255 }
256
257 pg.done();
258 throw e;
259 }
260 }
261
262 pg.add(1);
263 }
264
e272f05f 265 invalidateInfo();
b89dfb6e
NR
266 pg.done();
267 return;
268 }
269 }
270
271 super.imprt(other, luid, pg);
e604986c 272
e272f05f 273 invalidateInfo();
b89dfb6e
NR
274 }
275
68e2c6d2
NR
276 /**
277 * Return the {@link OutputType} for this {@link Story}.
278 *
279 * @param meta
280 * the {@link Story} {@link MetaData}
281 *
282 * @return the type
283 */
284 private OutputType getOutputType(MetaData meta) {
285 if (meta != null && meta.isImageDocument()) {
286 return image;
68e2c6d2 287 }
211f7ddb
NR
288
289 return text;
68e2c6d2
NR
290 }
291
292 /**
293 * Get the target {@link File} related to the given <tt>.info</tt>
294 * {@link File} and {@link MetaData}.
295 *
296 * @param meta
297 * the meta
298 * @param infoFile
299 * the <tt>.info</tt> {@link File}
300 *
301 * @return the target {@link File}
302 */
303 private File getTargetFile(MetaData meta, File infoFile) {
304 // Replace .info with whatever is needed:
305 String path = infoFile.getPath();
306 path = path.substring(0, path.length() - ".info".length());
307 String newExt = getOutputType(meta).getDefaultExtension(true);
308
309 return new File(path + newExt);
310 }
311
312 /**
313 * The target (full path) where the {@link Story} related to this
314 * {@link MetaData} should be located on disk for a new {@link Story}.
315 *
316 * @param key
317 * the {@link Story} {@link MetaData}
318 *
319 * @return the target
320 */
321 private File getExpectedFile(MetaData key) {
322 String title = key.getTitle();
323 if (title == null) {
324 title = "";
325 }
326 title = title.replaceAll("[^a-zA-Z0-9._+-]", "_");
327 return new File(getExpectedDir(key.getSource()), key.getLuid() + "_"
328 + title);
329 }
330
331 /**
332 * The directory (full path) where the new {@link Story} related to this
333 * {@link MetaData} should be located on disk.
334 *
085a2f9a 335 * @param source
68e2c6d2
NR
336 * the type (source)
337 *
338 * @return the target directory
339 */
085a2f9a
NR
340 private File getExpectedDir(String source) {
341 String sanitizedSource = source.replaceAll("[^a-zA-Z0-9._+-]", "_");
342 return new File(baseDir, sanitizedSource);
68e2c6d2
NR
343 }
344
345 /**
346 * Return the list of files/directories on disk for this {@link Story}.
347 * <p>
348 * If the {@link Story} is not found, and empty list is returned.
349 *
350 * @param luid
351 * the {@link Story} LUID
352 *
353 * @return the list of {@link File}s
354 *
355 * @throws IOException
356 * if the {@link Story} was not found
357 */
358 private List<File> getRelatedFiles(String luid) throws IOException {
359 List<File> files = new ArrayList<File>();
360
361 MetaData meta = getInfo(luid);
362 if (meta == null) {
363 throw new IOException("Story not found: " + luid);
211f7ddb 364 }
68e2c6d2 365
211f7ddb
NR
366 File infoFile = getStories(null).get(meta)[0];
367 File targetFile = getStories(null).get(meta)[1];
68e2c6d2 368
211f7ddb
NR
369 files.add(infoFile);
370 files.add(targetFile);
68e2c6d2 371
211f7ddb
NR
372 String readerExt = getOutputType(meta).getDefaultExtension(true);
373 String fileExt = getOutputType(meta).getDefaultExtension(false);
68e2c6d2 374
211f7ddb
NR
375 String path = targetFile.getAbsolutePath();
376 if (readerExt != null && !readerExt.equals(fileExt)) {
377 path = path.substring(0, path.length() - readerExt.length())
378 + fileExt;
379 File relatedFile = new File(path);
68e2c6d2 380
211f7ddb
NR
381 if (relatedFile.exists()) {
382 files.add(relatedFile);
68e2c6d2 383 }
211f7ddb 384 }
68e2c6d2 385
211f7ddb 386 String coverExt = "."
2a25f781
NR
387 + Instance.getConfig().getString(Config.IMAGE_FORMAT_COVER)
388 .toLowerCase();
211f7ddb
NR
389 File coverFile = new File(path + coverExt);
390 if (!coverFile.exists()) {
391 coverFile = new File(path.substring(0,
392 path.length() - fileExt.length())
393 + coverExt);
394 }
395
396 if (coverFile.exists()) {
397 files.add(coverFile);
68e2c6d2
NR
398 }
399
400 return files;
401 }
402
403 /**
404 * Fill the list of stories by reading the content of the local directory
405 * {@link LocalLibrary#baseDir}.
406 * <p>
407 * Will use a cached list when possible (see
e272f05f 408 * {@link BasicLibrary#invalidateInfo()}).
68e2c6d2
NR
409 *
410 * @param pg
411 * the optional {@link Progress}
412 *
413 * @return the list of stories
414 */
415 private synchronized Map<MetaData, File[]> getStories(Progress pg) {
416 if (pg == null) {
417 pg = new Progress();
418 } else {
419 pg.setMinMax(0, 100);
420 }
421
422 if (stories == null) {
423 stories = new HashMap<MetaData, File[]>();
424
425 lastId = 0;
426
427 File[] dirs = baseDir.listFiles(new FileFilter() {
211f7ddb 428 @Override
68e2c6d2
NR
429 public boolean accept(File file) {
430 return file != null && file.isDirectory();
431 }
432 });
433
b4f9071c
NR
434 if (dirs != null) {
435 Progress pgDirs = new Progress(0, 100 * dirs.length);
436 pg.addProgress(pgDirs, 100);
437
438 for (File dir : dirs) {
439 File[] infoFiles = dir.listFiles(new FileFilter() {
440 @Override
441 public boolean accept(File file) {
442 return file != null
443 && file.getPath().toLowerCase()
444 .endsWith(".info");
445 }
446 });
447
448 Progress pgFiles = new Progress(0, infoFiles.length);
449 pgDirs.addProgress(pgFiles, 100);
450 pgDirs.setName("Loading from: " + dir.getName());
451
452 String source = null;
453 for (File infoFile : infoFiles) {
454 pgFiles.setName(infoFile.getName());
68e2c6d2 455 try {
b4f9071c
NR
456 MetaData meta = InfoReader
457 .readMeta(infoFile, false);
458 source = meta.getSource();
459 try {
460 int id = Integer.parseInt(meta.getLuid());
461 if (id > lastId) {
462 lastId = id;
463 }
68e2c6d2 464
b4f9071c
NR
465 stories.put(meta, new File[] { infoFile,
466 getTargetFile(meta, infoFile) });
467 } catch (Exception e) {
468 // not normal!!
469 throw new IOException(
470 "Cannot understand the LUID of "
471 + infoFile
472 + ": "
473 + (meta == null ? "[meta is NULL]"
474 : meta.getLuid()), e);
475 }
476 } catch (IOException e) {
477 // We should not have not-supported files in the
478 // library
479 Instance.getTraceHandler().error(
480 new IOException(
481 "Cannot load file from library: "
482 + infoFile, e));
68e2c6d2 483 }
b4f9071c 484 pgFiles.add(1);
68e2c6d2 485 }
68e2c6d2 486
b4f9071c
NR
487 File cover = new File(dir, ".cover.png");
488 if (cover.exists()) {
14b57448 489 try {
b4f9071c
NR
490 InputStream in = new FileInputStream(cover);
491 try {
492 sourceCovers.put(source, new Image(in));
493 } finally {
494 in.close();
495 }
496 } catch (IOException e) {
497 Instance.getTraceHandler().error(e);
14b57448 498 }
14b57448 499 }
b4f9071c
NR
500
501 pgFiles.setName(null);
14b57448
NR
502 }
503
b4f9071c 504 pgDirs.setName("Loading directories");
68e2c6d2 505 }
68e2c6d2
NR
506 }
507
b4f9071c 508 pg.done();
68e2c6d2
NR
509 return stories;
510 }
085a2f9a
NR
511
512 /**
513 * Fix the source cover to the given story cover.
514 *
515 * @param source
516 * the source to change
517 * @param coverImage
518 * the cover image
519 */
16a81ef7 520 void setSourceCover(String source, Image coverImage) {
085a2f9a
NR
521 sourceCovers.put(source, coverImage);
522 File cover = new File(getExpectedDir(source), ".cover.png");
523 try {
16a81ef7
NR
524 Instance.getCache().saveAsImage(sourceCovers.get(source), cover,
525 true);
085a2f9a 526 } catch (IOException e) {
62c63b07 527 Instance.getTraceHandler().error(e);
085a2f9a
NR
528 sourceCovers.remove(source);
529 }
530 }
68e2c6d2 531}