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