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