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