Instance: use getInstance()
[nikiroo-utils.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 @SuppressWarnings("unused")
195 public Image getCustomSourceCover(String source) 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 @SuppressWarnings("unused")
213 public Image getCustomAuthorCover(String author) 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 public void refresh(Progress pg) {
333 try {
334 getMetas(pg);
335 } catch (IOException e) {
336 // We will let it fail later
337 }
338 }
339
340 /**
341 * List all the known types (sources) of stories.
342 *
343 * @return the sources
344 *
345 * @throws IOException
346 * in case of IOException
347 */
348 public synchronized List<String> getSources() throws IOException {
349 List<String> list = new ArrayList<String>();
350 for (MetaData meta : getMetas(null)) {
351 String storySource = meta.getSource();
352 if (!list.contains(storySource)) {
353 list.add(storySource);
354 }
355 }
356
357 Collections.sort(list);
358 return list;
359 }
360
361 /**
362 * List all the known types (sources) of stories, grouped by directory
363 * ("Source_1/a" and "Source_1/b" will be grouped into "Source_1").
364 * <p>
365 * Note that an empty item in the list means a non-grouped source (type) --
366 * e.g., you could have for Source_1:
367 * <ul>
368 * <li><tt></tt>: empty, so source is "Source_1"</li>
369 * <li><tt>a</tt>: empty, so source is "Source_1/a"</li>
370 * <li><tt>b</tt>: empty, so source is "Source_1/b"</li>
371 * </ul>
372 *
373 * @return the grouped list
374 *
375 * @throws IOException
376 * in case of IOException
377 */
378 public synchronized Map<String, List<String>> getSourcesGrouped()
379 throws IOException {
380 Map<String, List<String>> map = new TreeMap<String, List<String>>();
381 for (String source : getSources()) {
382 String name;
383 String subname;
384
385 int pos = source.indexOf('/');
386 if (pos > 0 && pos < source.length() - 1) {
387 name = source.substring(0, pos);
388 subname = source.substring(pos + 1);
389
390 } else {
391 name = source;
392 subname = "";
393 }
394
395 List<String> list = map.get(name);
396 if (list == null) {
397 list = new ArrayList<String>();
398 map.put(name, list);
399 }
400 list.add(subname);
401 }
402
403 return map;
404 }
405
406 /**
407 * List all the known authors of stories.
408 *
409 * @return the authors
410 *
411 * @throws IOException
412 * in case of IOException
413 */
414 public synchronized List<String> getAuthors() throws IOException {
415 List<String> list = new ArrayList<String>();
416 for (MetaData meta : getMetas(null)) {
417 String storyAuthor = meta.getAuthor();
418 if (!list.contains(storyAuthor)) {
419 list.add(storyAuthor);
420 }
421 }
422
423 Collections.sort(list);
424 return list;
425 }
426
427 /**
428 * Return the list of authors, grouped by starting letter(s) if needed.
429 * <p>
430 * If the number of author is not too high, only one group with an empty
431 * name and all the authors will be returned.
432 * <p>
433 * If not, the authors will be separated into groups:
434 * <ul>
435 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
436 * </li>
437 * <li><tt>0-9</tt>: any authors whose name starts with a number</li>
438 * <li><tt>A-C</tt> (for instance): any author whose name starts with
439 * <tt>A</tt>, <tt>B</tt> or <tt>C</tt></li>
440 * </ul>
441 * Note that the letters used in the groups can vary (except <tt>*</tt> and
442 * <tt>0-9</tt>, which may only be present or not).
443 *
444 * @return the authors' names, grouped by letter(s)
445 *
446 * @throws IOException
447 * in case of IOException
448 */
449 public Map<String, List<String>> getAuthorsGrouped() throws IOException {
450 int MAX = 20;
451
452 Map<String, List<String>> groups = new TreeMap<String, List<String>>();
453 List<String> authors = getAuthors();
454
455 // If all authors fit the max, just report them as is
456 if (authors.size() <= MAX) {
457 groups.put("", authors);
458 return groups;
459 }
460
461 // Create groups A to Z, which can be empty here
462 for (char car = 'A'; car <= 'Z'; car++) {
463 groups.put(Character.toString(car), getAuthorsGroup(authors, car));
464 }
465
466 // Collapse them
467 List<String> keys = new ArrayList<String>(groups.keySet());
468 for (int i = 0; i + 1 < keys.size(); i++) {
469 String keyNow = keys.get(i);
470 String keyNext = keys.get(i + 1);
471
472 List<String> now = groups.get(keyNow);
473 List<String> next = groups.get(keyNext);
474
475 int currentTotal = now.size() + next.size();
476 if (currentTotal <= MAX) {
477 String key = keyNow.charAt(0) + "-"
478 + keyNext.charAt(keyNext.length() - 1);
479
480 List<String> all = new ArrayList<String>();
481 all.addAll(now);
482 all.addAll(next);
483
484 groups.remove(keyNow);
485 groups.remove(keyNext);
486 groups.put(key, all);
487
488 keys.set(i, key); // set the new key instead of key(i)
489 keys.remove(i + 1); // remove the next, consumed key
490 i--; // restart at key(i)
491 }
492 }
493
494 // Add "special" groups
495 groups.put("*", getAuthorsGroup(authors, '*'));
496 groups.put("0-9", getAuthorsGroup(authors, '0'));
497
498 // Prune empty groups
499 keys = new ArrayList<String>(groups.keySet());
500 for (String key : keys) {
501 if (groups.get(key).isEmpty()) {
502 groups.remove(key);
503 }
504 }
505
506 return groups;
507 }
508
509 /**
510 * Get all the authors that start with the given character:
511 * <ul>
512 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
513 * </li>
514 * <li><tt>0</tt>: any authors whose name starts with a number</li>
515 * <li><tt>A</tt> (any capital latin letter): any author whose name starts
516 * with <tt>A</tt></li>
517 * </ul>
518 *
519 * @param authors
520 * the full list of authors
521 * @param car
522 * the starting character, <tt>*</tt>, <tt>0</tt> or a capital
523 * letter
524 *
525 * @return the authors that fulfil the starting letter
526 */
527 private List<String> getAuthorsGroup(List<String> authors, char car) {
528 List<String> accepted = new ArrayList<String>();
529 for (String author : authors) {
530 char first = '*';
531 for (int i = 0; first == '*' && i < author.length(); i++) {
532 String san = StringUtils.sanitize(author, true, true);
533 char c = san.charAt(i);
534 if (c >= '0' && c <= '9') {
535 first = '0';
536 } else if (c >= 'a' && c <= 'z') {
537 first = (char) (c - 'a' + 'A');
538 } else if (c >= 'A' && c <= 'Z') {
539 first = c;
540 }
541 }
542
543 if (first == car) {
544 accepted.add(author);
545 }
546 }
547
548 return accepted;
549 }
550
551 /**
552 * List all the stories in the {@link BasicLibrary}.
553 * <p>
554 * Cover images <b>MAYBE</b> not included.
555 *
556 * @return the stories
557 *
558 * @throws IOException
559 * in case of IOException
560 */
561 public synchronized List<MetaData> getList() throws IOException {
562 return getMetas(null);
563 }
564
565 /**
566 * List all the stories of the given source type in the {@link BasicLibrary}
567 * , or all the stories if NULL is passed as a type.
568 * <p>
569 * Cover images not included.
570 *
571 * @param type
572 * the type of story to retrieve, or NULL for all
573 *
574 * @return the stories
575 *
576 * @throws IOException
577 * in case of IOException
578 */
579 public synchronized List<MetaData> getListBySource(String type)
580 throws IOException {
581 List<MetaData> list = new ArrayList<MetaData>();
582 for (MetaData meta : getMetas(null)) {
583 String storyType = meta.getSource();
584 if (type == null || type.equalsIgnoreCase(storyType)) {
585 list.add(meta);
586 }
587 }
588
589 Collections.sort(list);
590 return list;
591 }
592
593 /**
594 * List all the stories of the given author in the {@link BasicLibrary}, or
595 * all the stories if NULL is passed as an author.
596 * <p>
597 * Cover images not included.
598 *
599 * @param author
600 * the author of the stories to retrieve, or NULL for all
601 *
602 * @return the stories
603 *
604 * @throws IOException
605 * in case of IOException
606 */
607 public synchronized List<MetaData> getListByAuthor(String author)
608 throws IOException {
609 List<MetaData> list = new ArrayList<MetaData>();
610 for (MetaData meta : getMetas(null)) {
611 String storyAuthor = meta.getAuthor();
612 if (author == null || author.equalsIgnoreCase(storyAuthor)) {
613 list.add(meta);
614 }
615 }
616
617 Collections.sort(list);
618 return list;
619 }
620
621 /**
622 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
623 * cover image <b>MAY</b> not be included.
624 *
625 * @param luid
626 * the Library UID of the story
627 *
628 * @return the corresponding {@link Story}
629 *
630 * @throws IOException
631 * in case of IOException
632 */
633 public synchronized MetaData getInfo(String luid) throws IOException {
634 if (luid != null) {
635 for (MetaData meta : getMetas(null)) {
636 if (luid.equals(meta.getLuid())) {
637 return meta;
638 }
639 }
640 }
641
642 return null;
643 }
644
645 /**
646 * Retrieve a specific {@link Story}.
647 *
648 * @param luid
649 * the Library UID of the story
650 * @param pg
651 * the optional progress reporter
652 *
653 * @return the corresponding {@link Story} or NULL if not found
654 *
655 * @throws IOException
656 * in case of IOException
657 */
658 public synchronized Story getStory(String luid, Progress pg)
659 throws IOException {
660 Progress pgMetas = new Progress();
661 Progress pgStory = new Progress();
662 if (pg != null) {
663 pg.setMinMax(0, 100);
664 pg.addProgress(pgMetas, 10);
665 pg.addProgress(pgStory, 90);
666 }
667
668 MetaData meta = null;
669 for (MetaData oneMeta : getMetas(pgMetas)) {
670 if (oneMeta.getLuid().equals(luid)) {
671 meta = oneMeta;
672 break;
673 }
674 }
675
676 pgMetas.done();
677
678 Story story = getStory(luid, meta, pgStory);
679 pgStory.done();
680
681 return story;
682 }
683
684 /**
685 * Retrieve a specific {@link Story}.
686 *
687 * @param luid
688 * the meta of the story
689 * @param pg
690 * the optional progress reporter
691 *
692 * @return the corresponding {@link Story} or NULL if not found
693 *
694 * @throws IOException
695 * in case of IOException
696 */
697 public synchronized Story getStory(String luid,
698 @SuppressWarnings("javadoc") MetaData meta, Progress pg)
699 throws IOException {
700
701 if (pg == null) {
702 pg = new Progress();
703 }
704
705 Progress pgGet = new Progress();
706 Progress pgProcess = new Progress();
707
708 pg.setMinMax(0, 2);
709 pg.addProgress(pgGet, 1);
710 pg.addProgress(pgProcess, 1);
711
712 Story story = null;
713 File file = getFile(luid, pgGet);
714 pgGet.done();
715 try {
716 SupportType type = SupportType.valueOfAllOkUC(meta.getType());
717 URL url = file.toURI().toURL();
718 if (type != null) {
719 story = BasicSupport.getSupport(type, url) //
720 .process(pgProcess);
721
722 // Because we do not want to clear the meta cache:
723 meta.setCover(story.getMeta().getCover());
724 meta.setResume(story.getMeta().getResume());
725 story.setMeta(meta);
726 //
727 } else {
728 throw new IOException("Unknown type: " + meta.getType());
729 }
730 } catch (IOException e) {
731 // We should not have not-supported files in the
732 // library
733 Instance.getInstance().getTraceHandler().error(new IOException(
734 String.format("Cannot load file of type '%s' from library: %s", meta.getType(), file), e));
735 } finally {
736 pgProcess.done();
737 pg.done();
738 }
739
740 return story;
741 }
742
743 /**
744 * Import the {@link Story} at the given {@link URL} into the
745 * {@link BasicLibrary}.
746 *
747 * @param url
748 * the {@link URL} to import
749 * @param pg
750 * the optional progress reporter
751 *
752 * @return the imported Story {@link MetaData}
753 *
754 * @throws UnknownHostException
755 * if the host is not supported
756 * @throws IOException
757 * in case of I/O error
758 */
759 public MetaData imprt(URL url, Progress pg) throws IOException {
760 if (pg == null)
761 pg = new Progress();
762
763 pg.setMinMax(0, 1000);
764 Progress pgProcess = new Progress();
765 Progress pgSave = new Progress();
766 pg.addProgress(pgProcess, 800);
767 pg.addProgress(pgSave, 200);
768
769 BasicSupport support = BasicSupport.getSupport(url);
770 if (support == null) {
771 throw new UnknownHostException("" + url);
772 }
773
774 Story story = save(support.process(pgProcess), pgSave);
775 pg.done();
776
777 return story.getMeta();
778 }
779
780 /**
781 * Import the story from one library to another, and keep the same LUID.
782 *
783 * @param other
784 * the other library to import from
785 * @param luid
786 * the Library UID
787 * @param pg
788 * the optional progress reporter
789 *
790 * @throws IOException
791 * in case of I/O error
792 */
793 public void imprt(BasicLibrary other, String luid, Progress pg)
794 throws IOException {
795 Progress pgGetStory = new Progress();
796 Progress pgSave = new Progress();
797 if (pg == null) {
798 pg = new Progress();
799 }
800
801 pg.setMinMax(0, 2);
802 pg.addProgress(pgGetStory, 1);
803 pg.addProgress(pgSave, 1);
804
805 Story story = other.getStory(luid, pgGetStory);
806 if (story != null) {
807 story = this.save(story, luid, pgSave);
808 pg.done();
809 } else {
810 pg.done();
811 throw new IOException("Cannot find story in Library: " + luid);
812 }
813 }
814
815 /**
816 * Export the {@link Story} to the given target in the given format.
817 *
818 * @param luid
819 * the {@link Story} ID
820 * @param type
821 * the {@link OutputType} to transform it to
822 * @param target
823 * the target to save to
824 * @param pg
825 * the optional progress reporter
826 *
827 * @return the saved resource (the main saved {@link File})
828 *
829 * @throws IOException
830 * in case of I/O error
831 */
832 public File export(String luid, OutputType type, String target, Progress pg)
833 throws IOException {
834 Progress pgGetStory = new Progress();
835 Progress pgOut = new Progress();
836 if (pg != null) {
837 pg.setMax(2);
838 pg.addProgress(pgGetStory, 1);
839 pg.addProgress(pgOut, 1);
840 }
841
842 BasicOutput out = BasicOutput.getOutput(type, false, false);
843 if (out == null) {
844 throw new IOException("Output type not supported: " + type);
845 }
846
847 Story story = getStory(luid, pgGetStory);
848 if (story == null) {
849 throw new IOException("Cannot find story to export: " + luid);
850 }
851
852 return out.process(story, target, pgOut);
853 }
854
855 /**
856 * Save a {@link Story} to the {@link BasicLibrary}.
857 *
858 * @param story
859 * the {@link Story} to save
860 * @param pg
861 * the optional progress reporter
862 *
863 * @return the same {@link Story}, whose LUID may have changed
864 *
865 * @throws IOException
866 * in case of I/O error
867 */
868 public Story save(Story story, Progress pg) throws IOException {
869 return save(story, null, pg);
870 }
871
872 /**
873 * Save a {@link Story} to the {@link BasicLibrary} -- the LUID <b>must</b>
874 * be correct, or NULL to get the next free one.
875 * <p>
876 * Will override any previous {@link Story} with the same LUID.
877 *
878 * @param story
879 * the {@link Story} to save
880 * @param luid
881 * the <b>correct</b> LUID or NULL to get the next free one
882 * @param pg
883 * the optional progress reporter
884 *
885 * @return the same {@link Story}, whose LUID may have changed
886 *
887 * @throws IOException
888 * in case of I/O error
889 */
890 public synchronized Story save(Story story, String luid, Progress pg)
891 throws IOException {
892
893 Instance.getInstance().getTraceHandler().trace(this.getClass().getSimpleName() + ": saving story " + luid);
894
895 // Do not change the original metadata, but change the original story
896 MetaData meta = story.getMeta().clone();
897 story.setMeta(meta);
898
899 if (luid == null || luid.isEmpty()) {
900 meta.setLuid(String.format("%03d", getNextId()));
901 } else {
902 meta.setLuid(luid);
903 }
904
905 if (luid != null && getInfo(luid) != null) {
906 delete(luid);
907 }
908
909 story = doSave(story, pg);
910
911 updateInfo(story.getMeta());
912
913 Instance.getInstance().getTraceHandler()
914 .trace(this.getClass().getSimpleName() + ": story saved (" + luid + ")");
915
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 {
929 Instance.getInstance().getTraceHandler().trace(this.getClass().getSimpleName() + ": deleting story " + luid);
930
931 doDelete(luid);
932 invalidateInfo(luid);
933
934 Instance.getInstance().getTraceHandler()
935 .trace(this.getClass().getSimpleName() + ": story deleted (" + luid
936 + ")");
937 }
938
939 /**
940 * Change the type (source) of the given {@link Story}.
941 *
942 * @param luid
943 * the {@link Story} LUID
944 * @param newSource
945 * the new source
946 * @param pg
947 * the optional progress reporter
948 *
949 * @throws IOException
950 * in case of I/O error or if the {@link Story} was not found
951 */
952 public synchronized void changeSource(String luid, String newSource,
953 Progress pg) throws IOException {
954 MetaData meta = getInfo(luid);
955 if (meta == null) {
956 throw new IOException("Story not found: " + luid);
957 }
958
959 changeSTA(luid, newSource, meta.getTitle(), meta.getAuthor(), pg);
960 }
961
962 /**
963 * Change the title (name) of the given {@link Story}.
964 *
965 * @param luid
966 * the {@link Story} LUID
967 * @param newTitle
968 * the new title
969 * @param pg
970 * the optional progress reporter
971 *
972 * @throws IOException
973 * in case of I/O error or if the {@link Story} was not found
974 */
975 public synchronized void changeTitle(String luid, String newTitle,
976 Progress pg) throws IOException {
977 MetaData meta = getInfo(luid);
978 if (meta == null) {
979 throw new IOException("Story not found: " + luid);
980 }
981
982 changeSTA(luid, meta.getSource(), newTitle, meta.getAuthor(), pg);
983 }
984
985 /**
986 * Change the author of the given {@link Story}.
987 *
988 * @param luid
989 * the {@link Story} LUID
990 * @param newAuthor
991 * the new author
992 * @param pg
993 * the optional progress reporter
994 *
995 * @throws IOException
996 * in case of I/O error or if the {@link Story} was not found
997 */
998 public synchronized void changeAuthor(String luid, String newAuthor,
999 Progress pg) throws IOException {
1000 MetaData meta = getInfo(luid);
1001 if (meta == null) {
1002 throw new IOException("Story not found: " + luid);
1003 }
1004
1005 changeSTA(luid, meta.getSource(), meta.getTitle(), newAuthor, pg);
1006 }
1007
1008 /**
1009 * Change the Source, Title and Author of the {@link Story} in one single
1010 * go.
1011 *
1012 * @param luid
1013 * the {@link Story} LUID
1014 * @param newSource
1015 * the new source
1016 * @param newTitle
1017 * the new title
1018 * @param newAuthor
1019 * the new author
1020 * @param pg
1021 * the optional progress reporter
1022 *
1023 * @throws IOException
1024 * in case of I/O error or if the {@link Story} was not found
1025 */
1026 protected synchronized void changeSTA(String luid, String newSource,
1027 String newTitle, String newAuthor, Progress pg) throws IOException {
1028 MetaData meta = getInfo(luid);
1029 if (meta == null) {
1030 throw new IOException("Story not found: " + luid);
1031 }
1032
1033 meta.setSource(newSource);
1034 meta.setTitle(newTitle);
1035 meta.setAuthor(newAuthor);
1036 saveMeta(meta, pg);
1037
1038 invalidateInfo(luid);
1039 }
1040
1041 /**
1042 * Save back the current state of the {@link MetaData} (LUID <b>MUST NOT</b>
1043 * change) for this {@link Story}.
1044 * <p>
1045 * By default, delete the old {@link Story} then recreate a new
1046 * {@link Story}.
1047 * <p>
1048 * Note that this behaviour can lead to data loss in case of problems!
1049 *
1050 * @param meta
1051 * the new {@link MetaData} (LUID <b>MUST NOT</b> change)
1052 * @param pg
1053 * the optional {@link Progress}
1054 *
1055 * @throws IOException
1056 * in case of I/O error or if the {@link Story} was not found
1057 */
1058 protected synchronized void saveMeta(MetaData meta, Progress pg)
1059 throws IOException {
1060 if (pg == null) {
1061 pg = new Progress();
1062 }
1063
1064 Progress pgGet = new Progress();
1065 Progress pgSet = new Progress();
1066 pg.addProgress(pgGet, 50);
1067 pg.addProgress(pgSet, 50);
1068
1069 Story story = getStory(meta.getLuid(), pgGet);
1070 if (story == null) {
1071 throw new IOException("Story not found: " + meta.getLuid());
1072 }
1073
1074 // TODO: this is not safe!
1075 delete(meta.getLuid());
1076 story.setMeta(meta);
1077 save(story, meta.getLuid(), pgSet);
1078
1079 pg.done();
1080 }
1081 }