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