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