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