Version 1.5.0
[fanfix.git] / src / be / nikiroo / fanfix / Library.java
CommitLineData
08fe2e33
NR
1package be.nikiroo.fanfix;
2
57f02339 3import java.awt.image.BufferedImage;
08fe2e33 4import java.io.File;
4f661b2b 5import java.io.FileFilter;
08fe2e33
NR
6import java.io.IOException;
7import java.net.URL;
8import java.util.ArrayList;
22848428 9import java.util.Collections;
08fe2e33
NR
10import java.util.HashMap;
11import java.util.List;
12import java.util.Map;
13import java.util.Map.Entry;
14
10d558d2 15import be.nikiroo.fanfix.bundles.Config;
08fe2e33
NR
16import be.nikiroo.fanfix.data.MetaData;
17import be.nikiroo.fanfix.data.Story;
18import be.nikiroo.fanfix.output.BasicOutput;
19import be.nikiroo.fanfix.output.BasicOutput.OutputType;
70c9b112 20import be.nikiroo.fanfix.output.InfoCover;
08fe2e33
NR
21import be.nikiroo.fanfix.supported.BasicSupport;
22import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
68686a37 23import be.nikiroo.fanfix.supported.InfoReader;
9843a5e5 24import be.nikiroo.utils.IOUtils;
3b2b638f 25import be.nikiroo.utils.Progress;
08fe2e33
NR
26
27/**
28 * Manage a library of Stories: import, export, list.
29 * <p>
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}.
57f02339
NR
32 * <p>
33 * Most of the {@link Library} functions work on either the LUID or a partial
34 * (cover not included) {@link MetaData} object.
08fe2e33
NR
35 *
36 * @author niki
37 */
38public class Library {
39 private File baseDir;
40 private Map<MetaData, File> stories;
08fe2e33 41 private int lastId;
2206ef66
NR
42 private OutputType text;
43 private OutputType image;
08fe2e33
NR
44
45 /**
46 * Create a new {@link Library} with the given backend directory.
47 *
48 * @param dir
2206ef66
NR
49 * the directory where to find the {@link Story} objects
50 * @param text
51 * the {@link OutputType} to save the text-focused stories into
52 * @param image
53 * the {@link OutputType} to save the images-focused stories into
08fe2e33 54 */
2206ef66 55 public Library(File dir, OutputType text, OutputType image) {
08fe2e33
NR
56 this.baseDir = dir;
57 this.stories = new HashMap<MetaData, File>();
58 this.lastId = 0;
2206ef66
NR
59 this.text = text;
60 this.image = image;
08fe2e33
NR
61
62 dir.mkdirs();
63 }
64
4f661b2b
NR
65 /**
66 * Refresh the {@link Library}, that is, make sure all stories are loaded.
67 *
68 * @param pg
69 * the optional progress reporter
70 */
71 public void refresh(Progress pg) {
72 getStories(pg);
73 }
74
333f0e7b 75 /**
70c9b112 76 * List all the known types (sources) of stories.
333f0e7b
NR
77 *
78 * @return the types
79 */
10d558d2 80 public synchronized List<String> getTypes() {
333f0e7b 81 List<String> list = new ArrayList<String>();
4f661b2b 82 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
4310bae9 83 String storyType = entry.getKey().getSource();
333f0e7b
NR
84 if (!list.contains(storyType)) {
85 list.add(storyType);
86 }
87 }
88
4310bae9
NR
89 Collections.sort(list);
90 return list;
91 }
92
93 /**
94 * List all the known authors of stories.
95 *
96 * @return the authors
97 */
98 public synchronized List<String> getAuthors() {
99 List<String> list = new ArrayList<String>();
4f661b2b 100 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
4310bae9
NR
101 String storyAuthor = entry.getKey().getAuthor();
102 if (!list.contains(storyAuthor)) {
103 list.add(storyAuthor);
104 }
105 }
106
107 Collections.sort(list);
108 return list;
109 }
110
111 /**
112 * List all the stories of the given author in the {@link Library}, or all
113 * the stories if NULL is passed as an author.
57f02339
NR
114 * <p>
115 * Cover images not included.
4310bae9
NR
116 *
117 * @param author
118 * the author of the stories to retrieve, or NULL for all
119 *
120 * @return the stories
121 */
122 public synchronized List<MetaData> getListByAuthor(String author) {
123 List<MetaData> list = new ArrayList<MetaData>();
4f661b2b 124 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
4310bae9
NR
125 String storyAuthor = entry.getKey().getAuthor();
126 if (author == null || author.equalsIgnoreCase(storyAuthor)) {
127 list.add(entry.getKey());
128 }
129 }
130
131 Collections.sort(list);
333f0e7b
NR
132 return list;
133 }
134
08fe2e33
NR
135 /**
136 * List all the stories of the given source type in the {@link Library}, or
137 * all the stories if NULL is passed as a type.
57f02339
NR
138 * <p>
139 * Cover images not included.
08fe2e33
NR
140 *
141 * @param type
142 * the type of story to retrieve, or NULL for all
143 *
144 * @return the stories
145 */
4310bae9 146 public synchronized List<MetaData> getListByType(String type) {
70c9b112
NR
147 if (type != null) {
148 // convert the type to dir name
149 type = getDir(type).getName();
150 }
151
08fe2e33 152 List<MetaData> list = new ArrayList<MetaData>();
4f661b2b 153 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
08fe2e33 154 String storyType = entry.getValue().getParentFile().getName();
333f0e7b 155 if (type == null || type.equalsIgnoreCase(storyType)) {
08fe2e33
NR
156 list.add(entry.getKey());
157 }
158 }
159
22848428 160 Collections.sort(list);
08fe2e33
NR
161 return list;
162 }
163
301791d3 164 /**
57f02339
NR
165 * Retrieve a {@link File} corresponding to the given {@link Story}, cover
166 * image not included.
301791d3
NR
167 *
168 * @param luid
169 * the Library UID of the story
170 *
171 * @return the corresponding {@link Story}
172 */
10d558d2 173 public synchronized MetaData getInfo(String luid) {
301791d3 174 if (luid != null) {
4f661b2b 175 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
301791d3
NR
176 if (luid.equals(entry.getKey().getLuid())) {
177 return entry.getKey();
178 }
179 }
180 }
181
182 return null;
183 }
184
2206ef66
NR
185 /**
186 * Retrieve a {@link File} corresponding to the given {@link Story}.
187 *
188 * @param luid
189 * the Library UID of the story
190 *
191 * @return the corresponding {@link Story}
192 */
10d558d2 193 public synchronized File getFile(String luid) {
2206ef66 194 if (luid != null) {
4f661b2b 195 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
2206ef66
NR
196 if (luid.equals(entry.getKey().getLuid())) {
197 return entry.getValue();
198 }
199 }
200 }
201
202 return null;
203 }
204
57f02339
NR
205 /**
206 * Return the cover image associated to this story.
207 *
208 * @param luid
209 * the Library UID of the story
210 *
211 * @return the cover image
212 */
213 public synchronized BufferedImage getCover(String luid) {
214 MetaData meta = getInfo(luid);
215 if (meta != null) {
216 try {
217 File infoFile = new File(getFile(meta).getPath() + ".info");
218 meta = readMeta(infoFile, true).getKey();
219 return meta.getCover();
220 } catch (IOException e) {
221 Instance.syserr(e);
222 }
223 }
224
225 return null;
226 }
227
08fe2e33
NR
228 /**
229 * Retrieve a specific {@link Story}.
230 *
231 * @param luid
232 * the Library UID of the story
92fb0719
NR
233 * @param pg
234 * the optional progress reporter
08fe2e33 235 *
3d247bc3 236 * @return the corresponding {@link Story} or NULL if not found
08fe2e33 237 */
10d558d2 238 public synchronized Story getStory(String luid, Progress pg) {
08fe2e33 239 if (luid != null) {
4f661b2b 240 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
08fe2e33
NR
241 if (luid.equals(entry.getKey().getLuid())) {
242 try {
fe999aa4
NR
243 SupportType type = SupportType.valueOfAllOkUC(entry
244 .getKey().getType());
245 URL url = entry.getValue().toURI().toURL();
246 if (type != null) {
92fb0719
NR
247 return BasicSupport.getSupport(type).process(url,
248 pg);
fe999aa4
NR
249 } else {
250 throw new IOException("Unknown type: "
251 + entry.getKey().getType());
252 }
08fe2e33
NR
253 } catch (IOException e) {
254 // We should not have not-supported files in the
255 // library
256 Instance.syserr(new IOException(
257 "Cannot load file from library: "
258 + entry.getValue().getPath(), e));
259 }
260 }
261 }
262 }
263
92fb0719
NR
264 if (pg != null) {
265 pg.setMinMax(0, 1);
266 pg.setProgress(1);
267 }
268
08fe2e33
NR
269 return null;
270 }
271
272 /**
273 * Import the {@link Story} at the given {@link URL} into the
274 * {@link Library}.
275 *
276 * @param url
277 * the {@link URL} to import
92fb0719
NR
278 * @param pg
279 * the optional progress reporter
08fe2e33
NR
280 *
281 * @return the imported {@link Story}
282 *
283 * @throws IOException
284 * in case of I/O error
285 */
92fb0719 286 public Story imprt(URL url, Progress pg) throws IOException {
08fe2e33
NR
287 BasicSupport support = BasicSupport.getSupport(url);
288 if (support == null) {
289 throw new IOException("URL not supported: " + url.toString());
290 }
291
92fb0719 292 return save(support.process(url, pg), null);
08fe2e33
NR
293 }
294
295 /**
296 * Export the {@link Story} to the given target in the given format.
297 *
298 * @param luid
299 * the {@link Story} ID
300 * @param type
301 * the {@link OutputType} to transform it to
302 * @param target
303 * the target to save to
92fb0719
NR
304 * @param pg
305 * the optional progress reporter
08fe2e33
NR
306 *
307 * @return the saved resource (the main saved {@link File})
308 *
309 * @throws IOException
310 * in case of I/O error
311 */
92fb0719 312 public File export(String luid, OutputType type, String target, Progress pg)
08fe2e33 313 throws IOException {
bee7dffe
NR
314 Progress pgGetStory = new Progress();
315 Progress pgOut = new Progress();
316 if (pg != null) {
317 pg.setMax(2);
318 pg.addProgress(pgGetStory, 1);
319 pg.addProgress(pgOut, 1);
320 }
321
08fe2e33
NR
322 BasicOutput out = BasicOutput.getOutput(type, true);
323 if (out == null) {
324 throw new IOException("Output type not supported: " + type);
325 }
326
bee7dffe 327 Story story = getStory(luid, pgGetStory);
73ce17ef
NR
328 if (story == null) {
329 throw new IOException("Cannot find story to export: " + luid);
330 }
331
bee7dffe 332 return out.process(story, target, pgOut);
08fe2e33
NR
333 }
334
335 /**
2206ef66
NR
336 * Save a {@link Story} to the {@link Library}.
337 *
338 * @param story
339 * the {@link Story} to save
bee7dffe
NR
340 * @param pg
341 * the optional progress reporter
2206ef66
NR
342 *
343 * @return the same {@link Story}, whose LUID may have changed
344 *
345 * @throws IOException
346 * in case of I/O error
347 */
bee7dffe
NR
348 public Story save(Story story, Progress pg) throws IOException {
349 return save(story, null, pg);
2206ef66
NR
350 }
351
352 /**
353 * Save a {@link Story} to the {@link Library} -- the LUID <b>must</b> be
354 * correct, or NULL to get the next free one.
08fe2e33
NR
355 *
356 * @param story
357 * the {@link Story} to save
2206ef66
NR
358 * @param luid
359 * the <b>correct</b> LUID or NULL to get the next free one
bee7dffe
NR
360 * @param pg
361 * the optional progress reporter
2206ef66
NR
362 *
363 * @return the same {@link Story}, whose LUID may have changed
08fe2e33
NR
364 *
365 * @throws IOException
366 * in case of I/O error
367 */
10d558d2
NR
368 public synchronized Story save(Story story, String luid, Progress pg)
369 throws IOException {
a7d266e6 370 // Do not change the original metadata, but change the original story
301791d3 371 MetaData key = story.getMeta().clone();
a7d266e6 372 story.setMeta(key);
08fe2e33 373
2206ef66 374 if (luid == null || luid.isEmpty()) {
4f661b2b 375 getStories(null); // refresh lastId if needed
2206ef66
NR
376 key.setLuid(String.format("%03d", (++lastId)));
377 } else {
378 key.setLuid(luid);
379 }
380
70c9b112
NR
381 getDir(key.getSource()).mkdirs();
382 if (!getDir(key.getSource()).exists()) {
08fe2e33
NR
383 throw new IOException("Cannot create library dir");
384 }
385
386 OutputType out;
08fe2e33 387 if (key != null && key.isImageDocument()) {
2206ef66 388 out = image;
08fe2e33 389 } else {
2206ef66 390 out = text;
08fe2e33 391 }
2206ef66 392
08fe2e33 393 BasicOutput it = BasicOutput.getOutput(out, true);
10d558d2
NR
394 it.process(story, getFile(key).getPath(), pg);
395
396 // empty cache
397 stories.clear();
2206ef66
NR
398
399 return story;
08fe2e33
NR
400 }
401
10d558d2
NR
402 /**
403 * Delete the given {@link Story} from this {@link Library}.
404 *
405 * @param luid
406 * the LUID of the target {@link Story}
407 *
408 * @return TRUE if it was deleted
409 */
410 public synchronized boolean delete(String luid) {
411 boolean ok = false;
412
70c9b112
NR
413 List<File> files = getFiles(luid);
414 if (!files.isEmpty()) {
415 for (File file : files) {
416 IOUtils.deltree(file);
417 }
418
419 ok = true;
420
421 // clear cache
422 stories.clear();
423 }
424
425 return ok;
426 }
427
428 /**
429 * Change the type (source) of the given {@link Story}.
430 *
431 * @param luid
432 * the {@link Story} LUID
433 * @param newSourcethe
434 * new source
435 *
436 * @return TRUE if the {@link Story} was found
437 */
438 public synchronized boolean changeType(String luid, String newType) {
439 MetaData meta = getInfo(luid);
440 if (meta != null) {
441 meta.setSource(newType);
442 File newDir = getDir(meta.getSource());
443 if (!newDir.exists()) {
444 newDir.mkdir();
445 }
446
447 List<File> files = getFiles(luid);
448 for (File file : files) {
449 if (file.getName().endsWith(".info")) {
450 try {
451 String name = file.getName().replaceFirst("\\.info$",
452 "");
453 InfoCover.writeInfo(newDir, name, meta);
454 file.delete();
455 } catch (IOException e) {
456 Instance.syserr(e);
457 }
458 } else {
459 file.renameTo(new File(newDir, file.getName()));
460 }
461 }
462
463 // clear cache
464 stories.clear();
465
466 return true;
467 }
468
469 return false;
470 }
471
472 /**
473 * Return the list of files/dirs on disk for this {@link Story}.
474 * <p>
475 * If the {@link Story} is not found, and empty list is returned.
476 *
477 * @param luid
478 * the {@link Story} LUID
479 *
480 * @return the list of {@link File}s
481 */
482 private List<File> getFiles(String luid) {
483 List<File> files = new ArrayList<File>();
484
10d558d2 485 MetaData meta = getInfo(luid);
4f661b2b 486 File file = getStories(null).get(meta);
10d558d2
NR
487
488 if (file != null) {
70c9b112 489 files.add(file);
9843a5e5 490
70c9b112
NR
491 String readerExt = getOutputType(meta).getDefaultExtension(true);
492 String fileExt = getOutputType(meta).getDefaultExtension(false);
493
494 String path = file.getAbsolutePath();
495 if (readerExt != null && !readerExt.equals(fileExt)) {
496 path = path.substring(0, path.length() - readerExt.length())
497 + fileExt;
498 file = new File(path);
499
500 if (file.exists()) {
501 files.add(file);
10d558d2 502 }
70c9b112 503 }
10d558d2 504
70c9b112
NR
505 File infoFile = new File(path + ".info");
506 if (!infoFile.exists()) {
507 infoFile = new File(path.substring(0,
508 path.length() - fileExt.length())
509 + ".info");
10d558d2
NR
510 }
511
70c9b112
NR
512 if (infoFile.exists()) {
513 files.add(infoFile);
514 }
515
516 String coverExt = "."
517 + Instance.getConfig().getString(Config.IMAGE_FORMAT_COVER);
518 File coverFile = new File(path + coverExt);
519 if (!coverFile.exists()) {
520 coverFile = new File(path.substring(0,
521 path.length() - fileExt.length())
522 + coverExt);
523 }
524
525 if (coverFile.exists()) {
526 files.add(coverFile);
527 }
10d558d2
NR
528 }
529
70c9b112 530 return files;
10d558d2
NR
531 }
532
08fe2e33
NR
533 /**
534 * The directory (full path) where the {@link Story} related to this
535 * {@link MetaData} should be located on disk.
536 *
70c9b112
NR
537 * @param type
538 * the type (source)
08fe2e33
NR
539 *
540 * @return the target directory
541 */
70c9b112
NR
542 private File getDir(String type) {
543 String source = type.replaceAll("[^a-zA-Z0-9._+-]", "_");
08fe2e33
NR
544 return new File(baseDir, source);
545 }
546
547 /**
548 * The target (full path) where the {@link Story} related to this
549 * {@link MetaData} should be located on disk.
550 *
551 * @param key
552 * the {@link Story} {@link MetaData}
553 *
554 * @return the target
555 */
556 private File getFile(MetaData key) {
a4143cd7
NR
557 String title = key.getTitle();
558 if (title == null) {
559 title = "";
560 }
561 title = title.replaceAll("[^a-zA-Z0-9._+-]", "_");
70c9b112 562 return new File(getDir(key.getSource()), key.getLuid() + "_" + title);
08fe2e33
NR
563 }
564
565 /**
566 * Return all the known stories in this {@link Library} object.
567 *
4f661b2b
NR
568 * @param pg
569 * the optional progress reporter
570 *
08fe2e33
NR
571 * @return the stories
572 */
4f661b2b
NR
573 private synchronized Map<MetaData, File> getStories(Progress pg) {
574 if (pg == null) {
575 pg = new Progress();
576 } else {
577 pg.setMinMax(0, 100);
578 }
579
08fe2e33
NR
580 if (stories.isEmpty()) {
581 lastId = 0;
68686a37 582
4f661b2b
NR
583 File[] dirs = baseDir.listFiles(new FileFilter() {
584 public boolean accept(File file) {
585 return file != null && file.isDirectory();
586 }
587 });
588
589 Progress pgDirs = new Progress(0, 100 * dirs.length);
590 pg.addProgress(pgDirs, 100);
591
4f661b2b
NR
592 for (File dir : dirs) {
593 File[] files = dir.listFiles(new FileFilter() {
594 public boolean accept(File file) {
595 return file != null
57f02339
NR
596 && file.getPath().toLowerCase()
597 .endsWith(".info");
4f661b2b
NR
598 }
599 });
600
601 Progress pgFiles = new Progress(0, files.length);
602 pgDirs.addProgress(pgFiles, 100);
603 pgDirs.setName("Loading from: " + dir.getName());
604
605 for (File file : files) {
57f02339 606 pgFiles.setName(file.getName());
4f661b2b 607 try {
57f02339 608 Entry<MetaData, File> entry = readMeta(file, false);
08fe2e33 609 try {
57f02339 610 int id = Integer.parseInt(entry.getKey().getLuid());
4f661b2b
NR
611 if (id > lastId) {
612 lastId = id;
08fe2e33 613 }
4f661b2b 614
57f02339 615 stories.put(entry.getKey(), entry.getValue());
4f661b2b
NR
616 } catch (Exception e) {
617 // not normal!!
57f02339 618 throw new IOException(
4f661b2b
NR
619 "Cannot understand the LUID of "
620 + file.getPath() + ": "
57f02339 621 + entry.getKey().getLuid(), e);
08fe2e33 622 }
4f661b2b
NR
623 } catch (IOException e) {
624 // We should not have not-supported files in the
625 // library
626 Instance.syserr(new IOException(
627 "Cannot load file from library: "
628 + file.getPath(), e));
08fe2e33 629 }
57f02339 630 pgFiles.add(1);
08fe2e33 631 }
4f661b2b
NR
632
633 pgFiles.setName(null);
08fe2e33 634 }
4f661b2b
NR
635
636 pgDirs.setName("Loading directories");
08fe2e33
NR
637 }
638
639 return stories;
640 }
2206ef66 641
57f02339
NR
642 private Entry<MetaData, File> readMeta(File infoFile, boolean withCover)
643 throws IOException {
644
645 final MetaData meta = InfoReader.readMeta(infoFile, withCover);
646
647 // Replace .info with whatever is needed:
648 String path = infoFile.getPath();
649 path = path.substring(0, path.length() - ".info".length());
650
651 String newExt = getOutputType(meta).getDefaultExtension(true);
652
653 File targetFile = new File(path + newExt);
654
655 final File ffile = targetFile;
656 return new Entry<MetaData, File>() {
657 public File setValue(File value) {
658 return null;
659 }
660
661 public File getValue() {
662 return ffile;
663 }
664
665 public MetaData getKey() {
666 return meta;
667 }
668 };
669 }
670
2206ef66
NR
671 /**
672 * Return the {@link OutputType} for this {@link Story}.
673 *
674 * @param meta
675 * the {@link Story} {@link MetaData}
676 *
677 * @return the type
678 */
679 private OutputType getOutputType(MetaData meta) {
680 if (meta != null && meta.isImageDocument()) {
681 return image;
682 } else {
683 return text;
684 }
685 }
08fe2e33 686}