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 invalid, and will never work as is. */
48 /** The library is currently out of commission, but may work later. */
52 * The library is available (you can query it).
54 * It does <b>not</b> specify if it is read-only or not.
56 * @return TRUE if it is
58 public boolean isReady() {
59 return (this == READ_WRITE
|| this == READ_ONLY
);
63 * This library can be modified (= you are allowed to modify it).
65 * @return TRUE if it is
67 public boolean isWritable() {
68 return (this == READ_WRITE
);
73 * Return a name for this library (the UI may display this).
77 * @return the name, or an empty {@link String} if none
79 public String
getLibraryName() {
86 * @return the current status
88 public Status
getStatus() {
89 return Status
.READ_WRITE
;
93 * Retrieve the main {@link File} corresponding to the given {@link Story},
94 * which can be passed to an external reader or instance.
96 * Do <b>NOT</b> alter this file.
99 * the Library UID of the story, can be NULL
101 * the optional {@link Progress}
103 * @return the corresponding {@link Story}
105 * @throws IOException
106 * in case of IOException
108 public abstract File
getFile(String luid
, Progress pg
) throws IOException
;
111 * Return the cover image associated to this story.
114 * the Library UID of the story
116 * @return the cover image
118 * @throws IOException
119 * in case of IOException
121 public abstract Image
getCover(String luid
) throws IOException
;
124 * Retrieve the list of {@link MetaData} known by this {@link BasicLibrary}
125 * in a easy-to-filter version.
128 * the optional {@link Progress}
129 * @return the list of {@link MetaData} as a {@link MetaResultList} you can
131 * @throws IOException
132 * in case of I/O eror
134 public MetaResultList
getList(Progress pg
) throws IOException
{
135 // TODO: ensure it is the main used interface
137 return new MetaResultList(getMetas(pg
));
140 // TODO: make something for (normal and custom) non-story covers
143 * Return the cover image associated to this source.
145 * By default, return the custom cover if any, and if not, return the cover
146 * of the first story with this source.
151 * @return the cover image or NULL
153 * @throws IOException
154 * in case of IOException
156 public Image
getSourceCover(String source
) throws IOException
{
157 Image custom
= getCustomSourceCover(source
);
158 if (custom
!= null) {
162 List
<MetaData
> metas
= getList().filter(source
, null, null);
163 if (metas
.size() > 0) {
164 return getCover(metas
.get(0).getLuid());
171 * Return the cover image associated to this author.
173 * By default, return the custom cover if any, and if not, return the cover
174 * of the first story with this author.
179 * @return the cover image or NULL
181 * @throws IOException
182 * in case of IOException
184 public Image
getAuthorCover(String author
) throws IOException
{
185 Image custom
= getCustomAuthorCover(author
);
186 if (custom
!= null) {
190 List
<MetaData
> metas
= getList().filter(null, author
, null);
191 if (metas
.size() > 0) {
192 return getCover(metas
.get(0).getLuid());
199 * Return the custom cover image associated to this source.
201 * By default, return NULL.
204 * the source to look for
206 * @return the custom cover or NULL if none
208 * @throws IOException
209 * in case of IOException
211 @SuppressWarnings("unused")
212 public Image
getCustomSourceCover(String source
) throws IOException
{
217 * Return the custom cover image associated to this author.
219 * By default, return NULL.
222 * the author to look for
224 * @return the custom cover or NULL if none
226 * @throws IOException
227 * in case of IOException
229 @SuppressWarnings("unused")
230 public Image
getCustomAuthorCover(String author
) throws IOException
{
235 * Set the source cover to the given story cover.
238 * the source to change
242 * @throws IOException
243 * in case of IOException
245 public abstract void setSourceCover(String source
, String luid
)
249 * Set the author cover to the given story cover.
252 * the author to change
256 * @throws IOException
257 * in case of IOException
259 public abstract void setAuthorCover(String author
, String luid
)
263 * Return the list of stories (represented by their {@link MetaData}, which
264 * <b>MAY</b> not have the cover included).
266 * The returned list <b>MUST</b> be a copy, not the original one.
269 * the optional {@link Progress}
271 * @return the list (can be empty but not NULL)
273 * @throws IOException
274 * in case of IOException
276 protected abstract List
<MetaData
> getMetas(Progress pg
) throws IOException
;
279 * Invalidate the {@link Story} cache (when the content should be re-read
280 * because it was changed).
282 protected void invalidateInfo() {
283 invalidateInfo(null);
287 * Invalidate the {@link Story} cache (when the content is removed).
289 * All the cache can be deleted if NULL is passed as meta.
292 * the LUID of the {@link Story} to clear from the cache, or NULL
295 protected abstract void invalidateInfo(String luid
);
298 * Invalidate the {@link Story} cache (when the content has changed, but we
299 * already have it) with the new given meta.
302 * the {@link Story} to clear from the cache
304 * @throws IOException
305 * in case of IOException
307 protected abstract void updateInfo(MetaData meta
) throws IOException
;
310 * Return the next LUID that can be used.
312 * @return the next luid
314 protected abstract String
getNextId();
317 * Delete the target {@link Story}.
320 * the LUID of the {@link Story}
322 * @throws IOException
323 * in case of I/O error or if the {@link Story} wa not found
325 protected abstract void doDelete(String luid
) throws IOException
;
328 * Actually save the story to the back-end.
331 * the {@link Story} to save
333 * the optional {@link Progress}
335 * @return the saved {@link Story} (which may have changed, especially
336 * regarding the {@link MetaData})
338 * @throws IOException
339 * in case of I/O error
341 protected abstract Story
doSave(Story story
, Progress pg
)
345 * Refresh the {@link BasicLibrary}, that is, make sure all metas are
349 * the optional progress reporter
351 public void refresh(Progress pg
) {
354 } catch (IOException e
) {
355 // We will let it fail later
360 * Check if the {@link Story} denoted by this Library UID is present in the
361 * cache (if we have no cache, we default to </tt>true</tt>).
366 * @return TRUE if it is
368 public boolean isCached(@SuppressWarnings("unused") String luid
) {
369 // By default, everything is cached
374 * Clear the {@link Story} from the cache, if needed.
376 * The next time we try to retrieve the {@link Story}, it may be required to
382 * @throws IOException
383 * in case of I/O error
385 @SuppressWarnings("unused")
386 public void clearFromCache(String luid
) throws IOException
{
387 // By default, this is a noop.
391 * @return the same as getList()
392 * @throws IOException
393 * in case of I/O error
394 * @deprecated please use {@link BasicLibrary#getList()} and
395 * {@link MetaResultList#getSources()} instead.
398 public List
<String
> getSources() throws IOException
{
399 return getList().getSources();
403 * @return the same as getList()
404 * @throws IOException
405 * in case of I/O error
406 * @deprecated please use {@link BasicLibrary#getList()} and
407 * {@link MetaResultList#getSourcesGrouped()} instead.
410 public Map
<String
, List
<String
>> getSourcesGrouped() throws IOException
{
411 return getList().getSourcesGrouped();
415 * @return the same as getList()
416 * @throws IOException
417 * in case of I/O error
418 * @deprecated please use {@link BasicLibrary#getList()} and
419 * {@link MetaResultList#getAuthors()} instead.
422 public List
<String
> getAuthors() throws IOException
{
423 return getList().getAuthors();
427 * @return the same as getList()
428 * @throws IOException
429 * in case of I/O error
430 * @deprecated please use {@link BasicLibrary#getList()} and
431 * {@link MetaResultList#getAuthorsGrouped()} instead.
434 public Map
<String
, List
<String
>> getAuthorsGrouped() throws IOException
{
435 return getList().getAuthorsGrouped();
439 * List all the stories in the {@link BasicLibrary}.
441 * Cover images <b>MAYBE</b> not included.
443 * @return the stories
445 * @throws IOException
446 * in case of IOException
448 public MetaResultList
getList() throws IOException
{
449 return getList(null);
453 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
454 * cover image <b>MAY</b> not be included.
457 * the Library UID of the story, can be NULL
459 * @return the corresponding {@link Story} or NULL if not found
461 * @throws IOException
462 * in case of IOException
464 public MetaData
getInfo(String luid
) throws IOException
{
466 for (MetaData meta
: getMetas(null)) {
467 if (luid
.equals(meta
.getLuid())) {
477 * Retrieve a specific {@link Story}.
480 * the Library UID of the story
482 * the optional progress reporter
484 * @return the corresponding {@link Story} or NULL if not found
486 * @throws IOException
487 * in case of IOException
489 public Story
getStory(String luid
, Progress pg
) throws IOException
{
490 Progress pgMetas
= new Progress();
491 Progress pgStory
= new Progress();
493 pg
.setMinMax(0, 100);
494 pg
.addProgress(pgMetas
, 10);
495 pg
.addProgress(pgStory
, 90);
498 MetaData meta
= null;
499 for (MetaData oneMeta
: getMetas(pgMetas
)) {
500 if (oneMeta
.getLuid().equals(luid
)) {
508 Story story
= getStory(luid
, meta
, pgStory
);
515 * Retrieve a specific {@link Story}.
518 * the LUID of the story
520 * the meta of the story
522 * the optional progress reporter
524 * @return the corresponding {@link Story} or NULL if not found
526 * @throws IOException
527 * in case of IOException
529 public synchronized Story
getStory(String luid
, MetaData meta
, Progress pg
)
536 Progress pgGet
= new Progress();
537 Progress pgProcess
= new Progress();
540 pg
.addProgress(pgGet
, 1);
541 pg
.addProgress(pgProcess
, 1);
546 if (luid
!= null && meta
!= null) {
547 file
= getFile(luid
, pgGet
);
553 SupportType type
= SupportType
.valueOfAllOkUC(meta
.getType());
555 throw new IOException("Unknown type: " + meta
.getType());
558 URL url
= file
.toURI().toURL();
559 story
= BasicSupport
.getSupport(type
, url
) //
562 // Because we do not want to clear the meta cache:
563 meta
.setCover(story
.getMeta().getCover());
564 meta
.setResume(story
.getMeta().getResume());
567 } catch (IOException e
) {
568 // We should not have not-supported files in the library
569 Instance
.getInstance().getTraceHandler()
570 .error(new IOException(String
.format(
571 "Cannot load file of type '%s' from library: %s",
572 meta
.getType(), file
), e
));
582 * Import the {@link Story} at the given {@link URL} into the
583 * {@link BasicLibrary}.
586 * the {@link URL} to import
588 * the optional progress reporter
590 * @return the imported Story {@link MetaData}
592 * @throws UnknownHostException
593 * if the host is not supported
594 * @throws IOException
595 * in case of I/O error
597 public MetaData
imprt(URL url
, Progress pg
) throws IOException
{
598 return imprt(url
, null, pg
);
602 * Import the {@link Story} at the given {@link URL} into the
603 * {@link BasicLibrary}.
606 * the {@link URL} to import
610 * the optional progress reporter
612 * @return the imported Story {@link MetaData}
614 * @throws UnknownHostException
615 * if the host is not supported
616 * @throws IOException
617 * in case of I/O error
619 MetaData
imprt(URL url
, String luid
, Progress pg
) throws IOException
{
623 pg
.setMinMax(0, 1000);
624 Progress pgProcess
= new Progress();
625 Progress pgSave
= new Progress();
626 pg
.addProgress(pgProcess
, 800);
627 pg
.addProgress(pgSave
, 200);
629 BasicSupport support
= BasicSupport
.getSupport(url
);
630 if (support
== null) {
631 throw new UnknownHostException("" + url
);
634 Story story
= save(support
.process(pgProcess
), luid
, pgSave
);
637 return story
.getMeta();
641 * Import the story from one library to another, and keep the same LUID.
644 * the other library to import from
648 * the optional progress reporter
650 * @throws IOException
651 * in case of I/O error
653 public void imprt(BasicLibrary other
, String luid
, Progress pg
)
655 Progress pgGetStory
= new Progress();
656 Progress pgSave
= new Progress();
662 pg
.addProgress(pgGetStory
, 1);
663 pg
.addProgress(pgSave
, 1);
665 Story story
= other
.getStory(luid
, pgGetStory
);
667 story
= this.save(story
, luid
, pgSave
);
671 throw new IOException("Cannot find story in Library: " + luid
);
676 * Export the {@link Story} to the given target in the given format.
679 * the {@link Story} ID
681 * the {@link OutputType} to transform it to
683 * the target to save to
685 * the optional progress reporter
687 * @return the saved resource (the main saved {@link File})
689 * @throws IOException
690 * in case of I/O error
692 public File
export(String luid
, OutputType type
, String target
, Progress pg
)
694 Progress pgGetStory
= new Progress();
695 Progress pgOut
= new Progress();
698 pg
.addProgress(pgGetStory
, 1);
699 pg
.addProgress(pgOut
, 1);
702 BasicOutput out
= BasicOutput
.getOutput(type
, false, false);
704 throw new IOException("Output type not supported: " + type
);
707 Story story
= getStory(luid
, pgGetStory
);
709 throw new IOException("Cannot find story to export: " + luid
);
712 return out
.process(story
, target
, pgOut
);
716 * Save a {@link Story} to the {@link BasicLibrary}.
719 * the {@link Story} to save
721 * the optional progress reporter
723 * @return the same {@link Story}, whose LUID may have changed
725 * @throws IOException
726 * in case of I/O error
728 public Story
save(Story story
, Progress pg
) throws IOException
{
729 return save(story
, null, pg
);
733 * Save a {@link Story} to the {@link BasicLibrary} -- the LUID <b>must</b>
734 * be correct, or NULL to get the next free one.
736 * Will override any previous {@link Story} with the same LUID.
739 * the {@link Story} to save
741 * the <b>correct</b> LUID or NULL to get the next free one
743 * the optional progress reporter
745 * @return the same {@link Story}, whose LUID may have changed
747 * @throws IOException
748 * in case of I/O error
750 public synchronized Story
save(Story story
, String luid
, Progress pg
)
756 Instance
.getInstance().getTraceHandler().trace(
757 this.getClass().getSimpleName() + ": saving story " + luid
);
759 // Do not change the original metadata, but change the original story
760 MetaData meta
= story
.getMeta().clone();
763 pg
.setName("Saving story");
765 if (luid
== null || luid
.isEmpty()) {
766 meta
.setLuid(getNextId());
771 if (luid
!= null && getInfo(luid
) != null) {
775 story
= doSave(story
, pg
);
777 updateInfo(story
.getMeta());
779 Instance
.getInstance().getTraceHandler()
780 .trace(this.getClass().getSimpleName() + ": story saved ("
783 pg
.setName(meta
.getTitle());
789 * Delete the given {@link Story} from this {@link BasicLibrary}.
792 * the LUID of the target {@link Story}
794 * @throws IOException
795 * in case of I/O error
797 public synchronized void delete(String luid
) throws IOException
{
798 Instance
.getInstance().getTraceHandler().trace(
799 this.getClass().getSimpleName() + ": deleting story " + luid
);
802 invalidateInfo(luid
);
804 Instance
.getInstance().getTraceHandler()
805 .trace(this.getClass().getSimpleName() + ": story deleted ("
810 * Change the type (source) of the given {@link Story}.
813 * the {@link Story} LUID
817 * the optional progress reporter
819 * @throws IOException
820 * in case of I/O error or if the {@link Story} was not found
822 public synchronized void changeSource(String luid
, String newSource
,
823 Progress pg
) throws IOException
{
824 MetaData meta
= getInfo(luid
);
826 throw new IOException("Story not found: " + luid
);
829 changeSTA(luid
, newSource
, meta
.getTitle(), meta
.getAuthor(), pg
);
833 * Change the title (name) of the given {@link Story}.
836 * the {@link Story} LUID
840 * the optional progress reporter
842 * @throws IOException
843 * in case of I/O error or if the {@link Story} was not found
845 public synchronized void changeTitle(String luid
, String newTitle
,
846 Progress pg
) throws IOException
{
847 MetaData meta
= getInfo(luid
);
849 throw new IOException("Story not found: " + luid
);
852 changeSTA(luid
, meta
.getSource(), newTitle
, meta
.getAuthor(), pg
);
856 * Change the author of the given {@link Story}.
859 * the {@link Story} LUID
863 * the optional progress reporter
865 * @throws IOException
866 * in case of I/O error or if the {@link Story} was not found
868 public synchronized void changeAuthor(String luid
, String newAuthor
,
869 Progress pg
) throws IOException
{
870 MetaData meta
= getInfo(luid
);
872 throw new IOException("Story not found: " + luid
);
875 changeSTA(luid
, meta
.getSource(), meta
.getTitle(), newAuthor
, pg
);
879 * Change the Source, Title and Author of the {@link Story} in one single
883 * the {@link Story} LUID
891 * the optional progress reporter
893 * @throws IOException
894 * in case of I/O error or if the {@link Story} was not found
896 protected synchronized void changeSTA(String luid
, String newSource
,
897 String newTitle
, String newAuthor
, Progress pg
) throws IOException
{
898 MetaData meta
= getInfo(luid
);
900 throw new IOException("Story not found: " + luid
);
903 meta
.setSource(newSource
);
904 meta
.setTitle(newTitle
);
905 meta
.setAuthor(newAuthor
);
910 * Save back the current state of the {@link MetaData} (LUID <b>MUST NOT</b>
911 * change) for this {@link Story}.
913 * By default, delete the old {@link Story} then recreate a new
916 * Note that this behaviour can lead to data loss in case of problems!
919 * the new {@link MetaData} (LUID <b>MUST NOT</b> change)
921 * the optional {@link Progress}
923 * @throws IOException
924 * in case of I/O error or if the {@link Story} was not found
926 protected synchronized void saveMeta(MetaData meta
, Progress pg
)
932 Progress pgGet
= new Progress();
933 Progress pgSet
= new Progress();
934 pg
.addProgress(pgGet
, 50);
935 pg
.addProgress(pgSet
, 50);
937 Story story
= getStory(meta
.getLuid(), pgGet
);
939 throw new IOException("Story not found: " + meta
.getLuid());
942 // TODO: this is not safe!
943 delete(meta
.getLuid());
945 save(story
, meta
.getLuid(), pgSet
);
951 * Describe a {@link Story} from its {@link MetaData} and return a list of
952 * title/value that represent this {@link Story}.
955 * the {@link MetaData} to represent
957 * @return the information, translated and sorted
959 static public Map
<String
, String
> getMetaDesc(MetaData meta
) {
960 Map
<String
, String
> metaDesc
= new LinkedHashMap
<String
, String
>();
964 StringBuilder tags
= new StringBuilder();
965 for (String tag
: meta
.getTags()) {
966 if (tags
.length() > 0) {
973 metaDesc
.put("Author", meta
.getAuthor());
974 metaDesc
.put("Published on", meta
.getPublisher());
975 metaDesc
.put("Publication date", meta
.getDate());
976 metaDesc
.put("Creation date", meta
.getCreationDate());
978 if (meta
.getWords() > 0) {
979 count
= StringUtils
.formatNumber(meta
.getWords());
981 if (meta
.isImageDocument()) {
982 metaDesc
.put("Number of images", count
);
984 metaDesc
.put("Number of words", count
);
986 metaDesc
.put("Source", meta
.getSource());
987 metaDesc
.put("Subject", meta
.getSubject());
988 metaDesc
.put("Language", meta
.getLang());
989 metaDesc
.put("Tags", tags
.toString());
990 metaDesc
.put("URL", meta
.getUrl());