update nikiroo-utils, remote lib errors need some work
[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 UNAUTHORIZED,
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 author
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,
603 @SuppressWarnings("javadoc") MetaData meta, Progress pg) {
604
605 if (pg == null) {
606 pg = new Progress();
607 }
608
609 Progress pgGet = new Progress();
610 Progress pgProcess = new Progress();
611
612 pg.setMinMax(0, 2);
613 pg.addProgress(pgGet, 1);
614 pg.addProgress(pgProcess, 1);
615
616 Story story = null;
617 File file = getFile(luid, pgGet);
618 pgGet.done();
619 try {
620 SupportType type = SupportType.valueOfAllOkUC(meta.getType());
621 URL url = file.toURI().toURL();
622 if (type != null) {
623 story = BasicSupport.getSupport(type, url) //
624 .process(pgProcess);
625
626 // Because we do not want to clear the meta cache:
627 meta.setCover(story.getMeta().getCover());
628 meta.setResume(story.getMeta().getResume());
629 story.setMeta(meta);
630 //
631 } else {
632 throw new IOException("Unknown type: " + meta.getType());
633 }
634 } catch (IOException e) {
635 // We should not have not-supported files in the
636 // library
637 Instance.getTraceHandler().error(
638 new IOException(String.format(
639 "Cannot load file of type '%s' from library: %s",
640 meta.getType(), file), e));
641 } finally {
642 pgProcess.done();
643 pg.done();
644 }
645
646 return story;
647 }
648
649 /**
650 * Import the {@link Story} at the given {@link URL} into the
651 * {@link BasicLibrary}.
652 *
653 * @param url
654 * the {@link URL} to import
655 * @param pg
656 * the optional progress reporter
657 *
658 * @return the imported {@link Story}
659 *
660 * @throws UnknownHostException
661 * if the host is not supported
662 * @throws IOException
663 * in case of I/O error
664 */
665 public Story imprt(URL url, Progress pg) throws IOException {
666 if (pg == null)
667 pg = new Progress();
668
669 pg.setMinMax(0, 1000);
670 Progress pgProcess = new Progress();
671 Progress pgSave = new Progress();
672 pg.addProgress(pgProcess, 800);
673 pg.addProgress(pgSave, 200);
674
675 BasicSupport support = BasicSupport.getSupport(url);
676 if (support == null) {
677 throw new UnknownHostException("" + url);
678 }
679
680 Story story = save(support.process(pgProcess), pgSave);
681 pg.done();
682
683 return story;
684 }
685
686 /**
687 * Import the story from one library to another, and keep the same LUID.
688 *
689 * @param other
690 * the other library to import from
691 * @param luid
692 * the Library UID
693 * @param pg
694 * the optional progress reporter
695 *
696 * @throws IOException
697 * in case of I/O error
698 */
699 public void imprt(BasicLibrary other, String luid, Progress pg)
700 throws IOException {
701 Progress pgGetStory = new Progress();
702 Progress pgSave = new Progress();
703 if (pg == null) {
704 pg = new Progress();
705 }
706
707 pg.setMinMax(0, 2);
708 pg.addProgress(pgGetStory, 1);
709 pg.addProgress(pgSave, 1);
710
711 Story story = other.getStory(luid, pgGetStory);
712 if (story != null) {
713 story = this.save(story, luid, pgSave);
714 pg.done();
715 } else {
716 pg.done();
717 throw new IOException("Cannot find story in Library: " + luid);
718 }
719 }
720
721 /**
722 * Export the {@link Story} to the given target in the given format.
723 *
724 * @param luid
725 * the {@link Story} ID
726 * @param type
727 * the {@link OutputType} to transform it to
728 * @param target
729 * the target to save to
730 * @param pg
731 * the optional progress reporter
732 *
733 * @return the saved resource (the main saved {@link File})
734 *
735 * @throws IOException
736 * in case of I/O error
737 */
738 public File export(String luid, OutputType type, String target, Progress pg)
739 throws IOException {
740 Progress pgGetStory = new Progress();
741 Progress pgOut = new Progress();
742 if (pg != null) {
743 pg.setMax(2);
744 pg.addProgress(pgGetStory, 1);
745 pg.addProgress(pgOut, 1);
746 }
747
748 BasicOutput out = BasicOutput.getOutput(type, false, false);
749 if (out == null) {
750 throw new IOException("Output type not supported: " + type);
751 }
752
753 Story story = getStory(luid, pgGetStory);
754 if (story == null) {
755 throw new IOException("Cannot find story to export: " + luid);
756 }
757
758 return out.process(story, target, pgOut);
759 }
760
761 /**
762 * Save a {@link Story} to the {@link BasicLibrary}.
763 *
764 * @param story
765 * the {@link Story} to save
766 * @param pg
767 * the optional progress reporter
768 *
769 * @return the same {@link Story}, whose LUID may have changed
770 *
771 * @throws IOException
772 * in case of I/O error
773 */
774 public Story save(Story story, Progress pg) throws IOException {
775 return save(story, null, pg);
776 }
777
778 /**
779 * Save a {@link Story} to the {@link BasicLibrary} -- the LUID <b>must</b>
780 * be correct, or NULL to get the next free one.
781 * <p>
782 * Will override any previous {@link Story} with the same LUID.
783 *
784 * @param story
785 * the {@link Story} to save
786 * @param luid
787 * the <b>correct</b> LUID or NULL to get the next free one
788 * @param pg
789 * the optional progress reporter
790 *
791 * @return the same {@link Story}, whose LUID may have changed
792 *
793 * @throws IOException
794 * in case of I/O error
795 */
796 public synchronized Story save(Story story, String luid, Progress pg)
797 throws IOException {
798
799 Instance.getTraceHandler().trace(
800 this.getClass().getSimpleName() + ": saving story " + luid);
801
802 // Do not change the original metadata, but change the original story
803 MetaData meta = story.getMeta().clone();
804 story.setMeta(meta);
805
806 if (luid == null || luid.isEmpty()) {
807 meta.setLuid(String.format("%03d", getNextId()));
808 } else {
809 meta.setLuid(luid);
810 }
811
812 if (luid != null && getInfo(luid) != null) {
813 delete(luid);
814 }
815
816 story = doSave(story, pg);
817
818 updateInfo(story.getMeta());
819
820 Instance.getTraceHandler().trace(
821 this.getClass().getSimpleName() + ": story saved (" + luid
822 + ")");
823
824 return story;
825 }
826
827 /**
828 * Delete the given {@link Story} from this {@link BasicLibrary}.
829 *
830 * @param luid
831 * the LUID of the target {@link Story}
832 *
833 * @throws IOException
834 * in case of I/O error
835 */
836 public synchronized void delete(String luid) throws IOException {
837 Instance.getTraceHandler().trace(
838 this.getClass().getSimpleName() + ": deleting story " + luid);
839
840 doDelete(luid);
841 invalidateInfo(luid);
842
843 Instance.getTraceHandler().trace(
844 this.getClass().getSimpleName() + ": story deleted (" + luid
845 + ")");
846 }
847
848 /**
849 * Change the type (source) of the given {@link Story}.
850 *
851 * @param luid
852 * the {@link Story} LUID
853 * @param newSource
854 * the new source
855 * @param pg
856 * the optional progress reporter
857 *
858 * @throws IOException
859 * in case of I/O error or if the {@link Story} was not found
860 */
861 public synchronized void changeSource(String luid, String newSource,
862 Progress pg) throws IOException {
863 MetaData meta = getInfo(luid);
864 if (meta == null) {
865 throw new IOException("Story not found: " + luid);
866 }
867
868 changeSTA(luid, newSource, meta.getTitle(), meta.getAuthor(), pg);
869 }
870
871 /**
872 * Change the title (name) of the given {@link Story}.
873 *
874 * @param luid
875 * the {@link Story} LUID
876 * @param newTitle
877 * the new title
878 * @param pg
879 * the optional progress reporter
880 *
881 * @throws IOException
882 * in case of I/O error or if the {@link Story} was not found
883 */
884 public synchronized void changeTitle(String luid, String newTitle,
885 Progress pg) throws IOException {
886 MetaData meta = getInfo(luid);
887 if (meta == null) {
888 throw new IOException("Story not found: " + luid);
889 }
890
891 changeSTA(luid, meta.getSource(), newTitle, meta.getAuthor(), pg);
892 }
893
894 /**
895 * Change the author of the given {@link Story}.
896 *
897 * @param luid
898 * the {@link Story} LUID
899 * @param newAuthor
900 * the new author
901 * @param pg
902 * the optional progress reporter
903 *
904 * @throws IOException
905 * in case of I/O error or if the {@link Story} was not found
906 */
907 public synchronized void changeAuthor(String luid, String newAuthor,
908 Progress pg) throws IOException {
909 MetaData meta = getInfo(luid);
910 if (meta == null) {
911 throw new IOException("Story not found: " + luid);
912 }
913
914 changeSTA(luid, meta.getSource(), meta.getTitle(), newAuthor, pg);
915 }
916
917 /**
918 * Change the Source, Title and Author of the {@link Story} in one single
919 * go.
920 *
921 * @param luid
922 * the {@link Story} LUID
923 * @param newSource
924 * the new source
925 * @param newTitle
926 * the new title
927 * @param newAuthor
928 * the new author
929 * @param pg
930 * the optional progress reporter
931 *
932 * @throws IOException
933 * in case of I/O error or if the {@link Story} was not found
934 */
935 protected synchronized void changeSTA(String luid, String newSource,
936 String newTitle, String newAuthor, Progress pg) throws IOException {
937 MetaData meta = getInfo(luid);
938 if (meta == null) {
939 throw new IOException("Story not found: " + luid);
940 }
941
942 meta.setSource(newSource);
943 meta.setTitle(newTitle);
944 meta.setAuthor(newAuthor);
945 saveMeta(meta, pg);
946
947 invalidateInfo(luid);
948 }
949
950 /**
951 * Save back the current state of the {@link MetaData} (LUID <b>MUST NOT</b>
952 * change) for this {@link Story}.
953 * <p>
954 * By default, delete the old {@link Story} then recreate a new
955 * {@link Story}.
956 * <p>
957 * Note that this behaviour can lead to data loss in case of problems!
958 *
959 * @param meta
960 * the new {@link MetaData} (LUID <b>MUST NOT</b> change)
961 * @param pg
962 * the optional {@link Progress}
963 *
964 * @throws IOException
965 * in case of I/O error or if the {@link Story} was not found
966 */
967 protected synchronized void saveMeta(MetaData meta, Progress pg)
968 throws IOException {
969 if (pg == null) {
970 pg = new Progress();
971 }
972
973 Progress pgGet = new Progress();
974 Progress pgSet = new Progress();
975 pg.addProgress(pgGet, 50);
976 pg.addProgress(pgSet, 50);
977
978 Story story = getStory(meta.getLuid(), pgGet);
979 if (story == null) {
980 throw new IOException("Story not found: " + meta.getLuid());
981 }
982
983 // TODO: this is not safe!
984 delete(meta.getLuid());
985 story.setMeta(meta);
986 save(story, meta.getLuid(), pgSet);
987
988 pg.done();
989 }
990 }