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