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