fix no desc bug
[fanfix.git] / src / be / nikiroo / fanfix / library / BasicLibrary.java
1 package be.nikiroo.fanfix.library;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.net.URL;
6 import java.net.UnknownHostException;
7 import java.util.ArrayList;
8 import java.util.Collections;
9 import java.util.List;
10
11 import be.nikiroo.fanfix.Instance;
12 import be.nikiroo.fanfix.data.MetaData;
13 import be.nikiroo.fanfix.data.Story;
14 import be.nikiroo.fanfix.output.BasicOutput;
15 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
16 import be.nikiroo.fanfix.supported.BasicSupport;
17 import be.nikiroo.fanfix.supported.SupportType;
18 import be.nikiroo.utils.Image;
19 import be.nikiroo.utils.Progress;
20
21 /**
22 * Manage a library of Stories: import, export, list, modify.
23 * <p>
24 * Each {@link Story} object will be associated with a (local to the library)
25 * unique ID, the LUID, which will be used to identify the {@link Story}.
26 * <p>
27 * Most of the {@link BasicLibrary} functions work on a partial (cover
28 * <b>MAY</b> not be included) {@link MetaData} object.
29 *
30 * @author niki
31 */
32 abstract public class BasicLibrary {
33 /**
34 * A {@link BasicLibrary} status.
35 *
36 * @author niki
37 */
38 public enum Status {
39 /** The library is ready. */
40 READY,
41 /** The library is invalid (not correctly set up). */
42 INVALID,
43 /** You are not allowed to access this library. */
44 UNAUTORIZED,
45 /** The library is currently out of commission. */
46 UNAVAILABLE,
47 }
48
49 /**
50 * Return a name for this library (the UI may display this).
51 * <p>
52 * Must not be NULL.
53 *
54 * @return the name, or an empty {@link String} if none
55 */
56 public String getLibraryName() {
57 return "";
58 }
59
60 /**
61 * The library status.
62 *
63 * @return the current status
64 */
65 public Status getStatus() {
66 return Status.READY;
67 }
68
69 /**
70 * Retrieve the main {@link File} corresponding to the given {@link Story},
71 * which can be passed to an external reader or instance.
72 * <p>
73 * Do <b>NOT</b> alter this file.
74 *
75 * @param luid
76 * the Library UID of the story
77 * @param pg
78 * the optional {@link Progress}
79 *
80 * @return the corresponding {@link Story}
81 */
82 public abstract File getFile(String luid, Progress pg);
83
84 /**
85 * Return the cover image associated to this story.
86 *
87 * @param luid
88 * the Library UID of the story
89 *
90 * @return the cover image
91 */
92 public abstract Image getCover(String luid);
93
94 /**
95 * Return the cover image associated to this source.
96 * <p>
97 * By default, return the custom cover if any, and if not, return the cover
98 * of the first story with this source.
99 *
100 * @param source
101 * the source
102 *
103 * @return the cover image or NULL
104 */
105 public Image getSourceCover(String source) {
106 Image custom = getCustomSourceCover(source);
107 if (custom != null) {
108 return custom;
109 }
110
111 List<MetaData> metas = getListBySource(source);
112 if (metas.size() > 0) {
113 return getCover(metas.get(0).getLuid());
114 }
115
116 return null;
117 }
118
119 /**
120 * Return the custom cover image associated to this source.
121 * <p>
122 * By default, return NULL.
123 *
124 * @param source
125 * the source to look for
126 *
127 * @return the custom cover or NULL if none
128 */
129 public Image getCustomSourceCover(@SuppressWarnings("unused") String source) {
130 return null;
131 }
132
133 /**
134 * Fix the source cover to the given story cover.
135 *
136 * @param source
137 * the source to change
138 * @param luid
139 * the story LUID
140 */
141 public abstract void setSourceCover(String source, String luid);
142
143 /**
144 * Return the list of stories (represented by their {@link MetaData}, which
145 * <b>MAY</b> not have the cover included).
146 *
147 * @param pg
148 * the optional {@link Progress}
149 *
150 * @return the list (can be empty but not NULL)
151 */
152 protected abstract List<MetaData> getMetas(Progress pg);
153
154 /**
155 * Invalidate the {@link Story} cache (when the content should be re-read
156 * because it was changed).
157 */
158 protected void deleteInfo() {
159 deleteInfo(null);
160 }
161
162 /**
163 * Invalidate the {@link Story} cache (when the content is removed).
164 * <p>
165 * All the cache can be deleted if NULL is passed as meta.
166 *
167 * @param luid
168 * the LUID of the {@link Story} to clear from the cache, or NULL
169 * for all stories
170 */
171 protected abstract void deleteInfo(String luid);
172
173 /**
174 * Invalidate the {@link Story} cache (when the content has changed, but we
175 * already have it) with the new given meta.
176 *
177 * @param meta
178 * the {@link Story} to clear from the cache
179 */
180 protected abstract void updateInfo(MetaData meta);
181
182 /**
183 * Return the next LUID that can be used.
184 *
185 * @return the next luid
186 */
187 protected abstract int getNextId();
188
189 /**
190 * Delete the target {@link Story}.
191 *
192 * @param luid
193 * the LUID of the {@link Story}
194 *
195 * @throws IOException
196 * in case of I/O error or if the {@link Story} wa not found
197 */
198 protected abstract void doDelete(String luid) throws IOException;
199
200 /**
201 * Actually save the story to the back-end.
202 *
203 * @param story
204 * the {@link Story} to save
205 * @param pg
206 * the optional {@link Progress}
207 *
208 * @return the saved {@link Story} (which may have changed, especially
209 * regarding the {@link MetaData})
210 *
211 * @throws IOException
212 * in case of I/O error
213 */
214 protected abstract Story doSave(Story story, Progress pg)
215 throws IOException;
216
217 /**
218 * Refresh the {@link BasicLibrary}, that is, make sure all metas are
219 * loaded.
220 *
221 * @param pg
222 * the optional progress reporter
223 */
224 public void refresh(Progress pg) {
225 getMetas(pg);
226 }
227
228 /**
229 * List all the known types (sources) of stories.
230 *
231 * @return the sources
232 */
233 public synchronized List<String> getSources() {
234 List<String> list = new ArrayList<String>();
235 for (MetaData meta : getMetas(null)) {
236 String storySource = meta.getSource();
237 if (!list.contains(storySource)) {
238 list.add(storySource);
239 }
240 }
241
242 Collections.sort(list);
243 return list;
244 }
245
246 /**
247 * List all the known authors of stories.
248 *
249 * @return the authors
250 */
251 public synchronized List<String> getAuthors() {
252 List<String> list = new ArrayList<String>();
253 for (MetaData meta : getMetas(null)) {
254 String storyAuthor = meta.getAuthor();
255 if (!list.contains(storyAuthor)) {
256 list.add(storyAuthor);
257 }
258 }
259
260 Collections.sort(list);
261 return list;
262 }
263
264 /**
265 * List all the stories in the {@link BasicLibrary}.
266 * <p>
267 * Cover images not included.
268 *
269 * @return the stories
270 */
271 public synchronized List<MetaData> getList() {
272 return getMetas(null);
273 }
274
275 /**
276 * List all the stories of the given source type in the {@link BasicLibrary}
277 * , or all the stories if NULL is passed as a type.
278 * <p>
279 * Cover images not included.
280 *
281 * @param type
282 * the type of story to retrieve, or NULL for all
283 *
284 * @return the stories
285 */
286 public synchronized List<MetaData> getListBySource(String type) {
287 List<MetaData> list = new ArrayList<MetaData>();
288 for (MetaData meta : getMetas(null)) {
289 String storyType = meta.getSource();
290 if (type == null || type.equalsIgnoreCase(storyType)) {
291 list.add(meta);
292 }
293 }
294
295 Collections.sort(list);
296 return list;
297 }
298
299 /**
300 * List all the stories of the given author in the {@link BasicLibrary}, or
301 * all the stories if NULL is passed as an author.
302 * <p>
303 * Cover images not included.
304 *
305 * @param author
306 * the author of the stories to retrieve, or NULL for all
307 *
308 * @return the stories
309 */
310 public synchronized List<MetaData> getListByAuthor(String author) {
311 List<MetaData> list = new ArrayList<MetaData>();
312 for (MetaData meta : getMetas(null)) {
313 String storyAuthor = meta.getAuthor();
314 if (author == null || author.equalsIgnoreCase(storyAuthor)) {
315 list.add(meta);
316 }
317 }
318
319 Collections.sort(list);
320 return list;
321 }
322
323 /**
324 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
325 * cover image <b>MAY</b> not be included.
326 *
327 * @param luid
328 * the Library UID of the story
329 *
330 * @return the corresponding {@link Story}
331 */
332 public synchronized MetaData getInfo(String luid) {
333 if (luid != null) {
334 for (MetaData meta : getMetas(null)) {
335 if (luid.equals(meta.getLuid())) {
336 return meta;
337 }
338 }
339 }
340
341 return null;
342 }
343
344 /**
345 * Retrieve a specific {@link Story}.
346 *
347 * @param luid
348 * the Library UID of the story
349 * @param pg
350 * the optional progress reporter
351 *
352 * @return the corresponding {@link Story} or NULL if not found
353 */
354 public synchronized Story getStory(String luid, Progress pg) {
355 if (pg == null) {
356 pg = new Progress();
357 }
358
359 Progress pgGet = new Progress();
360 Progress pgProcess = new Progress();
361
362 pg.setMinMax(0, 2);
363 pg.addProgress(pgGet, 1);
364 pg.addProgress(pgProcess, 1);
365
366 Story story = null;
367 for (MetaData meta : getMetas(null)) {
368 if (meta.getLuid().equals(luid)) {
369 File file = getFile(luid, pgGet);
370 pgGet.done();
371 try {
372 SupportType type = SupportType.valueOfAllOkUC(meta
373 .getType());
374 URL url = file.toURI().toURL();
375 if (type != null) {
376 story = BasicSupport.getSupport(type, url) //
377 .process(pgProcess);
378
379 // Because we do not want to clear the meta cache:
380 meta.setCover(story.getMeta().getCover());
381 meta.setResume(story.getMeta().getResume());
382 story.setMeta(meta);
383 //
384 } else {
385 throw new IOException("Unknown type: " + meta.getType());
386 }
387 } catch (IOException e) {
388 // We should not have not-supported files in the
389 // library
390 Instance.getTraceHandler().error(
391 new IOException("Cannot load file from library: "
392 + file, e));
393 } finally {
394 pgProcess.done();
395 pg.done();
396 }
397
398 break;
399 }
400 }
401
402 return story;
403 }
404
405 /**
406 * Import the {@link Story} at the given {@link URL} into the
407 * {@link BasicLibrary}.
408 *
409 * @param url
410 * the {@link URL} to import
411 * @param pg
412 * the optional progress reporter
413 *
414 * @return the imported {@link Story}
415 *
416 * @throws UnknownHostException
417 * if the host is not supported
418 * @throws IOException
419 * in case of I/O error
420 */
421 public Story imprt(URL url, Progress pg) throws IOException {
422 if (pg == null)
423 pg = new Progress();
424
425 pg.setMinMax(0, 1000);
426 Progress pgProcess = new Progress();
427 Progress pgSave = new Progress();
428 pg.addProgress(pgProcess, 800);
429 pg.addProgress(pgSave, 200);
430
431 BasicSupport support = BasicSupport.getSupport(url);
432 if (support == null) {
433 throw new UnknownHostException("" + url);
434 }
435
436 Story story = save(support.process(pgProcess), pgSave);
437 pg.done();
438
439 return story;
440 }
441
442 /**
443 * Import the story from one library to another, and keep the same LUID.
444 *
445 * @param other
446 * the other library to import from
447 * @param luid
448 * the Library UID
449 * @param pg
450 * the optional progress reporter
451 *
452 * @throws IOException
453 * in case of I/O error
454 */
455 public void imprt(BasicLibrary other, String luid, Progress pg)
456 throws IOException {
457 Progress pgGetStory = new Progress();
458 Progress pgSave = new Progress();
459 if (pg == null) {
460 pg = new Progress();
461 }
462
463 pg.setMinMax(0, 2);
464 pg.addProgress(pgGetStory, 1);
465 pg.addProgress(pgSave, 1);
466
467 Story story = other.getStory(luid, pgGetStory);
468 if (story != null) {
469 story = this.save(story, luid, pgSave);
470 pg.done();
471 } else {
472 pg.done();
473 throw new IOException("Cannot find story in Library: " + luid);
474 }
475 }
476
477 /**
478 * Export the {@link Story} to the given target in the given format.
479 *
480 * @param luid
481 * the {@link Story} ID
482 * @param type
483 * the {@link OutputType} to transform it to
484 * @param target
485 * the target to save to
486 * @param pg
487 * the optional progress reporter
488 *
489 * @return the saved resource (the main saved {@link File})
490 *
491 * @throws IOException
492 * in case of I/O error
493 */
494 public File export(String luid, OutputType type, String target, Progress pg)
495 throws IOException {
496 Progress pgGetStory = new Progress();
497 Progress pgOut = new Progress();
498 if (pg != null) {
499 pg.setMax(2);
500 pg.addProgress(pgGetStory, 1);
501 pg.addProgress(pgOut, 1);
502 }
503
504 BasicOutput out = BasicOutput.getOutput(type, false, false);
505 if (out == null) {
506 throw new IOException("Output type not supported: " + type);
507 }
508
509 Story story = getStory(luid, pgGetStory);
510 if (story == null) {
511 throw new IOException("Cannot find story to export: " + luid);
512 }
513
514 return out.process(story, target, pgOut);
515 }
516
517 /**
518 * Save a {@link Story} to the {@link BasicLibrary}.
519 *
520 * @param story
521 * the {@link Story} to save
522 * @param pg
523 * the optional progress reporter
524 *
525 * @return the same {@link Story}, whose LUID may have changed
526 *
527 * @throws IOException
528 * in case of I/O error
529 */
530 public Story save(Story story, Progress pg) throws IOException {
531 return save(story, null, pg);
532 }
533
534 /**
535 * Save a {@link Story} to the {@link BasicLibrary} -- the LUID <b>must</b>
536 * be correct, or NULL to get the next free one.
537 * <p>
538 * Will override any previous {@link Story} with the same LUID.
539 *
540 * @param story
541 * the {@link Story} to save
542 * @param luid
543 * the <b>correct</b> LUID or NULL to get the next free one
544 * @param pg
545 * the optional progress reporter
546 *
547 * @return the same {@link Story}, whose LUID may have changed
548 *
549 * @throws IOException
550 * in case of I/O error
551 */
552 public synchronized Story save(Story story, String luid, Progress pg)
553 throws IOException {
554
555 Instance.getTraceHandler().trace(
556 this.getClass().getSimpleName() + ": saving story " + luid);
557
558 // Do not change the original metadata, but change the original story
559 MetaData meta = story.getMeta().clone();
560 story.setMeta(meta);
561
562 if (luid == null || luid.isEmpty()) {
563 meta.setLuid(String.format("%03d", getNextId()));
564 } else {
565 meta.setLuid(luid);
566 }
567
568 if (luid != null && getInfo(luid) != null) {
569 delete(luid);
570 }
571
572 story = doSave(story, pg);
573
574 updateInfo(story.getMeta());
575
576 Instance.getTraceHandler().trace(
577 this.getClass().getSimpleName() + ": story saved (" + luid
578 + ")");
579
580 return story;
581 }
582
583 /**
584 * Delete the given {@link Story} from this {@link BasicLibrary}.
585 *
586 * @param luid
587 * the LUID of the target {@link Story}
588 *
589 * @throws IOException
590 * in case of I/O error
591 */
592 public synchronized void delete(String luid) throws IOException {
593 Instance.getTraceHandler().trace(
594 this.getClass().getSimpleName() + ": deleting story " + luid);
595
596 doDelete(luid);
597 deleteInfo(luid);
598
599 Instance.getTraceHandler().trace(
600 this.getClass().getSimpleName() + ": story deleted (" + luid
601 + ")");
602 }
603
604 /**
605 * Change the type (source) of the given {@link Story}.
606 *
607 * @param luid
608 * the {@link Story} LUID
609 * @param newSource
610 * the new source
611 * @param pg
612 * the optional progress reporter
613 *
614 * @throws IOException
615 * in case of I/O error or if the {@link Story} was not found
616 */
617 public synchronized void changeSource(String luid, String newSource,
618 Progress pg) throws IOException {
619 MetaData meta = getInfo(luid);
620 if (meta == null) {
621 throw new IOException("Story not found: " + luid);
622 }
623
624 meta.setSource(newSource);
625 saveMeta(meta, pg);
626 }
627
628 /**
629 * Save back the current state of the {@link MetaData} (LUID <b>MUST NOT</b>
630 * change) for this {@link Story}.
631 * <p>
632 * By default, delete the old {@link Story} then recreate a new
633 * {@link Story}.
634 * <p>
635 * Note that this behaviour can lead to data loss.
636 *
637 * @param meta
638 * the new {@link MetaData} (LUID <b>MUST NOT</b> change)
639 * @param pg
640 * the optional {@link Progress}
641 *
642 * @throws IOException
643 * in case of I/O error or if the {@link Story} was not found
644 */
645 protected synchronized void saveMeta(MetaData meta, Progress pg)
646 throws IOException {
647 if (pg == null) {
648 pg = new Progress();
649 }
650
651 Progress pgGet = new Progress();
652 Progress pgSet = new Progress();
653 pg.addProgress(pgGet, 50);
654 pg.addProgress(pgSet, 50);
655
656 Story story = getStory(meta.getLuid(), pgGet);
657 if (story == null) {
658 throw new IOException("Story not found: " + meta.getLuid());
659 }
660
661 delete(meta.getLuid());
662
663 story.setMeta(meta);
664 save(story, meta.getLuid(), pgSet);
665
666 pg.done();
667 }
668 }