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