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