changelog
[nikiroo-utils.git] / src / be / nikiroo / fanfix / library / BasicLibrary.java
CommitLineData
e42573a0 1package be.nikiroo.fanfix.library;
68e2c6d2 2
68e2c6d2
NR
3import java.io.File;
4import java.io.IOException;
5import java.net.URL;
fd1d31c2 6import java.net.UnknownHostException;
68e2c6d2
NR
7import java.util.ArrayList;
8import java.util.Collections;
9import java.util.List;
c1b93db3 10import java.util.Map;
c1b93db3 11import java.util.TreeMap;
68e2c6d2 12
e42573a0 13import be.nikiroo.fanfix.Instance;
68e2c6d2
NR
14import be.nikiroo.fanfix.data.MetaData;
15import be.nikiroo.fanfix.data.Story;
16import be.nikiroo.fanfix.output.BasicOutput;
17import be.nikiroo.fanfix.output.BasicOutput.OutputType;
18import be.nikiroo.fanfix.supported.BasicSupport;
0ffa4754 19import be.nikiroo.fanfix.supported.SupportType;
16a81ef7 20import be.nikiroo.utils.Image;
68e2c6d2 21import be.nikiroo.utils.Progress;
5f42f329 22import be.nikiroo.utils.StringUtils;
68e2c6d2
NR
23
24/**
25 * Manage a library of Stories: import, export, list, modify.
26 * <p>
27 * Each {@link Story} object will be associated with a (local to the library)
28 * unique ID, the LUID, which will be used to identify the {@link Story}.
29 * <p>
30 * Most of the {@link BasicLibrary} functions work on a partial (cover
31 * <b>MAY</b> not be included) {@link MetaData} object.
32 *
33 * @author niki
34 */
35abstract public class BasicLibrary {
e6249b0f
NR
36 /**
37 * A {@link BasicLibrary} status.
38 *
39 * @author niki
40 */
41 public enum Status {
0bb51c9c
NR
42 /** The library is ready and r/w. */
43 READ_WRITE,
44 /** The library is ready, but read-only. */
45 READ_ONLY,
e6249b0f
NR
46 /** The library is invalid (not correctly set up). */
47 INVALID,
48 /** You are not allowed to access this library. */
99206a39 49 UNAUTHORIZED,
e6249b0f 50 /** The library is currently out of commission. */
0bb51c9c
NR
51 UNAVAILABLE;
52
53 /**
54 * The library is available (you can query it).
55 * <p>
56 * It does <b>not</b> specify if it is read-only or not.
57 *
58 * @return TRUE if it is
59 */
60 public boolean isReady() {
61 return (this == READ_WRITE || this == READ_ONLY);
62 }
63
64 /**
65 * This library can be modified (= you are allowed to modify it).
66 *
67 * @return TRUE if it is
68 */
69 public boolean isWritable() {
70 return (this == READ_WRITE);
71 }
e6249b0f
NR
72 }
73
99ccbdf6
NR
74 /**
75 * Return a name for this library (the UI may display this).
76 * <p>
77 * Must not be NULL.
78 *
79 * @return the name, or an empty {@link String} if none
80 */
81 public String getLibraryName() {
82 return "";
83 }
84
e6249b0f
NR
85 /**
86 * The library status.
87 *
88 * @return the current status
89 */
90 public Status getStatus() {
0bb51c9c 91 return Status.READ_WRITE;
e6249b0f
NR
92 }
93
68e2c6d2
NR
94 /**
95 * Retrieve the main {@link File} corresponding to the given {@link Story},
96 * which can be passed to an external reader or instance.
97 * <p>
98 * Do <b>NOT</b> alter this file.
99 *
100 * @param luid
dc919036 101 * the Library UID of the story, can be NULL
ff05b828
NR
102 * @param pg
103 * the optional {@link Progress}
68e2c6d2
NR
104 *
105 * @return the corresponding {@link Story}
0bb51c9c
NR
106 *
107 * @throws IOException
108 * in case of IOException
68e2c6d2 109 */
0bb51c9c 110 public abstract File getFile(String luid, Progress pg) throws IOException;
68e2c6d2
NR
111
112 /**
113 * Return the cover image associated to this story.
114 *
115 * @param luid
116 * the Library UID of the story
117 *
118 * @return the cover image
0bb51c9c
NR
119 *
120 * @throws IOException
121 * in case of IOException
68e2c6d2 122 */
0bb51c9c 123 public abstract Image getCover(String luid) throws IOException;
68e2c6d2 124
1f2a7d5f 125 // TODO: ensure it is the main used interface
dc919036 126 public MetaResultList getList(Progress pg) throws IOException {
1f2a7d5f
NR
127 return new MetaResultList(getMetas(pg));
128 }
dc919036
NR
129
130 // TODO: make something for (normal and custom) not-story covers
131
14b57448
NR
132 /**
133 * Return the cover image associated to this source.
134 * <p>
e1de8087
NR
135 * By default, return the custom cover if any, and if not, return the cover
136 * of the first story with this source.
14b57448
NR
137 *
138 * @param source
139 * the source
140 *
141 * @return the cover image or NULL
0bb51c9c
NR
142 *
143 * @throws IOException
144 * in case of IOException
14b57448 145 */
0bb51c9c 146 public Image getSourceCover(String source) throws IOException {
e1de8087
NR
147 Image custom = getCustomSourceCover(source);
148 if (custom != null) {
149 return custom;
150 }
151
ef98466f 152 List<MetaData> metas = getList().filter(source, null, null);
14b57448
NR
153 if (metas.size() > 0) {
154 return getCover(metas.get(0).getLuid());
155 }
156
157 return null;
158 }
159
3989dfc5
NR
160 /**
161 * Return the cover image associated to this author.
162 * <p>
163 * By default, return the custom cover if any, and if not, return the cover
164 * of the first story with this author.
165 *
166 * @param author
167 * the author
168 *
169 * @return the cover image or NULL
0bb51c9c
NR
170 *
171 * @throws IOException
172 * in case of IOException
3989dfc5 173 */
0bb51c9c 174 public Image getAuthorCover(String author) throws IOException {
3989dfc5
NR
175 Image custom = getCustomAuthorCover(author);
176 if (custom != null) {
177 return custom;
178 }
179
ef98466f 180 List<MetaData> metas = getList().filter(null, author, null);
3989dfc5
NR
181 if (metas.size() > 0) {
182 return getCover(metas.get(0).getLuid());
183 }
184
185 return null;
186 }
187
e1de8087
NR
188 /**
189 * Return the custom cover image associated to this source.
190 * <p>
191 * By default, return NULL.
192 *
193 * @param source
194 * the source to look for
195 *
196 * @return the custom cover or NULL if none
0bb51c9c
NR
197 *
198 * @throws IOException
199 * in case of IOException
e1de8087 200 */
0c7a6637
NR
201 @SuppressWarnings("unused")
202 public Image getCustomSourceCover(String source) throws IOException {
e1de8087
NR
203 return null;
204 }
205
14b57448 206 /**
3989dfc5
NR
207 * Return the custom cover image associated to this author.
208 * <p>
209 * By default, return NULL.
210 *
211 * @param author
212 * the author to look for
213 *
214 * @return the custom cover or NULL if none
0bb51c9c
NR
215 *
216 * @throws IOException
217 * in case of IOException
3989dfc5 218 */
0c7a6637
NR
219 @SuppressWarnings("unused")
220 public Image getCustomAuthorCover(String author) throws IOException {
3989dfc5
NR
221 return null;
222 }
223
224 /**
225 * Set the source cover to the given story cover.
14b57448
NR
226 *
227 * @param source
228 * the source to change
229 * @param luid
230 * the story LUID
0bb51c9c
NR
231 *
232 * @throws IOException
233 * in case of IOException
14b57448 234 */
0bb51c9c
NR
235 public abstract void setSourceCover(String source, String luid)
236 throws IOException;
14b57448 237
3989dfc5
NR
238 /**
239 * Set the author cover to the given story cover.
240 *
d4449e96 241 * @param author
3989dfc5
NR
242 * the author to change
243 * @param luid
244 * the story LUID
0bb51c9c
NR
245 *
246 * @throws IOException
247 * in case of IOException
3989dfc5 248 */
0bb51c9c
NR
249 public abstract void setAuthorCover(String author, String luid)
250 throws IOException;
3989dfc5 251
68e2c6d2
NR
252 /**
253 * Return the list of stories (represented by their {@link MetaData}, which
254 * <b>MAY</b> not have the cover included).
1f2a7d5f
NR
255 * <p>
256 * The returned list <b>MUST</b> be a copy, not the original one.
68e2c6d2
NR
257 *
258 * @param pg
259 * the optional {@link Progress}
260 *
261 * @return the list (can be empty but not NULL)
0bb51c9c
NR
262 *
263 * @throws IOException
264 * in case of IOException
68e2c6d2 265 */
0bb51c9c 266 protected abstract List<MetaData> getMetas(Progress pg) throws IOException;
68e2c6d2
NR
267
268 /**
269 * Invalidate the {@link Story} cache (when the content should be re-read
270 * because it was changed).
271 */
c8d48938
NR
272 protected void invalidateInfo() {
273 invalidateInfo(null);
e272f05f
NR
274 }
275
276 /**
efa3c511
NR
277 * Invalidate the {@link Story} cache (when the content is removed).
278 * <p>
279 * All the cache can be deleted if NULL is passed as meta.
e272f05f
NR
280 *
281 * @param luid
efa3c511 282 * the LUID of the {@link Story} to clear from the cache, or NULL
e272f05f
NR
283 * for all stories
284 */
c8d48938 285 protected abstract void invalidateInfo(String luid);
efa3c511
NR
286
287 /**
288 * Invalidate the {@link Story} cache (when the content has changed, but we
289 * already have it) with the new given meta.
290 *
291 * @param meta
292 * the {@link Story} to clear from the cache
0bb51c9c
NR
293 *
294 * @throws IOException
295 * in case of IOException
efa3c511 296 */
0bb51c9c 297 protected abstract void updateInfo(MetaData meta) throws IOException;
68e2c6d2
NR
298
299 /**
300 * Return the next LUID that can be used.
301 *
302 * @return the next luid
303 */
304 protected abstract int getNextId();
305
306 /**
307 * Delete the target {@link Story}.
308 *
309 * @param luid
310 * the LUID of the {@link Story}
311 *
312 * @throws IOException
313 * in case of I/O error or if the {@link Story} wa not found
314 */
315 protected abstract void doDelete(String luid) throws IOException;
316
317 /**
318 * Actually save the story to the back-end.
319 *
320 * @param story
321 * the {@link Story} to save
322 * @param pg
323 * the optional {@link Progress}
324 *
325 * @return the saved {@link Story} (which may have changed, especially
326 * regarding the {@link MetaData})
327 *
328 * @throws IOException
329 * in case of I/O error
330 */
331 protected abstract Story doSave(Story story, Progress pg)
332 throws IOException;
333
334 /**
e6249b0f 335 * Refresh the {@link BasicLibrary}, that is, make sure all metas are
68e2c6d2
NR
336 * loaded.
337 *
68e2c6d2
NR
338 * @param pg
339 * the optional progress reporter
340 */
dc919036 341 public void refresh(Progress pg) {
0bb51c9c
NR
342 try {
343 getMetas(pg);
344 } catch (IOException e) {
345 // We will let it fail later
346 }
68e2c6d2 347 }
dc919036 348
4452446c
NR
349 /**
350 * Check if the {@link Story} denoted by this Library UID is present in the
351 * cache (if we have no cache, we default to </t>true</tt>).
352 *
353 * @param luid
354 * the Library UID
355 *
356 * @return TRUE if it is
357 */
358 public boolean isCached(String luid) {
359 // By default, everything is cached
360 return true;
361 }
dc919036 362
4452446c
NR
363 /**
364 * Clear the {@link Story} from the cache, if needed.
365 * <p>
366 * The next time we try to retrieve the {@link Story}, it may be required to
367 * cache it again.
368 *
369 * @param luid
370 * the story to clear
371 *
372 * @throws IOException
373 * in case of I/O error
374 */
375 public void clearFromCache(String luid) throws IOException {
376 // By default, this is a noop.
377 }
68e2c6d2
NR
378
379 /**
380 * List all the known types (sources) of stories.
381 *
382 * @return the sources
0bb51c9c
NR
383 *
384 * @throws IOException
385 * in case of IOException
68e2c6d2 386 */
dc919036 387 public List<String> getSources() throws IOException {
68e2c6d2
NR
388 List<String> list = new ArrayList<String>();
389 for (MetaData meta : getMetas(null)) {
390 String storySource = meta.getSource();
391 if (!list.contains(storySource)) {
392 list.add(storySource);
393 }
394 }
395
396 Collections.sort(list);
397 return list;
398 }
399
c1b93db3
NR
400 /**
401 * List all the known types (sources) of stories, grouped by directory
402 * ("Source_1/a" and "Source_1/b" will be grouped into "Source_1").
403 * <p>
404 * Note that an empty item in the list means a non-grouped source (type) --
405 * e.g., you could have for Source_1:
406 * <ul>
407 * <li><tt></tt>: empty, so source is "Source_1"</li>
408 * <li><tt>a</tt>: empty, so source is "Source_1/a"</li>
409 * <li><tt>b</tt>: empty, so source is "Source_1/b"</li>
410 * </ul>
411 *
412 * @return the grouped list
0bb51c9c
NR
413 *
414 * @throws IOException
415 * in case of IOException
c1b93db3 416 */
dc919036 417 public Map<String, List<String>> getSourcesGrouped() throws IOException {
c1b93db3
NR
418 Map<String, List<String>> map = new TreeMap<String, List<String>>();
419 for (String source : getSources()) {
420 String name;
421 String subname;
422
423 int pos = source.indexOf('/');
424 if (pos > 0 && pos < source.length() - 1) {
425 name = source.substring(0, pos);
426 subname = source.substring(pos + 1);
427
428 } else {
429 name = source;
430 subname = "";
431 }
432
433 List<String> list = map.get(name);
434 if (list == null) {
435 list = new ArrayList<String>();
436 map.put(name, list);
437 }
438 list.add(subname);
439 }
440
441 return map;
442 }
443
68e2c6d2
NR
444 /**
445 * List all the known authors of stories.
446 *
447 * @return the authors
0bb51c9c
NR
448 *
449 * @throws IOException
450 * in case of IOException
68e2c6d2 451 */
dc919036 452 public List<String> getAuthors() throws IOException {
68e2c6d2
NR
453 List<String> list = new ArrayList<String>();
454 for (MetaData meta : getMetas(null)) {
455 String storyAuthor = meta.getAuthor();
456 if (!list.contains(storyAuthor)) {
457 list.add(storyAuthor);
458 }
459 }
460
461 Collections.sort(list);
462 return list;
463 }
464
5f42f329
NR
465 /**
466 * Return the list of authors, grouped by starting letter(s) if needed.
467 * <p>
468 * If the number of author is not too high, only one group with an empty
469 * name and all the authors will be returned.
470 * <p>
471 * If not, the authors will be separated into groups:
472 * <ul>
473 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
474 * </li>
475 * <li><tt>0-9</tt>: any authors whose name starts with a number</li>
476 * <li><tt>A-C</tt> (for instance): any author whose name starts with
477 * <tt>A</tt>, <tt>B</tt> or <tt>C</tt></li>
478 * </ul>
479 * Note that the letters used in the groups can vary (except <tt>*</tt> and
480 * <tt>0-9</tt>, which may only be present or not).
481 *
482 * @return the authors' names, grouped by letter(s)
0bb51c9c
NR
483 *
484 * @throws IOException
485 * in case of IOException
5f42f329 486 */
0bb51c9c 487 public Map<String, List<String>> getAuthorsGrouped() throws IOException {
5f42f329
NR
488 int MAX = 20;
489
97b36d32 490 Map<String, List<String>> groups = new TreeMap<String, List<String>>();
5f42f329
NR
491 List<String> authors = getAuthors();
492
97b36d32 493 // If all authors fit the max, just report them as is
5f42f329 494 if (authors.size() <= MAX) {
97b36d32 495 groups.put("", authors);
5f42f329
NR
496 return groups;
497 }
498
97b36d32 499 // Create groups A to Z, which can be empty here
5f42f329 500 for (char car = 'A'; car <= 'Z'; car++) {
97b36d32 501 groups.put(Character.toString(car), getAuthorsGroup(authors, car));
5f42f329
NR
502 }
503
97b36d32
NR
504 // Collapse them
505 List<String> keys = new ArrayList<String>(groups.keySet());
506 for (int i = 0; i + 1 < keys.size(); i++) {
507 String keyNow = keys.get(i);
508 String keyNext = keys.get(i + 1);
509
510 List<String> now = groups.get(keyNow);
511 List<String> next = groups.get(keyNext);
512
513 int currentTotal = now.size() + next.size();
5f42f329 514 if (currentTotal <= MAX) {
97b36d32
NR
515 String key = keyNow.charAt(0) + "-"
516 + keyNext.charAt(keyNext.length() - 1);
517
5f42f329 518 List<String> all = new ArrayList<String>();
97b36d32
NR
519 all.addAll(now);
520 all.addAll(next);
521
522 groups.remove(keyNow);
523 groups.remove(keyNext);
524 groups.put(key, all);
525
526 keys.set(i, key); // set the new key instead of key(i)
527 keys.remove(i + 1); // remove the next, consumed key
528 i--; // restart at key(i)
5f42f329
NR
529 }
530 }
531
97b36d32
NR
532 // Add "special" groups
533 groups.put("*", getAuthorsGroup(authors, '*'));
534 groups.put("0-9", getAuthorsGroup(authors, '0'));
535
536 // Prune empty groups
537 keys = new ArrayList<String>(groups.keySet());
538 for (String key : keys) {
539 if (groups.get(key).isEmpty()) {
540 groups.remove(key);
5f42f329
NR
541 }
542 }
543
544 return groups;
545 }
546
547 /**
548 * Get all the authors that start with the given character:
549 * <ul>
550 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
551 * </li>
552 * <li><tt>0</tt>: any authors whose name starts with a number</li>
553 * <li><tt>A</tt> (any capital latin letter): any author whose name starts
554 * with <tt>A</tt></li>
555 * </ul>
556 *
557 * @param authors
558 * the full list of authors
559 * @param car
560 * the starting character, <tt>*</tt>, <tt>0</tt> or a capital
561 * letter
0bb51c9c 562 *
0c7a6637 563 * @return the authors that fulfil the starting letter
5f42f329 564 */
0c7a6637 565 private List<String> getAuthorsGroup(List<String> authors, char car) {
5f42f329
NR
566 List<String> accepted = new ArrayList<String>();
567 for (String author : authors) {
568 char first = '*';
569 for (int i = 0; first == '*' && i < author.length(); i++) {
570 String san = StringUtils.sanitize(author, true, true);
571 char c = san.charAt(i);
572 if (c >= '0' && c <= '9') {
573 first = '0';
574 } else if (c >= 'a' && c <= 'z') {
575 first = (char) (c - 'a' + 'A');
576 } else if (c >= 'A' && c <= 'Z') {
577 first = c;
578 }
579 }
580
581 if (first == car) {
582 accepted.add(author);
583 }
584 }
585
586 return accepted;
587 }
588
68e2c6d2
NR
589 /**
590 * List all the stories in the {@link BasicLibrary}.
591 * <p>
3a0605e6 592 * Cover images <b>MAYBE</b> not included.
68e2c6d2
NR
593 *
594 * @return the stories
0bb51c9c
NR
595 *
596 * @throws IOException
597 * in case of IOException
68e2c6d2 598 */
ef98466f
NR
599 public MetaResultList getList() throws IOException {
600 return getList(null);
68e2c6d2
NR
601 }
602
603 /**
604 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
605 * cover image <b>MAY</b> not be included.
606 *
607 * @param luid
dc919036 608 * the Library UID of the story, can be NULL
68e2c6d2 609 *
dc919036 610 * @return the corresponding {@link Story} or NULL if not found
0bb51c9c
NR
611 *
612 * @throws IOException
613 * in case of IOException
68e2c6d2 614 */
dc919036 615 public MetaData getInfo(String luid) throws IOException {
68e2c6d2
NR
616 if (luid != null) {
617 for (MetaData meta : getMetas(null)) {
618 if (luid.equals(meta.getLuid())) {
619 return meta;
620 }
621 }
622 }
623
624 return null;
625 }
626
627 /**
628 * Retrieve a specific {@link Story}.
629 *
630 * @param luid
631 * the Library UID of the story
632 * @param pg
633 * the optional progress reporter
634 *
635 * @return the corresponding {@link Story} or NULL if not found
0bb51c9c
NR
636 *
637 * @throws IOException
638 * in case of IOException
68e2c6d2 639 */
0bb51c9c
NR
640 public synchronized Story getStory(String luid, Progress pg)
641 throws IOException {
60f72311
NR
642 Progress pgMetas = new Progress();
643 Progress pgStory = new Progress();
644 if (pg != null) {
645 pg.setMinMax(0, 100);
646 pg.addProgress(pgMetas, 10);
647 pg.addProgress(pgStory, 90);
648 }
649
650 MetaData meta = null;
651 for (MetaData oneMeta : getMetas(pgMetas)) {
652 if (oneMeta.getLuid().equals(luid)) {
653 meta = oneMeta;
654 break;
655 }
656 }
657
658 pgMetas.done();
659
660 Story story = getStory(luid, meta, pgStory);
661 pgStory.done();
662
663 return story;
664 }
665
666 /**
667 * Retrieve a specific {@link Story}.
668 *
669 * @param luid
dc919036
NR
670 * the LUID of the story
671 * @param meta
60f72311
NR
672 * the meta of the story
673 * @param pg
674 * the optional progress reporter
675 *
676 * @return the corresponding {@link Story} or NULL if not found
0bb51c9c
NR
677 *
678 * @throws IOException
679 * in case of IOException
60f72311 680 */
dc919036 681 public synchronized Story getStory(String luid, MetaData meta, Progress pg)
0bb51c9c 682 throws IOException {
60f72311 683
68e2c6d2
NR
684 if (pg == null) {
685 pg = new Progress();
686 }
687
ff05b828
NR
688 Progress pgGet = new Progress();
689 Progress pgProcess = new Progress();
690
691 pg.setMinMax(0, 2);
692 pg.addProgress(pgGet, 1);
693 pg.addProgress(pgProcess, 1);
694
68e2c6d2 695 Story story = null;
dc919036
NR
696 File file = null;
697
698 if (luid != null && meta != null) {
699 file = getFile(luid, pgGet);
700 }
701
60f72311
NR
702 pgGet.done();
703 try {
dc919036
NR
704 if (file != null) {
705 SupportType type = SupportType.valueOfAllOkUC(meta.getType());
706 if (type == null) {
707 throw new IOException("Unknown type: " + meta.getType());
708 }
709
710 URL url = file.toURI().toURL();
60f72311
NR
711 story = BasicSupport.getSupport(type, url) //
712 .process(pgProcess);
713
714 // Because we do not want to clear the meta cache:
715 meta.setCover(story.getMeta().getCover());
716 meta.setResume(story.getMeta().getResume());
717 story.setMeta(meta);
68e2c6d2 718 }
60f72311 719 } catch (IOException e) {
dc919036
NR
720 // We should not have not-supported files in the library
721 Instance.getInstance().getTraceHandler()
722 .error(new IOException(String.format(
723 "Cannot load file of type '%s' from library: %s",
724 meta.getType(), file), e));
60f72311
NR
725 } finally {
726 pgProcess.done();
727 pg.done();
68e2c6d2
NR
728 }
729
730 return story;
731 }
732
733 /**
734 * Import the {@link Story} at the given {@link URL} into the
735 * {@link BasicLibrary}.
736 *
737 * @param url
738 * the {@link URL} to import
739 * @param pg
740 * the optional progress reporter
741 *
b6b65795 742 * @return the imported Story {@link MetaData}
68e2c6d2 743 *
fd1d31c2
NR
744 * @throws UnknownHostException
745 * if the host is not supported
68e2c6d2
NR
746 * @throws IOException
747 * in case of I/O error
748 */
b6b65795 749 public MetaData imprt(URL url, Progress pg) throws IOException {
9b863b20
NR
750 if (pg == null)
751 pg = new Progress();
752
753 pg.setMinMax(0, 1000);
754 Progress pgProcess = new Progress();
755 Progress pgSave = new Progress();
756 pg.addProgress(pgProcess, 800);
757 pg.addProgress(pgSave, 200);
758
68e2c6d2
NR
759 BasicSupport support = BasicSupport.getSupport(url);
760 if (support == null) {
fd1d31c2 761 throw new UnknownHostException("" + url);
68e2c6d2
NR
762 }
763
9b863b20 764 Story story = save(support.process(pgProcess), pgSave);
3b039231 765 pg.setName(story.getMeta().getTitle());
9b863b20
NR
766 pg.done();
767
b6b65795 768 return story.getMeta();
68e2c6d2
NR
769 }
770
b89dfb6e
NR
771 /**
772 * Import the story from one library to another, and keep the same LUID.
773 *
774 * @param other
775 * the other library to import from
776 * @param luid
777 * the Library UID
778 * @param pg
779 * the optional progress reporter
780 *
781 * @throws IOException
782 * in case of I/O error
783 */
784 public void imprt(BasicLibrary other, String luid, Progress pg)
785 throws IOException {
786 Progress pgGetStory = new Progress();
787 Progress pgSave = new Progress();
788 if (pg == null) {
789 pg = new Progress();
790 }
791
792 pg.setMinMax(0, 2);
793 pg.addProgress(pgGetStory, 1);
794 pg.addProgress(pgSave, 1);
795
796 Story story = other.getStory(luid, pgGetStory);
797 if (story != null) {
798 story = this.save(story, luid, pgSave);
799 pg.done();
800 } else {
801 pg.done();
802 throw new IOException("Cannot find story in Library: " + luid);
803 }
804 }
805
68e2c6d2
NR
806 /**
807 * Export the {@link Story} to the given target in the given format.
808 *
809 * @param luid
810 * the {@link Story} ID
811 * @param type
812 * the {@link OutputType} to transform it to
813 * @param target
814 * the target to save to
815 * @param pg
816 * the optional progress reporter
817 *
818 * @return the saved resource (the main saved {@link File})
819 *
820 * @throws IOException
821 * in case of I/O error
822 */
823 public File export(String luid, OutputType type, String target, Progress pg)
824 throws IOException {
825 Progress pgGetStory = new Progress();
826 Progress pgOut = new Progress();
827 if (pg != null) {
828 pg.setMax(2);
829 pg.addProgress(pgGetStory, 1);
830 pg.addProgress(pgOut, 1);
831 }
832
925298fd 833 BasicOutput out = BasicOutput.getOutput(type, false, false);
68e2c6d2
NR
834 if (out == null) {
835 throw new IOException("Output type not supported: " + type);
836 }
837
838 Story story = getStory(luid, pgGetStory);
839 if (story == null) {
840 throw new IOException("Cannot find story to export: " + luid);
841 }
842
843 return out.process(story, target, pgOut);
844 }
845
846 /**
847 * Save a {@link Story} to the {@link BasicLibrary}.
848 *
849 * @param story
850 * the {@link Story} to save
851 * @param pg
852 * the optional progress reporter
853 *
854 * @return the same {@link Story}, whose LUID may have changed
855 *
856 * @throws IOException
857 * in case of I/O error
858 */
859 public Story save(Story story, Progress pg) throws IOException {
860 return save(story, null, pg);
861 }
862
863 /**
864 * Save a {@link Story} to the {@link BasicLibrary} -- the LUID <b>must</b>
865 * be correct, or NULL to get the next free one.
866 * <p>
867 * Will override any previous {@link Story} with the same LUID.
868 *
869 * @param story
870 * the {@link Story} to save
871 * @param luid
872 * the <b>correct</b> LUID or NULL to get the next free one
873 * @param pg
874 * the optional progress reporter
875 *
876 * @return the same {@link Story}, whose LUID may have changed
877 *
878 * @throws IOException
879 * in case of I/O error
880 */
881 public synchronized Story save(Story story, String luid, Progress pg)
882 throws IOException {
3b039231
NR
883 if (pg == null) {
884 pg = new Progress();
885 }
dc919036
NR
886
887 Instance.getInstance().getTraceHandler().trace(
888 this.getClass().getSimpleName() + ": saving story " + luid);
9e2fad36 889
68e2c6d2
NR
890 // Do not change the original metadata, but change the original story
891 MetaData meta = story.getMeta().clone();
892 story.setMeta(meta);
893
3b039231 894 pg.setName("Saving story");
dc919036 895
68e2c6d2
NR
896 if (luid == null || luid.isEmpty()) {
897 meta.setLuid(String.format("%03d", getNextId()));
898 } else {
899 meta.setLuid(luid);
900 }
901
e272f05f 902 if (luid != null && getInfo(luid) != null) {
68e2c6d2
NR
903 delete(luid);
904 }
14b57448 905
efa3c511 906 story = doSave(story, pg);
0bf8264e 907
efa3c511 908 updateInfo(story.getMeta());
68e2c6d2 909
d66deb8d 910 Instance.getInstance().getTraceHandler()
dc919036
NR
911 .trace(this.getClass().getSimpleName() + ": story saved ("
912 + luid + ")");
9e2fad36 913
3b039231
NR
914 pg.setName(meta.getTitle());
915 pg.done();
68e2c6d2
NR
916 return story;
917 }
918
919 /**
920 * Delete the given {@link Story} from this {@link BasicLibrary}.
921 *
922 * @param luid
923 * the LUID of the target {@link Story}
924 *
925 * @throws IOException
926 * in case of I/O error
927 */
928 public synchronized void delete(String luid) throws IOException {
dc919036
NR
929 Instance.getInstance().getTraceHandler().trace(
930 this.getClass().getSimpleName() + ": deleting story " + luid);
9e2fad36 931
68e2c6d2 932 doDelete(luid);
c8d48938 933 invalidateInfo(luid);
9e2fad36 934
d66deb8d 935 Instance.getInstance().getTraceHandler()
dc919036
NR
936 .trace(this.getClass().getSimpleName() + ": story deleted ("
937 + luid + ")");
68e2c6d2
NR
938 }
939
940 /**
941 * Change the type (source) of the given {@link Story}.
942 *
943 * @param luid
944 * the {@link Story} LUID
945 * @param newSource
946 * the new source
947 * @param pg
948 * the optional progress reporter
949 *
950 * @throws IOException
951 * in case of I/O error or if the {@link Story} was not found
952 */
953 public synchronized void changeSource(String luid, String newSource,
954 Progress pg) throws IOException {
955 MetaData meta = getInfo(luid);
956 if (meta == null) {
957 throw new IOException("Story not found: " + luid);
958 }
959
c8d48938
NR
960 changeSTA(luid, newSource, meta.getTitle(), meta.getAuthor(), pg);
961 }
962
963 /**
964 * Change the title (name) of the given {@link Story}.
965 *
966 * @param luid
967 * the {@link Story} LUID
968 * @param newTitle
969 * the new title
970 * @param pg
971 * the optional progress reporter
972 *
973 * @throws IOException
974 * in case of I/O error or if the {@link Story} was not found
975 */
976 public synchronized void changeTitle(String luid, String newTitle,
977 Progress pg) throws IOException {
978 MetaData meta = getInfo(luid);
979 if (meta == null) {
980 throw new IOException("Story not found: " + luid);
981 }
982
983 changeSTA(luid, meta.getSource(), newTitle, meta.getAuthor(), pg);
984 }
985
986 /**
987 * Change the author of the given {@link Story}.
988 *
989 * @param luid
990 * the {@link Story} LUID
991 * @param newAuthor
992 * the new author
993 * @param pg
994 * the optional progress reporter
995 *
996 * @throws IOException
997 * in case of I/O error or if the {@link Story} was not found
998 */
999 public synchronized void changeAuthor(String luid, String newAuthor,
1000 Progress pg) throws IOException {
1001 MetaData meta = getInfo(luid);
1002 if (meta == null) {
1003 throw new IOException("Story not found: " + luid);
1004 }
1005
1006 changeSTA(luid, meta.getSource(), meta.getTitle(), newAuthor, pg);
1007 }
1008
1009 /**
1010 * Change the Source, Title and Author of the {@link Story} in one single
1011 * go.
1012 *
1013 * @param luid
1014 * the {@link Story} LUID
1015 * @param newSource
1016 * the new source
1017 * @param newTitle
1018 * the new title
1019 * @param newAuthor
1020 * the new author
1021 * @param pg
1022 * the optional progress reporter
1023 *
1024 * @throws IOException
1025 * in case of I/O error or if the {@link Story} was not found
1026 */
1027 protected synchronized void changeSTA(String luid, String newSource,
1028 String newTitle, String newAuthor, Progress pg) throws IOException {
1029 MetaData meta = getInfo(luid);
1030 if (meta == null) {
1031 throw new IOException("Story not found: " + luid);
1032 }
1033
68e2c6d2 1034 meta.setSource(newSource);
c8d48938
NR
1035 meta.setTitle(newTitle);
1036 meta.setAuthor(newAuthor);
68e2c6d2
NR
1037 saveMeta(meta, pg);
1038 }
1039
1040 /**
1041 * Save back the current state of the {@link MetaData} (LUID <b>MUST NOT</b>
1042 * change) for this {@link Story}.
1043 * <p>
1044 * By default, delete the old {@link Story} then recreate a new
1045 * {@link Story}.
1046 * <p>
c8d48938 1047 * Note that this behaviour can lead to data loss in case of problems!
68e2c6d2
NR
1048 *
1049 * @param meta
1050 * the new {@link MetaData} (LUID <b>MUST NOT</b> change)
1051 * @param pg
1052 * the optional {@link Progress}
1053 *
1054 * @throws IOException
1055 * in case of I/O error or if the {@link Story} was not found
1056 */
1057 protected synchronized void saveMeta(MetaData meta, Progress pg)
1058 throws IOException {
1059 if (pg == null) {
1060 pg = new Progress();
1061 }
1062
1063 Progress pgGet = new Progress();
1064 Progress pgSet = new Progress();
1065 pg.addProgress(pgGet, 50);
1066 pg.addProgress(pgSet, 50);
1067
1068 Story story = getStory(meta.getLuid(), pgGet);
1069 if (story == null) {
1070 throw new IOException("Story not found: " + meta.getLuid());
1071 }
1072
c8d48938 1073 // TODO: this is not safe!
68e2c6d2 1074 delete(meta.getLuid());
68e2c6d2
NR
1075 story.setMeta(meta);
1076 save(story, meta.getLuid(), pgSet);
1077
1078 pg.done();
1079 }
1080}