1 package be
.nikiroo
.fanfix
.library
;
4 import java
.io
.IOException
;
6 import java
.net
.UnknownHostException
;
7 import java
.util
.ArrayList
;
8 import java
.util
.Collections
;
11 import java
.util
.TreeMap
;
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
;
25 * Manage a library of Stories: import, export, list, modify.
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}.
30 * Most of the {@link BasicLibrary} functions work on a partial (cover
31 * <b>MAY</b> not be included) {@link MetaData} object.
35 abstract public class BasicLibrary
{
37 * A {@link BasicLibrary} status.
42 /** The library is ready. */
44 /** The library is invalid (not correctly set up). */
46 /** You are not allowed to access this library. */
48 /** The library is currently out of commission. */
53 * Return a name for this library (the UI may display this).
57 * @return the name, or an empty {@link String} if none
59 public String
getLibraryName() {
66 * @return the current status
68 public Status
getStatus() {
73 * Retrieve the main {@link File} corresponding to the given {@link Story},
74 * which can be passed to an external reader or instance.
76 * Do <b>NOT</b> alter this file.
79 * the Library UID of the story
81 * the optional {@link Progress}
83 * @return the corresponding {@link Story}
85 public abstract File
getFile(String luid
, Progress pg
);
88 * Return the cover image associated to this story.
91 * the Library UID of the story
93 * @return the cover image
95 public abstract Image
getCover(String luid
);
98 * Return the cover image associated to this source.
100 * By default, return the custom cover if any, and if not, return the cover
101 * of the first story with this source.
106 * @return the cover image or NULL
108 public Image
getSourceCover(String source
) {
109 Image custom
= getCustomSourceCover(source
);
110 if (custom
!= null) {
114 List
<MetaData
> metas
= getListBySource(source
);
115 if (metas
.size() > 0) {
116 return getCover(metas
.get(0).getLuid());
123 * Return the cover image associated to this author.
125 * By default, return the custom cover if any, and if not, return the cover
126 * of the first story with this author.
131 * @return the cover image or NULL
133 public Image
getAuthorCover(String author
) {
134 Image custom
= getCustomAuthorCover(author
);
135 if (custom
!= null) {
139 List
<MetaData
> metas
= getListByAuthor(author
);
140 if (metas
.size() > 0) {
141 return getCover(metas
.get(0).getLuid());
148 * Return the custom cover image associated to this source.
150 * By default, return NULL.
153 * the source to look for
155 * @return the custom cover or NULL if none
157 public Image
getCustomSourceCover(@SuppressWarnings("unused") String source
) {
162 * Return the custom cover image associated to this author.
164 * By default, return NULL.
167 * the author to look for
169 * @return the custom cover or NULL if none
171 public Image
getCustomAuthorCover(@SuppressWarnings("unused") String author
) {
176 * Set the source cover to the given story cover.
179 * the source to change
183 public abstract void setSourceCover(String source
, String luid
);
186 * Set the author cover to the given story cover.
189 * the author to change
193 public abstract void setAuthorCover(String author
, String luid
);
196 * Return the list of stories (represented by their {@link MetaData}, which
197 * <b>MAY</b> not have the cover included).
200 * the optional {@link Progress}
202 * @return the list (can be empty but not NULL)
204 protected abstract List
<MetaData
> getMetas(Progress pg
);
207 * Invalidate the {@link Story} cache (when the content should be re-read
208 * because it was changed).
210 protected void invalidateInfo() {
211 invalidateInfo(null);
215 * Invalidate the {@link Story} cache (when the content is removed).
217 * All the cache can be deleted if NULL is passed as meta.
220 * the LUID of the {@link Story} to clear from the cache, or NULL
223 protected abstract void invalidateInfo(String luid
);
226 * Invalidate the {@link Story} cache (when the content has changed, but we
227 * already have it) with the new given meta.
230 * the {@link Story} to clear from the cache
232 protected abstract void updateInfo(MetaData meta
);
235 * Return the next LUID that can be used.
237 * @return the next luid
239 protected abstract int getNextId();
242 * Delete the target {@link Story}.
245 * the LUID of the {@link Story}
247 * @throws IOException
248 * in case of I/O error or if the {@link Story} wa not found
250 protected abstract void doDelete(String luid
) throws IOException
;
253 * Actually save the story to the back-end.
256 * the {@link Story} to save
258 * the optional {@link Progress}
260 * @return the saved {@link Story} (which may have changed, especially
261 * regarding the {@link MetaData})
263 * @throws IOException
264 * in case of I/O error
266 protected abstract Story
doSave(Story story
, Progress pg
)
270 * Refresh the {@link BasicLibrary}, that is, make sure all metas are
274 * the optional progress reporter
276 public void refresh(Progress pg
) {
281 * List all the known types (sources) of stories.
283 * @return the sources
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
);
294 Collections
.sort(list
);
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").
302 * Note that an empty item in the list means a non-grouped source (type) --
303 * e.g., you could have for Source_1:
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>
310 * @return the grouped list
312 public synchronized Map
<String
, List
<String
>> getSourcesGrouped() {
313 Map
<String
, List
<String
>> map
= new TreeMap
<String
, List
<String
>>();
314 for (String source
: getSources()) {
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);
328 List
<String
> list
= map
.get(name
);
330 list
= new ArrayList
<String
>();
340 * List all the known authors of stories.
342 * @return the authors
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
);
353 Collections
.sort(list
);
358 * Return the list of authors, grouped by starting letter(s) if needed.
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.
363 * If not, the authors will be separated into groups:
365 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
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>
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).
374 * @return the authors' names, grouped by letter(s)
376 public Map
<String
, List
<String
>> getAuthorsGrouped() {
379 Map
<String
, List
<String
>> groups
= new TreeMap
<String
, List
<String
>>();
380 List
<String
> authors
= getAuthors();
382 // If all authors fit the max, just report them as is
383 if (authors
.size() <= MAX
) {
384 groups
.put("", authors
);
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
));
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);
399 List
<String
> now
= groups
.get(keyNow
);
400 List
<String
> next
= groups
.get(keyNext
);
402 int currentTotal
= now
.size() + next
.size();
403 if (currentTotal
<= MAX
) {
404 String key
= keyNow
.charAt(0) + "-"
405 + keyNext
.charAt(keyNext
.length() - 1);
407 List
<String
> all
= new ArrayList
<String
>();
411 groups
.remove(keyNow
);
412 groups
.remove(keyNext
);
413 groups
.put(key
, all
);
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)
421 // Add "special" groups
422 groups
.put("*", getAuthorsGroup(authors
, '*'));
423 groups
.put("0-9", getAuthorsGroup(authors
, '0'));
425 // Prune empty groups
426 keys
= new ArrayList
<String
>(groups
.keySet());
427 for (String key
: keys
) {
428 if (groups
.get(key
).isEmpty()) {
437 * Get all the authors that start with the given character:
439 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
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>
447 * the full list of authors
449 * the starting character, <tt>*</tt>, <tt>0</tt> or a capital
451 * @return the authors that fulfill the starting letter
453 private List
<String
> getAuthorsGroup(List
<String
> authors
, char car
) {
454 List
<String
> accepted
= new ArrayList
<String
>();
455 for (String author
: authors
) {
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') {
462 } else if (c
>= 'a' && c
<= 'z') {
463 first
= (char) (c
- 'a' + 'A');
464 } else if (c
>= 'A' && c
<= 'Z') {
470 accepted
.add(author
);
478 * List all the stories in the {@link BasicLibrary}.
480 * Cover images <b>MAYBE</b> not included.
482 * @return the stories
484 public synchronized List
<MetaData
> getList() {
485 return getMetas(null);
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.
492 * Cover images not included.
495 * the type of story to retrieve, or NULL for all
497 * @return the stories
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
)) {
508 Collections
.sort(list
);
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.
516 * Cover images not included.
519 * the author of the stories to retrieve, or NULL for all
521 * @return the stories
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
)) {
532 Collections
.sort(list
);
537 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
538 * cover image <b>MAY</b> not be included.
541 * the Library UID of the story
543 * @return the corresponding {@link Story}
545 public synchronized MetaData
getInfo(String luid
) {
547 for (MetaData meta
: getMetas(null)) {
548 if (luid
.equals(meta
.getLuid())) {
558 * Retrieve a specific {@link Story}.
561 * the Library UID of the story
563 * the optional progress reporter
565 * @return the corresponding {@link Story} or NULL if not found
567 public synchronized Story
getStory(String luid
, Progress pg
) {
568 Progress pgMetas
= new Progress();
569 Progress pgStory
= new Progress();
571 pg
.setMinMax(0, 100);
572 pg
.addProgress(pgMetas
, 10);
573 pg
.addProgress(pgStory
, 90);
576 MetaData meta
= null;
577 for (MetaData oneMeta
: getMetas(pgMetas
)) {
578 if (oneMeta
.getLuid().equals(luid
)) {
586 Story story
= getStory(luid
, meta
, pgStory
);
593 * Retrieve a specific {@link Story}.
596 * the meta of the story
598 * the optional progress reporter
600 * @return the corresponding {@link Story} or NULL if not found
602 public synchronized Story
getStory(String luid
,
603 @SuppressWarnings("javadoc") MetaData meta
, Progress pg
) {
609 Progress pgGet
= new Progress();
610 Progress pgProcess
= new Progress();
613 pg
.addProgress(pgGet
, 1);
614 pg
.addProgress(pgProcess
, 1);
617 File file
= getFile(luid
, pgGet
);
620 SupportType type
= SupportType
.valueOfAllOkUC(meta
.getType());
621 URL url
= file
.toURI().toURL();
623 story
= BasicSupport
.getSupport(type
, url
) //
626 // Because we do not want to clear the meta cache:
627 meta
.setCover(story
.getMeta().getCover());
628 meta
.setResume(story
.getMeta().getResume());
632 throw new IOException("Unknown type: " + meta
.getType());
634 } catch (IOException e
) {
635 // We should not have not-supported files in the
637 Instance
.getTraceHandler().error(
638 new IOException(String
.format(
639 "Cannot load file of type '%s' from library: %s",
640 meta
.getType(), file
), e
));
650 * Import the {@link Story} at the given {@link URL} into the
651 * {@link BasicLibrary}.
654 * the {@link URL} to import
656 * the optional progress reporter
658 * @return the imported {@link Story}
660 * @throws UnknownHostException
661 * if the host is not supported
662 * @throws IOException
663 * in case of I/O error
665 public Story
imprt(URL url
, Progress pg
) throws IOException
{
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);
675 BasicSupport support
= BasicSupport
.getSupport(url
);
676 if (support
== null) {
677 throw new UnknownHostException("" + url
);
680 Story story
= save(support
.process(pgProcess
), pgSave
);
687 * Import the story from one library to another, and keep the same LUID.
690 * the other library to import from
694 * the optional progress reporter
696 * @throws IOException
697 * in case of I/O error
699 public void imprt(BasicLibrary other
, String luid
, Progress pg
)
701 Progress pgGetStory
= new Progress();
702 Progress pgSave
= new Progress();
708 pg
.addProgress(pgGetStory
, 1);
709 pg
.addProgress(pgSave
, 1);
711 Story story
= other
.getStory(luid
, pgGetStory
);
713 story
= this.save(story
, luid
, pgSave
);
717 throw new IOException("Cannot find story in Library: " + luid
);
722 * Export the {@link Story} to the given target in the given format.
725 * the {@link Story} ID
727 * the {@link OutputType} to transform it to
729 * the target to save to
731 * the optional progress reporter
733 * @return the saved resource (the main saved {@link File})
735 * @throws IOException
736 * in case of I/O error
738 public File
export(String luid
, OutputType type
, String target
, Progress pg
)
740 Progress pgGetStory
= new Progress();
741 Progress pgOut
= new Progress();
744 pg
.addProgress(pgGetStory
, 1);
745 pg
.addProgress(pgOut
, 1);
748 BasicOutput out
= BasicOutput
.getOutput(type
, false, false);
750 throw new IOException("Output type not supported: " + type
);
753 Story story
= getStory(luid
, pgGetStory
);
755 throw new IOException("Cannot find story to export: " + luid
);
758 return out
.process(story
, target
, pgOut
);
762 * Save a {@link Story} to the {@link BasicLibrary}.
765 * the {@link Story} to save
767 * the optional progress reporter
769 * @return the same {@link Story}, whose LUID may have changed
771 * @throws IOException
772 * in case of I/O error
774 public Story
save(Story story
, Progress pg
) throws IOException
{
775 return save(story
, null, pg
);
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.
782 * Will override any previous {@link Story} with the same LUID.
785 * the {@link Story} to save
787 * the <b>correct</b> LUID or NULL to get the next free one
789 * the optional progress reporter
791 * @return the same {@link Story}, whose LUID may have changed
793 * @throws IOException
794 * in case of I/O error
796 public synchronized Story
save(Story story
, String luid
, Progress pg
)
799 Instance
.getTraceHandler().trace(
800 this.getClass().getSimpleName() + ": saving story " + luid
);
802 // Do not change the original metadata, but change the original story
803 MetaData meta
= story
.getMeta().clone();
806 if (luid
== null || luid
.isEmpty()) {
807 meta
.setLuid(String
.format("%03d", getNextId()));
812 if (luid
!= null && getInfo(luid
) != null) {
816 story
= doSave(story
, pg
);
818 updateInfo(story
.getMeta());
820 Instance
.getTraceHandler().trace(
821 this.getClass().getSimpleName() + ": story saved (" + luid
828 * Delete the given {@link Story} from this {@link BasicLibrary}.
831 * the LUID of the target {@link Story}
833 * @throws IOException
834 * in case of I/O error
836 public synchronized void delete(String luid
) throws IOException
{
837 Instance
.getTraceHandler().trace(
838 this.getClass().getSimpleName() + ": deleting story " + luid
);
841 invalidateInfo(luid
);
843 Instance
.getTraceHandler().trace(
844 this.getClass().getSimpleName() + ": story deleted (" + luid
849 * Change the type (source) of the given {@link Story}.
852 * the {@link Story} LUID
856 * the optional progress reporter
858 * @throws IOException
859 * in case of I/O error or if the {@link Story} was not found
861 public synchronized void changeSource(String luid
, String newSource
,
862 Progress pg
) throws IOException
{
863 MetaData meta
= getInfo(luid
);
865 throw new IOException("Story not found: " + luid
);
868 changeSTA(luid
, newSource
, meta
.getTitle(), meta
.getAuthor(), pg
);
872 * Change the title (name) of the given {@link Story}.
875 * the {@link Story} LUID
879 * the optional progress reporter
881 * @throws IOException
882 * in case of I/O error or if the {@link Story} was not found
884 public synchronized void changeTitle(String luid
, String newTitle
,
885 Progress pg
) throws IOException
{
886 MetaData meta
= getInfo(luid
);
888 throw new IOException("Story not found: " + luid
);
891 changeSTA(luid
, meta
.getSource(), newTitle
, meta
.getAuthor(), pg
);
895 * Change the author of the given {@link Story}.
898 * the {@link Story} LUID
902 * the optional progress reporter
904 * @throws IOException
905 * in case of I/O error or if the {@link Story} was not found
907 public synchronized void changeAuthor(String luid
, String newAuthor
,
908 Progress pg
) throws IOException
{
909 MetaData meta
= getInfo(luid
);
911 throw new IOException("Story not found: " + luid
);
914 changeSTA(luid
, meta
.getSource(), meta
.getTitle(), newAuthor
, pg
);
918 * Change the Source, Title and Author of the {@link Story} in one single
922 * the {@link Story} LUID
930 * the optional progress reporter
932 * @throws IOException
933 * in case of I/O error or if the {@link Story} was not found
935 protected synchronized void changeSTA(String luid
, String newSource
,
936 String newTitle
, String newAuthor
, Progress pg
) throws IOException
{
937 MetaData meta
= getInfo(luid
);
939 throw new IOException("Story not found: " + luid
);
942 meta
.setSource(newSource
);
943 meta
.setTitle(newTitle
);
944 meta
.setAuthor(newAuthor
);
949 * Save back the current state of the {@link MetaData} (LUID <b>MUST NOT</b>
950 * change) for this {@link Story}.
952 * By default, delete the old {@link Story} then recreate a new
955 * Note that this behaviour can lead to data loss in case of problems!
958 * the new {@link MetaData} (LUID <b>MUST NOT</b> change)
960 * the optional {@link Progress}
962 * @throws IOException
963 * in case of I/O error or if the {@link Story} was not found
965 protected synchronized void saveMeta(MetaData meta
, Progress pg
)
971 Progress pgGet
= new Progress();
972 Progress pgSet
= new Progress();
973 pg
.addProgress(pgGet
, 50);
974 pg
.addProgress(pgSet
, 50);
976 Story story
= getStory(meta
.getLuid(), pgGet
);
978 throw new IOException("Story not found: " + meta
.getLuid());
981 // TODO: this is not safe!
982 delete(meta
.getLuid());
984 save(story
, meta
.getLuid(), pgSet
);