1 package be
.nikiroo
.fanfix
.library
;
4 import java
.io
.IOException
;
6 import java
.net
.UnknownHostException
;
7 import java
.util
.LinkedHashMap
;
11 import be
.nikiroo
.fanfix
.Instance
;
12 import be
.nikiroo
.fanfix
.data
.MetaData
;
13 import be
.nikiroo
.fanfix
.data
.Story
;
14 import be
.nikiroo
.fanfix
.output
.BasicOutput
;
15 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
16 import be
.nikiroo
.fanfix
.supported
.BasicSupport
;
17 import be
.nikiroo
.fanfix
.supported
.SupportType
;
18 import be
.nikiroo
.utils
.Image
;
19 import be
.nikiroo
.utils
.Progress
;
20 import be
.nikiroo
.utils
.StringUtils
;
23 * Manage a library of Stories: import, export, list, modify.
25 * Each {@link Story} object will be associated with a (local to the library)
26 * unique ID, the LUID, which will be used to identify the {@link Story}.
28 * Most of the {@link BasicLibrary} functions work on a partial (cover
29 * <b>MAY</b> not be included) {@link MetaData} object.
33 abstract public class BasicLibrary
{
35 * A {@link BasicLibrary} status.
40 /** The library is ready and r/w. */
42 /** The library is ready, but read-only. */
44 /** You are not allowed to access this library. */
46 /** The library is currently out of commission. */
50 * The library is available (you can query it).
52 * It does <b>not</b> specify if it is read-only or not.
54 * @return TRUE if it is
56 public boolean isReady() {
57 return (this == READ_WRITE
|| this == READ_ONLY
);
61 * This library can be modified (= you are allowed to modify it).
63 * @return TRUE if it is
65 public boolean isWritable() {
66 return (this == READ_WRITE
);
71 * Return a name for this library (the UI may display this).
75 * @return the name, or an empty {@link String} if none
77 public String
getLibraryName() {
84 * @return the current status
86 public Status
getStatus() {
87 return Status
.READ_WRITE
;
91 * Retrieve the main {@link File} corresponding to the given {@link Story},
92 * which can be passed to an external reader or instance.
94 * Do <b>NOT</b> alter this file.
97 * the Library UID of the story, can be NULL
99 * the optional {@link Progress}
101 * @return the corresponding {@link Story}
103 * @throws IOException
104 * in case of IOException
106 public abstract File
getFile(String luid
, Progress pg
) throws IOException
;
109 * Return the cover image associated to this story.
112 * the Library UID of the story
114 * @return the cover image
116 * @throws IOException
117 * in case of IOException
119 public abstract Image
getCover(String luid
) throws IOException
;
122 * Retrieve the list of {@link MetaData} known by this {@link BasicLibrary}
123 * in a easy-to-filter version.
126 * the optional {@link Progress}
127 * @return the list of {@link MetaData} as a {@link MetaResultList} you can
129 * @throws IOException
130 * in case of I/O eror
132 public MetaResultList
getList(Progress pg
) throws IOException
{
133 // TODO: ensure it is the main used interface
135 return new MetaResultList(getMetas(pg
));
138 // TODO: make something for (normal and custom) non-story covers
141 * Return the cover image associated to this source.
143 * By default, return the custom cover if any, and if not, return the cover
144 * of the first story with this source.
149 * @return the cover image or NULL
151 * @throws IOException
152 * in case of IOException
154 public Image
getSourceCover(String source
) throws IOException
{
155 Image custom
= getCustomSourceCover(source
);
156 if (custom
!= null) {
160 List
<MetaData
> metas
= getList().filter(source
, null, null);
161 if (metas
.size() > 0) {
162 return getCover(metas
.get(0).getLuid());
169 * Return the cover image associated to this author.
171 * By default, return the custom cover if any, and if not, return the cover
172 * of the first story with this author.
177 * @return the cover image or NULL
179 * @throws IOException
180 * in case of IOException
182 public Image
getAuthorCover(String author
) throws IOException
{
183 Image custom
= getCustomAuthorCover(author
);
184 if (custom
!= null) {
188 List
<MetaData
> metas
= getList().filter(null, author
, null);
189 if (metas
.size() > 0) {
190 return getCover(metas
.get(0).getLuid());
197 * Return the custom cover image associated to this source.
199 * By default, return NULL.
202 * the source to look for
204 * @return the custom cover or NULL if none
206 * @throws IOException
207 * in case of IOException
209 @SuppressWarnings("unused")
210 public Image
getCustomSourceCover(String source
) throws IOException
{
215 * Return the custom cover image associated to this author.
217 * By default, return NULL.
220 * the author to look for
222 * @return the custom cover or NULL if none
224 * @throws IOException
225 * in case of IOException
227 @SuppressWarnings("unused")
228 public Image
getCustomAuthorCover(String author
) throws IOException
{
233 * Set the source cover to the given story cover.
236 * the source to change
240 * @throws IOException
241 * in case of IOException
243 public abstract void setSourceCover(String source
, String luid
)
247 * Set the author cover to the given story cover.
250 * the author to change
254 * @throws IOException
255 * in case of IOException
257 public abstract void setAuthorCover(String author
, String luid
)
261 * Return the list of stories (represented by their {@link MetaData}, which
262 * <b>MAY</b> not have the cover included).
264 * The returned list <b>MUST</b> be a copy, not the original one.
267 * the optional {@link Progress}
269 * @return the list (can be empty but not NULL)
271 * @throws IOException
272 * in case of IOException
274 protected abstract List
<MetaData
> getMetas(Progress pg
) throws IOException
;
277 * Invalidate the {@link Story} cache (when the content should be re-read
278 * because it was changed).
280 protected void invalidateInfo() {
281 invalidateInfo(null);
285 * Invalidate the {@link Story} cache (when the content is removed).
287 * All the cache can be deleted if NULL is passed as meta.
290 * the LUID of the {@link Story} to clear from the cache, or NULL
293 protected abstract void invalidateInfo(String luid
);
296 * Invalidate the {@link Story} cache (when the content has changed, but we
297 * already have it) with the new given meta.
300 * the {@link Story} to clear from the cache
302 * @throws IOException
303 * in case of IOException
305 protected abstract void updateInfo(MetaData meta
) throws IOException
;
308 * Return the next LUID that can be used.
310 * @return the next luid
312 protected abstract int getNextId();
315 * Delete the target {@link Story}.
318 * the LUID of the {@link Story}
320 * @throws IOException
321 * in case of I/O error or if the {@link Story} wa not found
323 protected abstract void doDelete(String luid
) throws IOException
;
326 * Actually save the story to the back-end.
329 * the {@link Story} to save
331 * the optional {@link Progress}
333 * @return the saved {@link Story} (which may have changed, especially
334 * regarding the {@link MetaData})
336 * @throws IOException
337 * in case of I/O error
339 protected abstract Story
doSave(Story story
, Progress pg
)
343 * Refresh the {@link BasicLibrary}, that is, make sure all metas are
347 * the optional progress reporter
349 public void refresh(Progress pg
) {
352 } catch (IOException e
) {
353 // We will let it fail later
358 * Check if the {@link Story} denoted by this Library UID is present in the
359 * cache (if we have no cache, we default to </tt>true</tt>).
364 * @return TRUE if it is
366 public boolean isCached(@SuppressWarnings("unused") String luid
) {
367 // By default, everything is cached
372 * Clear the {@link Story} from the cache, if needed.
374 * The next time we try to retrieve the {@link Story}, it may be required to
380 * @throws IOException
381 * in case of I/O error
383 @SuppressWarnings("unused")
384 public void clearFromCache(String luid
) throws IOException
{
385 // By default, this is a noop.
389 * @return the same as getList()
390 * @throws IOException
391 * in case of I/O error
392 * @deprecated please use {@link BasicLibrary#getList()} and
393 * {@link MetaResultList#getSources()} instead.
396 public List
<String
> getSources() throws IOException
{
397 return getList().getSources();
401 * @return the same as getList()
402 * @throws IOException
403 * in case of I/O error
404 * @deprecated please use {@link BasicLibrary#getList()} and
405 * {@link MetaResultList#getSourcesGrouped()} instead.
408 public Map
<String
, List
<String
>> getSourcesGrouped() throws IOException
{
409 return getList().getSourcesGrouped();
413 * @return the same as getList()
414 * @throws IOException
415 * in case of I/O error
416 * @deprecated please use {@link BasicLibrary#getList()} and
417 * {@link MetaResultList#getAuthors()} instead.
420 public List
<String
> getAuthors() throws IOException
{
421 return getList().getAuthors();
425 * @return the same as getList()
426 * @throws IOException
427 * in case of I/O error
428 * @deprecated please use {@link BasicLibrary#getList()} and
429 * {@link MetaResultList#getAuthorsGrouped()} instead.
432 public Map
<String
, List
<String
>> getAuthorsGrouped() throws IOException
{
433 return getList().getAuthorsGrouped();
437 * List all the stories in the {@link BasicLibrary}.
439 * Cover images <b>MAYBE</b> not included.
441 * @return the stories
443 * @throws IOException
444 * in case of IOException
446 public MetaResultList
getList() throws IOException
{
447 return getList(null);
451 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
452 * cover image <b>MAY</b> not be included.
455 * the Library UID of the story, can be NULL
457 * @return the corresponding {@link Story} or NULL if not found
459 * @throws IOException
460 * in case of IOException
462 public MetaData
getInfo(String luid
) throws IOException
{
464 for (MetaData meta
: getMetas(null)) {
465 if (luid
.equals(meta
.getLuid())) {
475 * Retrieve a specific {@link Story}.
478 * the Library UID of the story
480 * the optional progress reporter
482 * @return the corresponding {@link Story} or NULL if not found
484 * @throws IOException
485 * in case of IOException
487 public Story
getStory(String luid
, Progress pg
) throws IOException
{
488 Progress pgMetas
= new Progress();
489 Progress pgStory
= new Progress();
491 pg
.setMinMax(0, 100);
492 pg
.addProgress(pgMetas
, 10);
493 pg
.addProgress(pgStory
, 90);
496 MetaData meta
= null;
497 for (MetaData oneMeta
: getMetas(pgMetas
)) {
498 if (oneMeta
.getLuid().equals(luid
)) {
506 Story story
= getStory(luid
, meta
, pgStory
);
513 * Retrieve a specific {@link Story}.
516 * the LUID of the story
518 * the meta of the story
520 * the optional progress reporter
522 * @return the corresponding {@link Story} or NULL if not found
524 * @throws IOException
525 * in case of IOException
527 public synchronized Story
getStory(String luid
, MetaData meta
, Progress pg
)
534 Progress pgGet
= new Progress();
535 Progress pgProcess
= new Progress();
538 pg
.addProgress(pgGet
, 1);
539 pg
.addProgress(pgProcess
, 1);
544 if (luid
!= null && meta
!= null) {
545 file
= getFile(luid
, pgGet
);
551 SupportType type
= SupportType
.valueOfAllOkUC(meta
.getType());
553 throw new IOException("Unknown type: " + meta
.getType());
556 URL url
= file
.toURI().toURL();
557 story
= BasicSupport
.getSupport(type
, url
) //
560 // Because we do not want to clear the meta cache:
561 meta
.setCover(story
.getMeta().getCover());
562 meta
.setResume(story
.getMeta().getResume());
565 } catch (IOException e
) {
566 // We should not have not-supported files in the library
567 Instance
.getInstance().getTraceHandler()
568 .error(new IOException(String
.format(
569 "Cannot load file of type '%s' from library: %s",
570 meta
.getType(), file
), e
));
580 * Import the {@link Story} at the given {@link URL} into the
581 * {@link BasicLibrary}.
584 * the {@link URL} to import
586 * the optional progress reporter
588 * @return the imported Story {@link MetaData}
590 * @throws UnknownHostException
591 * if the host is not supported
592 * @throws IOException
593 * in case of I/O error
595 public MetaData
imprt(URL url
, Progress pg
) throws IOException
{
599 pg
.setMinMax(0, 1000);
600 Progress pgProcess
= new Progress();
601 Progress pgSave
= new Progress();
602 pg
.addProgress(pgProcess
, 800);
603 pg
.addProgress(pgSave
, 200);
605 BasicSupport support
= BasicSupport
.getSupport(url
);
606 if (support
== null) {
607 throw new UnknownHostException("" + url
);
610 Story story
= save(support
.process(pgProcess
), pgSave
);
613 return story
.getMeta();
617 * Import the story from one library to another, and keep the same LUID.
620 * the other library to import from
624 * the optional progress reporter
626 * @throws IOException
627 * in case of I/O error
629 public void imprt(BasicLibrary other
, String luid
, Progress pg
)
631 Progress pgGetStory
= new Progress();
632 Progress pgSave
= new Progress();
638 pg
.addProgress(pgGetStory
, 1);
639 pg
.addProgress(pgSave
, 1);
641 Story story
= other
.getStory(luid
, pgGetStory
);
643 story
= this.save(story
, luid
, pgSave
);
647 throw new IOException("Cannot find story in Library: " + luid
);
652 * Export the {@link Story} to the given target in the given format.
655 * the {@link Story} ID
657 * the {@link OutputType} to transform it to
659 * the target to save to
661 * the optional progress reporter
663 * @return the saved resource (the main saved {@link File})
665 * @throws IOException
666 * in case of I/O error
668 public File
export(String luid
, OutputType type
, String target
, Progress pg
)
670 Progress pgGetStory
= new Progress();
671 Progress pgOut
= new Progress();
674 pg
.addProgress(pgGetStory
, 1);
675 pg
.addProgress(pgOut
, 1);
678 BasicOutput out
= BasicOutput
.getOutput(type
, false, false);
680 throw new IOException("Output type not supported: " + type
);
683 Story story
= getStory(luid
, pgGetStory
);
685 throw new IOException("Cannot find story to export: " + luid
);
688 return out
.process(story
, target
, pgOut
);
692 * Save a {@link Story} to the {@link BasicLibrary}.
695 * the {@link Story} to save
697 * the optional progress reporter
699 * @return the same {@link Story}, whose LUID may have changed
701 * @throws IOException
702 * in case of I/O error
704 public Story
save(Story story
, Progress pg
) throws IOException
{
705 return save(story
, null, pg
);
709 * Save a {@link Story} to the {@link BasicLibrary} -- the LUID <b>must</b>
710 * be correct, or NULL to get the next free one.
712 * Will override any previous {@link Story} with the same LUID.
715 * the {@link Story} to save
717 * the <b>correct</b> LUID or NULL to get the next free one
719 * the optional progress reporter
721 * @return the same {@link Story}, whose LUID may have changed
723 * @throws IOException
724 * in case of I/O error
726 public synchronized Story
save(Story story
, String luid
, Progress pg
)
732 Instance
.getInstance().getTraceHandler().trace(
733 this.getClass().getSimpleName() + ": saving story " + luid
);
735 // Do not change the original metadata, but change the original story
736 MetaData meta
= story
.getMeta().clone();
739 pg
.setName("Saving story");
741 if (luid
== null || luid
.isEmpty()) {
742 meta
.setLuid(String
.format("%03d", getNextId()));
747 if (luid
!= null && getInfo(luid
) != null) {
751 story
= doSave(story
, pg
);
753 updateInfo(story
.getMeta());
755 Instance
.getInstance().getTraceHandler()
756 .trace(this.getClass().getSimpleName() + ": story saved ("
759 pg
.setName(meta
.getTitle());
765 * Delete the given {@link Story} from this {@link BasicLibrary}.
768 * the LUID of the target {@link Story}
770 * @throws IOException
771 * in case of I/O error
773 public synchronized void delete(String luid
) throws IOException
{
774 Instance
.getInstance().getTraceHandler().trace(
775 this.getClass().getSimpleName() + ": deleting story " + luid
);
778 invalidateInfo(luid
);
780 Instance
.getInstance().getTraceHandler()
781 .trace(this.getClass().getSimpleName() + ": story deleted ("
786 * Change the type (source) of the given {@link Story}.
789 * the {@link Story} LUID
793 * the optional progress reporter
795 * @throws IOException
796 * in case of I/O error or if the {@link Story} was not found
798 public synchronized void changeSource(String luid
, String newSource
,
799 Progress pg
) throws IOException
{
800 MetaData meta
= getInfo(luid
);
802 throw new IOException("Story not found: " + luid
);
805 changeSTA(luid
, newSource
, meta
.getTitle(), meta
.getAuthor(), pg
);
809 * Change the title (name) of the given {@link Story}.
812 * the {@link Story} LUID
816 * the optional progress reporter
818 * @throws IOException
819 * in case of I/O error or if the {@link Story} was not found
821 public synchronized void changeTitle(String luid
, String newTitle
,
822 Progress pg
) throws IOException
{
823 MetaData meta
= getInfo(luid
);
825 throw new IOException("Story not found: " + luid
);
828 changeSTA(luid
, meta
.getSource(), newTitle
, meta
.getAuthor(), pg
);
832 * Change the author of the given {@link Story}.
835 * the {@link Story} LUID
839 * the optional progress reporter
841 * @throws IOException
842 * in case of I/O error or if the {@link Story} was not found
844 public synchronized void changeAuthor(String luid
, String newAuthor
,
845 Progress pg
) throws IOException
{
846 MetaData meta
= getInfo(luid
);
848 throw new IOException("Story not found: " + luid
);
851 changeSTA(luid
, meta
.getSource(), meta
.getTitle(), newAuthor
, pg
);
855 * Change the Source, Title and Author of the {@link Story} in one single
859 * the {@link Story} LUID
867 * the optional progress reporter
869 * @throws IOException
870 * in case of I/O error or if the {@link Story} was not found
872 protected synchronized void changeSTA(String luid
, String newSource
,
873 String newTitle
, String newAuthor
, Progress pg
) throws IOException
{
874 MetaData meta
= getInfo(luid
);
876 throw new IOException("Story not found: " + luid
);
879 meta
.setSource(newSource
);
880 meta
.setTitle(newTitle
);
881 meta
.setAuthor(newAuthor
);
886 * Save back the current state of the {@link MetaData} (LUID <b>MUST NOT</b>
887 * change) for this {@link Story}.
889 * By default, delete the old {@link Story} then recreate a new
892 * Note that this behaviour can lead to data loss in case of problems!
895 * the new {@link MetaData} (LUID <b>MUST NOT</b> change)
897 * the optional {@link Progress}
899 * @throws IOException
900 * in case of I/O error or if the {@link Story} was not found
902 protected synchronized void saveMeta(MetaData meta
, Progress pg
)
908 Progress pgGet
= new Progress();
909 Progress pgSet
= new Progress();
910 pg
.addProgress(pgGet
, 50);
911 pg
.addProgress(pgSet
, 50);
913 Story story
= getStory(meta
.getLuid(), pgGet
);
915 throw new IOException("Story not found: " + meta
.getLuid());
918 // TODO: this is not safe!
919 delete(meta
.getLuid());
921 save(story
, meta
.getLuid(), pgSet
);
927 * Describe a {@link Story} from its {@link MetaData} and return a list of
928 * title/value that represent this {@link Story}.
931 * the {@link MetaData} to represent
933 * @return the information, translated and sorted
935 static public Map
<String
, String
> getMetaDesc(MetaData meta
) {
936 Map
<String
, String
> metaDesc
= new LinkedHashMap
<String
, String
>();
940 StringBuilder tags
= new StringBuilder();
941 for (String tag
: meta
.getTags()) {
942 if (tags
.length() > 0) {
949 metaDesc
.put("Author", meta
.getAuthor());
950 metaDesc
.put("Published on", meta
.getPublisher());
951 metaDesc
.put("Publication date", meta
.getDate());
952 metaDesc
.put("Creation date", meta
.getCreationDate());
954 if (meta
.getWords() > 0) {
955 count
= StringUtils
.formatNumber(meta
.getWords());
957 if (meta
.isImageDocument()) {
958 metaDesc
.put("Number of images", count
);
960 metaDesc
.put("Number of words", count
);
962 metaDesc
.put("Source", meta
.getSource());
963 metaDesc
.put("Subject", meta
.getSubject());
964 metaDesc
.put("Language", meta
.getLang());
965 metaDesc
.put("Tags", tags
.toString());
966 metaDesc
.put("URL", meta
.getUrl());