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