586c4ef17e11ff4d8c7eefa1cf6c689dac71ce59
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 and r/w. */
44 /** The library is ready, but read-only. */
46 /** The library is invalid (not correctly set up). */
48 /** You are not allowed to access this library. */
50 /** The library is currently out of commission. */
54 * The library is available (you can query it).
56 * It does <b>not</b> specify if it is read-only or not.
58 * @return TRUE if it is
60 public boolean isReady() {
61 return (this == READ_WRITE
|| this == READ_ONLY
);
65 * This library can be modified (= you are allowed to modify it).
67 * @return TRUE if it is
69 public boolean isWritable() {
70 return (this == READ_WRITE
);
75 * Return a name for this library (the UI may display this).
79 * @return the name, or an empty {@link String} if none
81 public String
getLibraryName() {
88 * @return the current status
90 public Status
getStatus() {
91 return Status
.READ_WRITE
;
95 * Retrieve the main {@link File} corresponding to the given {@link Story},
96 * which can be passed to an external reader or instance.
98 * Do <b>NOT</b> alter this file.
101 * the Library UID of the story, can be NULL
103 * the optional {@link Progress}
105 * @return the corresponding {@link Story}
107 * @throws IOException
108 * in case of IOException
110 public abstract File
getFile(String luid
, Progress pg
) throws IOException
;
113 * Return the cover image associated to this story.
116 * the Library UID of the story
118 * @return the cover image
120 * @throws IOException
121 * in case of IOException
123 public abstract Image
getCover(String luid
) throws IOException
;
125 // TODO: ensure it is the main used interface
126 public MetaResultList
getList(Progress pg
) throws IOException
{
127 return new MetaResultList(getMetas(pg
));
130 // TODO: make something for (normal and custom) not-story covers
133 * Return the cover image associated to this source.
135 * By default, return the custom cover if any, and if not, return the cover
136 * of the first story with this source.
141 * @return the cover image or NULL
143 * @throws IOException
144 * in case of IOException
146 public Image
getSourceCover(String source
) throws IOException
{
147 Image custom
= getCustomSourceCover(source
);
148 if (custom
!= null) {
152 List
<MetaData
> metas
= getList().filter(source
, null, null);
153 if (metas
.size() > 0) {
154 return getCover(metas
.get(0).getLuid());
161 * Return the cover image associated to this author.
163 * By default, return the custom cover if any, and if not, return the cover
164 * of the first story with this author.
169 * @return the cover image or NULL
171 * @throws IOException
172 * in case of IOException
174 public Image
getAuthorCover(String author
) throws IOException
{
175 Image custom
= getCustomAuthorCover(author
);
176 if (custom
!= null) {
180 List
<MetaData
> metas
= getList().filter(null, author
, null);
181 if (metas
.size() > 0) {
182 return getCover(metas
.get(0).getLuid());
189 * Return the custom cover image associated to this source.
191 * By default, return NULL.
194 * the source to look for
196 * @return the custom cover or NULL if none
198 * @throws IOException
199 * in case of IOException
201 @SuppressWarnings("unused")
202 public Image
getCustomSourceCover(String source
) throws IOException
{
207 * Return the custom cover image associated to this author.
209 * By default, return NULL.
212 * the author to look for
214 * @return the custom cover or NULL if none
216 * @throws IOException
217 * in case of IOException
219 @SuppressWarnings("unused")
220 public Image
getCustomAuthorCover(String author
) throws IOException
{
225 * Set the source cover to the given story cover.
228 * the source to change
232 * @throws IOException
233 * in case of IOException
235 public abstract void setSourceCover(String source
, String luid
)
239 * Set the author cover to the given story cover.
242 * the author to change
246 * @throws IOException
247 * in case of IOException
249 public abstract void setAuthorCover(String author
, String luid
)
253 * Return the list of stories (represented by their {@link MetaData}, which
254 * <b>MAY</b> not have the cover included).
256 * The returned list <b>MUST</b> be a copy, not the original one.
259 * the optional {@link Progress}
261 * @return the list (can be empty but not NULL)
263 * @throws IOException
264 * in case of IOException
266 protected abstract List
<MetaData
> getMetas(Progress pg
) throws IOException
;
269 * Invalidate the {@link Story} cache (when the content should be re-read
270 * because it was changed).
272 protected void invalidateInfo() {
273 invalidateInfo(null);
277 * Invalidate the {@link Story} cache (when the content is removed).
279 * All the cache can be deleted if NULL is passed as meta.
282 * the LUID of the {@link Story} to clear from the cache, or NULL
285 protected abstract void invalidateInfo(String luid
);
288 * Invalidate the {@link Story} cache (when the content has changed, but we
289 * already have it) with the new given meta.
292 * the {@link Story} to clear from the cache
294 * @throws IOException
295 * in case of IOException
297 protected abstract void updateInfo(MetaData meta
) throws IOException
;
300 * Return the next LUID that can be used.
302 * @return the next luid
304 protected abstract int getNextId();
307 * Delete the target {@link Story}.
310 * the LUID of the {@link Story}
312 * @throws IOException
313 * in case of I/O error or if the {@link Story} wa not found
315 protected abstract void doDelete(String luid
) throws IOException
;
318 * Actually save the story to the back-end.
321 * the {@link Story} to save
323 * the optional {@link Progress}
325 * @return the saved {@link Story} (which may have changed, especially
326 * regarding the {@link MetaData})
328 * @throws IOException
329 * in case of I/O error
331 protected abstract Story
doSave(Story story
, Progress pg
)
335 * Refresh the {@link BasicLibrary}, that is, make sure all metas are
339 * the optional progress reporter
341 public void refresh(Progress pg
) {
344 } catch (IOException e
) {
345 // We will let it fail later
350 * Check if the {@link Story} denoted by this Library UID is present in the
351 * cache (if we have no cache, we default to </t>true</tt>).
356 * @return TRUE if it is
358 public boolean isCached(String luid
) {
359 // By default, everything is cached
364 * Clear the {@link Story} from the cache, if needed.
366 * The next time we try to retrieve the {@link Story}, it may be required to
372 * @throws IOException
373 * in case of I/O error
375 public void clearFromCache(String luid
) throws IOException
{
376 // By default, this is a noop.
380 * List all the known types (sources) of stories.
382 * @return the sources
384 * @throws IOException
385 * in case of IOException
387 public List
<String
> getSources() throws IOException
{
388 List
<String
> list
= new ArrayList
<String
>();
389 for (MetaData meta
: getMetas(null)) {
390 String storySource
= meta
.getSource();
391 if (!list
.contains(storySource
)) {
392 list
.add(storySource
);
396 Collections
.sort(list
);
401 * List all the known types (sources) of stories, grouped by directory
402 * ("Source_1/a" and "Source_1/b" will be grouped into "Source_1").
404 * Note that an empty item in the list means a non-grouped source (type) --
405 * e.g., you could have for Source_1:
407 * <li><tt></tt>: empty, so source is "Source_1"</li>
408 * <li><tt>a</tt>: empty, so source is "Source_1/a"</li>
409 * <li><tt>b</tt>: empty, so source is "Source_1/b"</li>
412 * @return the grouped list
414 * @throws IOException
415 * in case of IOException
417 public Map
<String
, List
<String
>> getSourcesGrouped() throws IOException
{
418 Map
<String
, List
<String
>> map
= new TreeMap
<String
, List
<String
>>();
419 for (String source
: getSources()) {
423 int pos
= source
.indexOf('/');
424 if (pos
> 0 && pos
< source
.length() - 1) {
425 name
= source
.substring(0, pos
);
426 subname
= source
.substring(pos
+ 1);
433 List
<String
> list
= map
.get(name
);
435 list
= new ArrayList
<String
>();
445 * List all the known authors of stories.
447 * @return the authors
449 * @throws IOException
450 * in case of IOException
452 public List
<String
> getAuthors() throws IOException
{
453 List
<String
> list
= new ArrayList
<String
>();
454 for (MetaData meta
: getMetas(null)) {
455 String storyAuthor
= meta
.getAuthor();
456 if (!list
.contains(storyAuthor
)) {
457 list
.add(storyAuthor
);
461 Collections
.sort(list
);
466 * Return the list of authors, grouped by starting letter(s) if needed.
468 * If the number of author is not too high, only one group with an empty
469 * name and all the authors will be returned.
471 * If not, the authors will be separated into groups:
473 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
475 * <li><tt>0-9</tt>: any authors whose name starts with a number</li>
476 * <li><tt>A-C</tt> (for instance): any author whose name starts with
477 * <tt>A</tt>, <tt>B</tt> or <tt>C</tt></li>
479 * Note that the letters used in the groups can vary (except <tt>*</tt> and
480 * <tt>0-9</tt>, which may only be present or not).
482 * @return the authors' names, grouped by letter(s)
484 * @throws IOException
485 * in case of IOException
487 public Map
<String
, List
<String
>> getAuthorsGrouped() throws IOException
{
490 Map
<String
, List
<String
>> groups
= new TreeMap
<String
, List
<String
>>();
491 List
<String
> authors
= getAuthors();
493 // If all authors fit the max, just report them as is
494 if (authors
.size() <= MAX
) {
495 groups
.put("", authors
);
499 // Create groups A to Z, which can be empty here
500 for (char car
= 'A'; car
<= 'Z'; car
++) {
501 groups
.put(Character
.toString(car
), getAuthorsGroup(authors
, car
));
505 List
<String
> keys
= new ArrayList
<String
>(groups
.keySet());
506 for (int i
= 0; i
+ 1 < keys
.size(); i
++) {
507 String keyNow
= keys
.get(i
);
508 String keyNext
= keys
.get(i
+ 1);
510 List
<String
> now
= groups
.get(keyNow
);
511 List
<String
> next
= groups
.get(keyNext
);
513 int currentTotal
= now
.size() + next
.size();
514 if (currentTotal
<= MAX
) {
515 String key
= keyNow
.charAt(0) + "-"
516 + keyNext
.charAt(keyNext
.length() - 1);
518 List
<String
> all
= new ArrayList
<String
>();
522 groups
.remove(keyNow
);
523 groups
.remove(keyNext
);
524 groups
.put(key
, all
);
526 keys
.set(i
, key
); // set the new key instead of key(i)
527 keys
.remove(i
+ 1); // remove the next, consumed key
528 i
--; // restart at key(i)
532 // Add "special" groups
533 groups
.put("*", getAuthorsGroup(authors
, '*'));
534 groups
.put("0-9", getAuthorsGroup(authors
, '0'));
536 // Prune empty groups
537 keys
= new ArrayList
<String
>(groups
.keySet());
538 for (String key
: keys
) {
539 if (groups
.get(key
).isEmpty()) {
548 * Get all the authors that start with the given character:
550 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
552 * <li><tt>0</tt>: any authors whose name starts with a number</li>
553 * <li><tt>A</tt> (any capital latin letter): any author whose name starts
554 * with <tt>A</tt></li>
558 * the full list of authors
560 * the starting character, <tt>*</tt>, <tt>0</tt> or a capital
563 * @return the authors that fulfil the starting letter
565 private List
<String
> getAuthorsGroup(List
<String
> authors
, char car
) {
566 List
<String
> accepted
= new ArrayList
<String
>();
567 for (String author
: authors
) {
569 for (int i
= 0; first
== '*' && i
< author
.length(); i
++) {
570 String san
= StringUtils
.sanitize(author
, true, true);
571 char c
= san
.charAt(i
);
572 if (c
>= '0' && c
<= '9') {
574 } else if (c
>= 'a' && c
<= 'z') {
575 first
= (char) (c
- 'a' + 'A');
576 } else if (c
>= 'A' && c
<= 'Z') {
582 accepted
.add(author
);
590 * List all the stories in the {@link BasicLibrary}.
592 * Cover images <b>MAYBE</b> not included.
594 * @return the stories
596 * @throws IOException
597 * in case of IOException
599 public MetaResultList
getList() throws IOException
{
600 return getList(null);
604 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
605 * cover image <b>MAY</b> not be included.
608 * the Library UID of the story, can be NULL
610 * @return the corresponding {@link Story} or NULL if not found
612 * @throws IOException
613 * in case of IOException
615 public MetaData
getInfo(String luid
) throws IOException
{
617 for (MetaData meta
: getMetas(null)) {
618 if (luid
.equals(meta
.getLuid())) {
628 * Retrieve a specific {@link Story}.
631 * the Library UID of the story
633 * the optional progress reporter
635 * @return the corresponding {@link Story} or NULL if not found
637 * @throws IOException
638 * in case of IOException
640 public synchronized Story
getStory(String luid
, Progress pg
)
642 Progress pgMetas
= new Progress();
643 Progress pgStory
= new Progress();
645 pg
.setMinMax(0, 100);
646 pg
.addProgress(pgMetas
, 10);
647 pg
.addProgress(pgStory
, 90);
650 MetaData meta
= null;
651 for (MetaData oneMeta
: getMetas(pgMetas
)) {
652 if (oneMeta
.getLuid().equals(luid
)) {
660 Story story
= getStory(luid
, meta
, pgStory
);
667 * Retrieve a specific {@link Story}.
670 * the LUID of the story
672 * the meta of the story
674 * the optional progress reporter
676 * @return the corresponding {@link Story} or NULL if not found
678 * @throws IOException
679 * in case of IOException
681 public synchronized Story
getStory(String luid
, MetaData meta
, Progress pg
)
688 Progress pgGet
= new Progress();
689 Progress pgProcess
= new Progress();
692 pg
.addProgress(pgGet
, 1);
693 pg
.addProgress(pgProcess
, 1);
698 if (luid
!= null && meta
!= null) {
699 file
= getFile(luid
, pgGet
);
705 SupportType type
= SupportType
.valueOfAllOkUC(meta
.getType());
707 throw new IOException("Unknown type: " + meta
.getType());
710 URL url
= file
.toURI().toURL();
711 story
= BasicSupport
.getSupport(type
, url
) //
714 // Because we do not want to clear the meta cache:
715 meta
.setCover(story
.getMeta().getCover());
716 meta
.setResume(story
.getMeta().getResume());
719 } catch (IOException e
) {
720 // We should not have not-supported files in the library
721 Instance
.getInstance().getTraceHandler()
722 .error(new IOException(String
.format(
723 "Cannot load file of type '%s' from library: %s",
724 meta
.getType(), file
), e
));
734 * Import the {@link Story} at the given {@link URL} into the
735 * {@link BasicLibrary}.
738 * the {@link URL} to import
740 * the optional progress reporter
742 * @return the imported Story {@link MetaData}
744 * @throws UnknownHostException
745 * if the host is not supported
746 * @throws IOException
747 * in case of I/O error
749 public MetaData
imprt(URL url
, Progress pg
) throws IOException
{
753 pg
.setMinMax(0, 1000);
754 Progress pgProcess
= new Progress();
755 Progress pgSave
= new Progress();
756 pg
.addProgress(pgProcess
, 800);
757 pg
.addProgress(pgSave
, 200);
759 BasicSupport support
= BasicSupport
.getSupport(url
);
760 if (support
== null) {
761 throw new UnknownHostException("" + url
);
764 Story story
= save(support
.process(pgProcess
), pgSave
);
765 pg
.setName(story
.getMeta().getTitle());
768 return story
.getMeta();
772 * Import the story from one library to another, and keep the same LUID.
775 * the other library to import from
779 * the optional progress reporter
781 * @throws IOException
782 * in case of I/O error
784 public void imprt(BasicLibrary other
, String luid
, Progress pg
)
786 Progress pgGetStory
= new Progress();
787 Progress pgSave
= new Progress();
793 pg
.addProgress(pgGetStory
, 1);
794 pg
.addProgress(pgSave
, 1);
796 Story story
= other
.getStory(luid
, pgGetStory
);
798 story
= this.save(story
, luid
, pgSave
);
802 throw new IOException("Cannot find story in Library: " + luid
);
807 * Export the {@link Story} to the given target in the given format.
810 * the {@link Story} ID
812 * the {@link OutputType} to transform it to
814 * the target to save to
816 * the optional progress reporter
818 * @return the saved resource (the main saved {@link File})
820 * @throws IOException
821 * in case of I/O error
823 public File
export(String luid
, OutputType type
, String target
, Progress pg
)
825 Progress pgGetStory
= new Progress();
826 Progress pgOut
= new Progress();
829 pg
.addProgress(pgGetStory
, 1);
830 pg
.addProgress(pgOut
, 1);
833 BasicOutput out
= BasicOutput
.getOutput(type
, false, false);
835 throw new IOException("Output type not supported: " + type
);
838 Story story
= getStory(luid
, pgGetStory
);
840 throw new IOException("Cannot find story to export: " + luid
);
843 return out
.process(story
, target
, pgOut
);
847 * Save a {@link Story} to the {@link BasicLibrary}.
850 * the {@link Story} to save
852 * the optional progress reporter
854 * @return the same {@link Story}, whose LUID may have changed
856 * @throws IOException
857 * in case of I/O error
859 public Story
save(Story story
, Progress pg
) throws IOException
{
860 return save(story
, null, pg
);
864 * Save a {@link Story} to the {@link BasicLibrary} -- the LUID <b>must</b>
865 * be correct, or NULL to get the next free one.
867 * Will override any previous {@link Story} with the same LUID.
870 * the {@link Story} to save
872 * the <b>correct</b> LUID or NULL to get the next free one
874 * the optional progress reporter
876 * @return the same {@link Story}, whose LUID may have changed
878 * @throws IOException
879 * in case of I/O error
881 public synchronized Story
save(Story story
, String luid
, Progress pg
)
887 Instance
.getInstance().getTraceHandler().trace(
888 this.getClass().getSimpleName() + ": saving story " + luid
);
890 // Do not change the original metadata, but change the original story
891 MetaData meta
= story
.getMeta().clone();
894 pg
.setName("Saving story");
896 if (luid
== null || luid
.isEmpty()) {
897 meta
.setLuid(String
.format("%03d", getNextId()));
902 if (luid
!= null && getInfo(luid
) != null) {
906 story
= doSave(story
, pg
);
908 updateInfo(story
.getMeta());
910 Instance
.getInstance().getTraceHandler()
911 .trace(this.getClass().getSimpleName() + ": story saved ("
914 pg
.setName(meta
.getTitle());
920 * Delete the given {@link Story} from this {@link BasicLibrary}.
923 * the LUID of the target {@link Story}
925 * @throws IOException
926 * in case of I/O error
928 public synchronized void delete(String luid
) throws IOException
{
929 Instance
.getInstance().getTraceHandler().trace(
930 this.getClass().getSimpleName() + ": deleting story " + luid
);
933 invalidateInfo(luid
);
935 Instance
.getInstance().getTraceHandler()
936 .trace(this.getClass().getSimpleName() + ": story deleted ("
941 * Change the type (source) of the given {@link Story}.
944 * the {@link Story} LUID
948 * the optional progress reporter
950 * @throws IOException
951 * in case of I/O error or if the {@link Story} was not found
953 public synchronized void changeSource(String luid
, String newSource
,
954 Progress pg
) throws IOException
{
955 MetaData meta
= getInfo(luid
);
957 throw new IOException("Story not found: " + luid
);
960 changeSTA(luid
, newSource
, meta
.getTitle(), meta
.getAuthor(), pg
);
964 * Change the title (name) of the given {@link Story}.
967 * the {@link Story} LUID
971 * the optional progress reporter
973 * @throws IOException
974 * in case of I/O error or if the {@link Story} was not found
976 public synchronized void changeTitle(String luid
, String newTitle
,
977 Progress pg
) throws IOException
{
978 MetaData meta
= getInfo(luid
);
980 throw new IOException("Story not found: " + luid
);
983 changeSTA(luid
, meta
.getSource(), newTitle
, meta
.getAuthor(), pg
);
987 * Change the author of the given {@link Story}.
990 * the {@link Story} LUID
994 * the optional progress reporter
996 * @throws IOException
997 * in case of I/O error or if the {@link Story} was not found
999 public synchronized void changeAuthor(String luid
, String newAuthor
,
1000 Progress pg
) throws IOException
{
1001 MetaData meta
= getInfo(luid
);
1003 throw new IOException("Story not found: " + luid
);
1006 changeSTA(luid
, meta
.getSource(), meta
.getTitle(), newAuthor
, pg
);
1010 * Change the Source, Title and Author of the {@link Story} in one single
1014 * the {@link Story} LUID
1022 * the optional progress reporter
1024 * @throws IOException
1025 * in case of I/O error or if the {@link Story} was not found
1027 protected synchronized void changeSTA(String luid
, String newSource
,
1028 String newTitle
, String newAuthor
, Progress pg
) throws IOException
{
1029 MetaData meta
= getInfo(luid
);
1031 throw new IOException("Story not found: " + luid
);
1034 meta
.setSource(newSource
);
1035 meta
.setTitle(newTitle
);
1036 meta
.setAuthor(newAuthor
);
1041 * Save back the current state of the {@link MetaData} (LUID <b>MUST NOT</b>
1042 * change) for this {@link Story}.
1044 * By default, delete the old {@link Story} then recreate a new
1047 * Note that this behaviour can lead to data loss in case of problems!
1050 * the new {@link MetaData} (LUID <b>MUST NOT</b> change)
1052 * the optional {@link Progress}
1054 * @throws IOException
1055 * in case of I/O error or if the {@link Story} was not found
1057 protected synchronized void saveMeta(MetaData meta
, Progress pg
)
1058 throws IOException
{
1060 pg
= new Progress();
1063 Progress pgGet
= new Progress();
1064 Progress pgSet
= new Progress();
1065 pg
.addProgress(pgGet
, 50);
1066 pg
.addProgress(pgSet
, 50);
1068 Story story
= getStory(meta
.getLuid(), pgGet
);
1069 if (story
== null) {
1070 throw new IOException("Story not found: " + meta
.getLuid());
1073 // TODO: this is not safe!
1074 delete(meta
.getLuid());
1075 story
.setMeta(meta
);
1076 save(story
, meta
.getLuid(), pgSet
);