Invalidate info for one luid instead of all
[fanfix.git] / src / be / nikiroo / fanfix / library / BasicLibrary.java
1 package be.nikiroo.fanfix.library;
2
3 import java.awt.image.BufferedImage;
4 import java.io.File;
5 import java.io.IOException;
6 import java.net.URL;
7 import java.net.UnknownHostException;
8 import java.util.ArrayList;
9 import java.util.Collections;
10 import java.util.List;
11
12 import be.nikiroo.fanfix.Instance;
13 import be.nikiroo.fanfix.data.MetaData;
14 import be.nikiroo.fanfix.data.Story;
15 import be.nikiroo.fanfix.output.BasicOutput;
16 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
17 import be.nikiroo.fanfix.supported.BasicSupport;
18 import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
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 BufferedImage getCover(String luid);
93
94 /**
95 * Return the cover image associated to this source.
96 * <p>
97 * By default, return the cover of the first story with this source.
98 *
99 * @param source
100 * the source
101 *
102 * @return the cover image or NULL
103 */
104 public BufferedImage getSourceCover(String source) {
105 List<MetaData> metas = getListBySource(source);
106 if (metas.size() > 0) {
107 return getCover(metas.get(0).getLuid());
108 }
109
110 return null;
111 }
112
113 /**
114 * Fix the source cover to the given story cover.
115 *
116 * @param source
117 * the source to change
118 * @param luid
119 * the story LUID
120 */
121 public abstract void setSourceCover(String source, String luid);
122
123 /**
124 * Return the list of stories (represented by their {@link MetaData}, which
125 * <b>MAY</b> not have the cover included).
126 *
127 * @param pg
128 * the optional {@link Progress}
129 *
130 * @return the list (can be empty but not NULL)
131 */
132 protected abstract List<MetaData> getMetas(Progress pg);
133
134 /**
135 * Invalidate the {@link Story} cache (when the content should be re-read
136 * because it was changed).
137 */
138 protected void invalidateInfo() {
139 invalidateInfo(null);
140 }
141
142 /**
143 * Invalidate the {@link Story} cache (when the content should be re-read
144 * because it was changed).
145 *
146 * @param luid
147 * the luid of the {@link Story} to clear from the cache, or NULL
148 * for all stories
149 */
150 protected abstract void invalidateInfo(String luid);
151
152 /**
153 * Return the next LUID that can be used.
154 *
155 * @return the next luid
156 */
157 protected abstract int getNextId();
158
159 /**
160 * Delete the target {@link Story}.
161 *
162 * @param luid
163 * the LUID of the {@link Story}
164 *
165 * @throws IOException
166 * in case of I/O error or if the {@link Story} wa not found
167 */
168 protected abstract void doDelete(String luid) throws IOException;
169
170 /**
171 * Actually save the story to the back-end.
172 *
173 * @param story
174 * the {@link Story} to save
175 * @param pg
176 * the optional {@link Progress}
177 *
178 * @return the saved {@link Story} (which may have changed, especially
179 * regarding the {@link MetaData})
180 *
181 * @throws IOException
182 * in case of I/O error
183 */
184 protected abstract Story doSave(Story story, Progress pg)
185 throws IOException;
186
187 /**
188 * Refresh the {@link BasicLibrary}, that is, make sure all metas are
189 * loaded.
190 *
191 * @param pg
192 * the optional progress reporter
193 */
194 public void refresh(Progress pg) {
195 getMetas(pg);
196 }
197
198 /**
199 * List all the known types (sources) of stories.
200 *
201 * @return the sources
202 */
203 public synchronized List<String> getSources() {
204 List<String> list = new ArrayList<String>();
205 for (MetaData meta : getMetas(null)) {
206 String storySource = meta.getSource();
207 if (!list.contains(storySource)) {
208 list.add(storySource);
209 }
210 }
211
212 Collections.sort(list);
213 return list;
214 }
215
216 /**
217 * List all the known authors of stories.
218 *
219 * @return the authors
220 */
221 public synchronized List<String> getAuthors() {
222 List<String> list = new ArrayList<String>();
223 for (MetaData meta : getMetas(null)) {
224 String storyAuthor = meta.getAuthor();
225 if (!list.contains(storyAuthor)) {
226 list.add(storyAuthor);
227 }
228 }
229
230 Collections.sort(list);
231 return list;
232 }
233
234 /**
235 * List all the stories in the {@link BasicLibrary}.
236 * <p>
237 * Cover images not included.
238 *
239 * @return the stories
240 */
241 public synchronized List<MetaData> getList() {
242 return getMetas(null);
243 }
244
245 /**
246 * List all the stories of the given source type in the {@link BasicLibrary}
247 * , or all the stories if NULL is passed as a type.
248 * <p>
249 * Cover images not included.
250 *
251 * @param type
252 * the type of story to retrieve, or NULL for all
253 *
254 * @return the stories
255 */
256 public synchronized List<MetaData> getListBySource(String type) {
257 List<MetaData> list = new ArrayList<MetaData>();
258 for (MetaData meta : getMetas(null)) {
259 String storyType = meta.getSource();
260 if (type == null || type.equalsIgnoreCase(storyType)) {
261 list.add(meta);
262 }
263 }
264
265 Collections.sort(list);
266 return list;
267 }
268
269 /**
270 * List all the stories of the given author in the {@link BasicLibrary}, or
271 * all the stories if NULL is passed as an author.
272 * <p>
273 * Cover images not included.
274 *
275 * @param author
276 * the author of the stories to retrieve, or NULL for all
277 *
278 * @return the stories
279 */
280 public synchronized List<MetaData> getListByAuthor(String author) {
281 List<MetaData> list = new ArrayList<MetaData>();
282 for (MetaData meta : getMetas(null)) {
283 String storyAuthor = meta.getAuthor();
284 if (author == null || author.equalsIgnoreCase(storyAuthor)) {
285 list.add(meta);
286 }
287 }
288
289 Collections.sort(list);
290 return list;
291 }
292
293 /**
294 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
295 * cover image <b>MAY</b> not be included.
296 *
297 * @param luid
298 * the Library UID of the story
299 *
300 * @return the corresponding {@link Story}
301 */
302 public synchronized MetaData getInfo(String luid) {
303 if (luid != null) {
304 for (MetaData meta : getMetas(null)) {
305 if (luid.equals(meta.getLuid())) {
306 return meta;
307 }
308 }
309 }
310
311 return null;
312 }
313
314 /**
315 * Retrieve a specific {@link Story}.
316 *
317 * @param luid
318 * the Library UID of the story
319 * @param pg
320 * the optional progress reporter
321 *
322 * @return the corresponding {@link Story} or NULL if not found
323 */
324 public synchronized Story getStory(String luid, Progress pg) {
325 if (pg == null) {
326 pg = new Progress();
327 }
328
329 Progress pgGet = new Progress();
330 Progress pgProcess = new Progress();
331
332 pg.setMinMax(0, 2);
333 pg.addProgress(pgGet, 1);
334 pg.addProgress(pgProcess, 1);
335
336 Story story = null;
337 for (MetaData meta : getMetas(null)) {
338 if (meta.getLuid().equals(luid)) {
339 File file = getFile(luid, pgGet);
340 pgGet.done();
341 try {
342 SupportType type = SupportType.valueOfAllOkUC(meta
343 .getType());
344 URL url = file.toURI().toURL();
345 if (type != null) {
346 story = BasicSupport.getSupport(type).process(url,
347 pgProcess);
348 // Because we do not want to clear the meta cache:
349 meta.setCover(story.getMeta().getCover());
350 story.setMeta(meta);
351 //
352 } else {
353 throw new IOException("Unknown type: " + meta.getType());
354 }
355 } catch (IOException e) {
356 // We should not have not-supported files in the
357 // library
358 Instance.getTraceHandler().error(
359 new IOException("Cannot load file from library: "
360 + file, e));
361 } finally {
362 pgProcess.done();
363 pg.done();
364 }
365
366 break;
367 }
368 }
369
370 return story;
371 }
372
373 /**
374 * Import the {@link Story} at the given {@link URL} into the
375 * {@link BasicLibrary}.
376 *
377 * @param url
378 * the {@link URL} to import
379 * @param pg
380 * the optional progress reporter
381 *
382 * @return the imported {@link Story}
383 *
384 * @throws UnknownHostException
385 * if the host is not supported
386 * @throws IOException
387 * in case of I/O error
388 */
389 public Story imprt(URL url, Progress pg) throws IOException {
390 BasicSupport support = BasicSupport.getSupport(url);
391 if (support == null) {
392 throw new UnknownHostException("" + url);
393 }
394
395 return save(support.process(url, pg), null);
396 }
397
398 /**
399 * Import the story from one library to another, and keep the same LUID.
400 *
401 * @param other
402 * the other library to import from
403 * @param luid
404 * the Library UID
405 * @param pg
406 * the optional progress reporter
407 *
408 * @throws IOException
409 * in case of I/O error
410 */
411 public void imprt(BasicLibrary other, String luid, Progress pg)
412 throws IOException {
413 Progress pgGetStory = new Progress();
414 Progress pgSave = new Progress();
415 if (pg == null) {
416 pg = new Progress();
417 }
418
419 pg.setMinMax(0, 2);
420 pg.addProgress(pgGetStory, 1);
421 pg.addProgress(pgSave, 1);
422
423 Story story = other.getStory(luid, pgGetStory);
424 if (story != null) {
425 story = this.save(story, luid, pgSave);
426 pg.done();
427 } else {
428 pg.done();
429 throw new IOException("Cannot find story in Library: " + luid);
430 }
431 }
432
433 /**
434 * Export the {@link Story} to the given target in the given format.
435 *
436 * @param luid
437 * the {@link Story} ID
438 * @param type
439 * the {@link OutputType} to transform it to
440 * @param target
441 * the target to save to
442 * @param pg
443 * the optional progress reporter
444 *
445 * @return the saved resource (the main saved {@link File})
446 *
447 * @throws IOException
448 * in case of I/O error
449 */
450 public File export(String luid, OutputType type, String target, Progress pg)
451 throws IOException {
452 Progress pgGetStory = new Progress();
453 Progress pgOut = new Progress();
454 if (pg != null) {
455 pg.setMax(2);
456 pg.addProgress(pgGetStory, 1);
457 pg.addProgress(pgOut, 1);
458 }
459
460 BasicOutput out = BasicOutput.getOutput(type, false, false);
461 if (out == null) {
462 throw new IOException("Output type not supported: " + type);
463 }
464
465 Story story = getStory(luid, pgGetStory);
466 if (story == null) {
467 throw new IOException("Cannot find story to export: " + luid);
468 }
469
470 return out.process(story, target, pgOut);
471 }
472
473 /**
474 * Save a {@link Story} to the {@link BasicLibrary}.
475 *
476 * @param story
477 * the {@link Story} to save
478 * @param pg
479 * the optional progress reporter
480 *
481 * @return the same {@link Story}, whose LUID may have changed
482 *
483 * @throws IOException
484 * in case of I/O error
485 */
486 public Story save(Story story, Progress pg) throws IOException {
487 return save(story, null, pg);
488 }
489
490 /**
491 * Save a {@link Story} to the {@link BasicLibrary} -- the LUID <b>must</b>
492 * be correct, or NULL to get the next free one.
493 * <p>
494 * Will override any previous {@link Story} with the same LUID.
495 *
496 * @param story
497 * the {@link Story} to save
498 * @param luid
499 * the <b>correct</b> LUID or NULL to get the next free one
500 * @param pg
501 * the optional progress reporter
502 *
503 * @return the same {@link Story}, whose LUID may have changed
504 *
505 * @throws IOException
506 * in case of I/O error
507 */
508 public synchronized Story save(Story story, String luid, Progress pg)
509 throws IOException {
510 // Do not change the original metadata, but change the original story
511 MetaData meta = story.getMeta().clone();
512 story.setMeta(meta);
513
514 if (luid == null || luid.isEmpty()) {
515 meta.setLuid(String.format("%03d", getNextId()));
516 } else {
517 meta.setLuid(luid);
518 }
519
520 if (luid != null && getInfo(luid) != null) {
521 delete(luid);
522 }
523
524 doSave(story, pg);
525
526 invalidateInfo(luid);
527
528 return story;
529 }
530
531 /**
532 * Delete the given {@link Story} from this {@link BasicLibrary}.
533 *
534 * @param luid
535 * the LUID of the target {@link Story}
536 *
537 * @throws IOException
538 * in case of I/O error
539 */
540 public synchronized void delete(String luid) throws IOException {
541 doDelete(luid);
542 invalidateInfo(luid);
543 }
544
545 /**
546 * Change the type (source) of the given {@link Story}.
547 *
548 * @param luid
549 * the {@link Story} LUID
550 * @param newSource
551 * the new source
552 * @param pg
553 * the optional progress reporter
554 *
555 * @throws IOException
556 * in case of I/O error or if the {@link Story} was not found
557 */
558 public synchronized void changeSource(String luid, String newSource,
559 Progress pg) throws IOException {
560 MetaData meta = getInfo(luid);
561 if (meta == null) {
562 throw new IOException("Story not found: " + luid);
563 }
564
565 meta.setSource(newSource);
566 saveMeta(meta, pg);
567 }
568
569 /**
570 * Save back the current state of the {@link MetaData} (LUID <b>MUST NOT</b>
571 * change) for this {@link Story}.
572 * <p>
573 * By default, delete the old {@link Story} then recreate a new
574 * {@link Story}.
575 * <p>
576 * Note that this behaviour can lead to data loss.
577 *
578 * @param meta
579 * the new {@link MetaData} (LUID <b>MUST NOT</b> change)
580 * @param pg
581 * the optional {@link Progress}
582 *
583 * @throws IOException
584 * in case of I/O error or if the {@link Story} was not found
585 */
586 protected synchronized void saveMeta(MetaData meta, Progress pg)
587 throws IOException {
588 if (pg == null) {
589 pg = new Progress();
590 }
591
592 Progress pgGet = new Progress();
593 Progress pgSet = new Progress();
594 pg.addProgress(pgGet, 50);
595 pg.addProgress(pgSet, 50);
596
597 Story story = getStory(meta.getLuid(), pgGet);
598 if (story == null) {
599 throw new IOException("Story not found: " + meta.getLuid());
600 }
601
602 delete(meta.getLuid());
603
604 story.setMeta(meta);
605 save(story, meta.getLuid(), pgSet);
606
607 pg.done();
608 }
609 }