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