1 package be
.nikiroo
.fanfix
.library
;
4 import java
.io
.IOException
;
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
;
12 import java
.util
.Map
.Entry
;
13 import java
.util
.TreeMap
;
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
;
27 * Manage a library of Stories: import, export, list, modify.
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}.
32 * Most of the {@link BasicLibrary} functions work on a partial (cover
33 * <b>MAY</b> not be included) {@link MetaData} object.
37 abstract public class BasicLibrary
{
39 * A {@link BasicLibrary} status.
44 /** The library is ready. */
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. */
55 * Return a name for this library (the UI may display this).
59 * @return the name, or an empty {@link String} if none
61 public String
getLibraryName() {
68 * @return the current status
70 public Status
getStatus() {
75 * Retrieve the main {@link File} corresponding to the given {@link Story},
76 * which can be passed to an external reader or instance.
78 * Do <b>NOT</b> alter this file.
81 * the Library UID of the story
83 * the optional {@link Progress}
85 * @return the corresponding {@link Story}
87 public abstract File
getFile(String luid
, Progress pg
);
90 * Return the cover image associated to this story.
93 * the Library UID of the story
95 * @return the cover image
97 public abstract Image
getCover(String luid
);
100 * Return the cover image associated to this source.
102 * By default, return the custom cover if any, and if not, return the cover
103 * of the first story with this source.
108 * @return the cover image or NULL
110 public Image
getSourceCover(String source
) {
111 Image custom
= getCustomSourceCover(source
);
112 if (custom
!= null) {
116 List
<MetaData
> metas
= getListBySource(source
);
117 if (metas
.size() > 0) {
118 return getCover(metas
.get(0).getLuid());
125 * Return the custom cover image associated to this source.
127 * By default, return NULL.
130 * the source to look for
132 * @return the custom cover or NULL if none
134 public Image
getCustomSourceCover(@SuppressWarnings("unused") String source
) {
139 * Fix the source cover to the given story cover.
142 * the source to change
146 public abstract void setSourceCover(String source
, String luid
);
149 * Return the list of stories (represented by their {@link MetaData}, which
150 * <b>MAY</b> not have the cover included).
153 * the optional {@link Progress}
155 * @return the list (can be empty but not NULL)
157 protected abstract List
<MetaData
> getMetas(Progress pg
);
160 * Invalidate the {@link Story} cache (when the content should be re-read
161 * because it was changed).
163 protected void invalidateInfo() {
164 invalidateInfo(null);
168 * Invalidate the {@link Story} cache (when the content is removed).
170 * All the cache can be deleted if NULL is passed as meta.
173 * the LUID of the {@link Story} to clear from the cache, or NULL
176 protected abstract void invalidateInfo(String luid
);
179 * Invalidate the {@link Story} cache (when the content has changed, but we
180 * already have it) with the new given meta.
183 * the {@link Story} to clear from the cache
185 protected abstract void updateInfo(MetaData meta
);
188 * Return the next LUID that can be used.
190 * @return the next luid
192 protected abstract int getNextId();
195 * Delete the target {@link Story}.
198 * the LUID of the {@link Story}
200 * @throws IOException
201 * in case of I/O error or if the {@link Story} wa not found
203 protected abstract void doDelete(String luid
) throws IOException
;
206 * Actually save the story to the back-end.
209 * the {@link Story} to save
211 * the optional {@link Progress}
213 * @return the saved {@link Story} (which may have changed, especially
214 * regarding the {@link MetaData})
216 * @throws IOException
217 * in case of I/O error
219 protected abstract Story
doSave(Story story
, Progress pg
)
223 * Refresh the {@link BasicLibrary}, that is, make sure all metas are
227 * the optional progress reporter
229 public void refresh(Progress pg
) {
234 * List all the known types (sources) of stories.
236 * @return the sources
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
);
247 Collections
.sort(list
);
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").
255 * Note that an empty item in the list means a non-grouped source (type) --
256 * e.g., you could have for Source_1:
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>
263 * @return the grouped list
265 public synchronized Map
<String
, List
<String
>> getSourcesGrouped() {
266 Map
<String
, List
<String
>> map
= new TreeMap
<String
, List
<String
>>();
267 for (String source
: getSources()) {
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);
281 List
<String
> list
= map
.get(name
);
283 list
= new ArrayList
<String
>();
293 * List all the known authors of stories.
295 * @return the authors
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
);
306 Collections
.sort(list
);
311 * Return the list of authors, grouped by starting letter(s) if needed.
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.
316 * If not, the authors will be separated into groups:
318 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
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>
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).
327 * @return the authors' names, grouped by letter(s)
329 public List
<Entry
<String
, List
<String
>>> getAuthorsGrouped() {
332 List
<Entry
<String
, List
<String
>>> groups
= new ArrayList
<Entry
<String
, List
<String
>>>();
333 List
<String
> authors
= getAuthors();
335 if (authors
.size() <= MAX
) {
336 groups
.add(new SimpleEntry
<String
, List
<String
>>("", authors
));
340 groups
.add(new SimpleEntry
<String
, List
<String
>>("*", getAuthorsGroup(
342 groups
.add(new SimpleEntry
<String
, List
<String
>>("0-9",
343 getAuthorsGroup(authors
, '0')));
345 for (char car
= 'A'; car
<= 'Z'; car
++) {
346 groups
.add(new SimpleEntry
<String
, List
<String
>>(Character
347 .toString(car
), getAuthorsGroup(authors
, car
)));
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);
367 for (int i
= 0; i
< groups
.size(); i
++) {
368 if (groups
.get(i
).getValue().size() == 0) {
378 * Get all the authors that start with the given character:
380 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
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>
388 * the full list of authors
390 * the starting character, <tt>*</tt>, <tt>0</tt> or a capital
392 * @return the authors that fulfill the starting letter
394 private List
<String
> getAuthorsGroup(List
<String
> authors
, char car
) {
395 List
<String
> accepted
= new ArrayList
<String
>();
396 for (String author
: authors
) {
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') {
403 } else if (c
>= 'a' && c
<= 'z') {
404 first
= (char) (c
- 'a' + 'A');
405 } else if (c
>= 'A' && c
<= 'Z') {
411 accepted
.add(author
);
419 * List all the stories in the {@link BasicLibrary}.
421 * Cover images <b>MAYBE</b> not included.
423 * @return the stories
425 public synchronized List
<MetaData
> getList() {
426 return getMetas(null);
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.
433 * Cover images not included.
436 * the type of story to retrieve, or NULL for all
438 * @return the stories
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
)) {
449 Collections
.sort(list
);
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.
457 * Cover images not included.
460 * the author of the stories to retrieve, or NULL for all
462 * @return the stories
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
)) {
473 Collections
.sort(list
);
478 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
479 * cover image <b>MAY</b> not be included.
482 * the Library UID of the story
484 * @return the corresponding {@link Story}
486 public synchronized MetaData
getInfo(String luid
) {
488 for (MetaData meta
: getMetas(null)) {
489 if (luid
.equals(meta
.getLuid())) {
499 * Retrieve a specific {@link Story}.
502 * the Library UID of the story
504 * the optional progress reporter
506 * @return the corresponding {@link Story} or NULL if not found
508 public synchronized Story
getStory(String luid
, Progress pg
) {
513 Progress pgGet
= new Progress();
514 Progress pgProcess
= new Progress();
517 pg
.addProgress(pgGet
, 1);
518 pg
.addProgress(pgProcess
, 1);
521 for (MetaData meta
: getMetas(null)) {
522 if (meta
.getLuid().equals(luid
)) {
523 File file
= getFile(luid
, pgGet
);
526 SupportType type
= SupportType
.valueOfAllOkUC(meta
528 URL url
= file
.toURI().toURL();
530 story
= BasicSupport
.getSupport(type
, url
) //
533 // Because we do not want to clear the meta cache:
534 meta
.setCover(story
.getMeta().getCover());
535 meta
.setResume(story
.getMeta().getResume());
539 throw new IOException("Unknown type: " + meta
.getType());
541 } catch (IOException e
) {
542 // We should not have not-supported files in the
544 Instance
.getTraceHandler().error(
545 new IOException("Cannot load file from library: "
560 * Import the {@link Story} at the given {@link URL} into the
561 * {@link BasicLibrary}.
564 * the {@link URL} to import
566 * the optional progress reporter
568 * @return the imported {@link Story}
570 * @throws UnknownHostException
571 * if the host is not supported
572 * @throws IOException
573 * in case of I/O error
575 public Story
imprt(URL url
, Progress pg
) throws IOException
{
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);
585 BasicSupport support
= BasicSupport
.getSupport(url
);
586 if (support
== null) {
587 throw new UnknownHostException("" + url
);
590 Story story
= save(support
.process(pgProcess
), pgSave
);
597 * Import the story from one library to another, and keep the same LUID.
600 * the other library to import from
604 * the optional progress reporter
606 * @throws IOException
607 * in case of I/O error
609 public void imprt(BasicLibrary other
, String luid
, Progress pg
)
611 Progress pgGetStory
= new Progress();
612 Progress pgSave
= new Progress();
618 pg
.addProgress(pgGetStory
, 1);
619 pg
.addProgress(pgSave
, 1);
621 Story story
= other
.getStory(luid
, pgGetStory
);
623 story
= this.save(story
, luid
, pgSave
);
627 throw new IOException("Cannot find story in Library: " + luid
);
632 * Export the {@link Story} to the given target in the given format.
635 * the {@link Story} ID
637 * the {@link OutputType} to transform it to
639 * the target to save to
641 * the optional progress reporter
643 * @return the saved resource (the main saved {@link File})
645 * @throws IOException
646 * in case of I/O error
648 public File
export(String luid
, OutputType type
, String target
, Progress pg
)
650 Progress pgGetStory
= new Progress();
651 Progress pgOut
= new Progress();
654 pg
.addProgress(pgGetStory
, 1);
655 pg
.addProgress(pgOut
, 1);
658 BasicOutput out
= BasicOutput
.getOutput(type
, false, false);
660 throw new IOException("Output type not supported: " + type
);
663 Story story
= getStory(luid
, pgGetStory
);
665 throw new IOException("Cannot find story to export: " + luid
);
668 return out
.process(story
, target
, pgOut
);
672 * Save a {@link Story} to the {@link BasicLibrary}.
675 * the {@link Story} to save
677 * the optional progress reporter
679 * @return the same {@link Story}, whose LUID may have changed
681 * @throws IOException
682 * in case of I/O error
684 public Story
save(Story story
, Progress pg
) throws IOException
{
685 return save(story
, null, pg
);
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.
692 * Will override any previous {@link Story} with the same LUID.
695 * the {@link Story} to save
697 * the <b>correct</b> LUID or NULL to get the next free one
699 * the optional progress reporter
701 * @return the same {@link Story}, whose LUID may have changed
703 * @throws IOException
704 * in case of I/O error
706 public synchronized Story
save(Story story
, String luid
, Progress pg
)
709 Instance
.getTraceHandler().trace(
710 this.getClass().getSimpleName() + ": saving story " + luid
);
712 // Do not change the original metadata, but change the original story
713 MetaData meta
= story
.getMeta().clone();
716 if (luid
== null || luid
.isEmpty()) {
717 meta
.setLuid(String
.format("%03d", getNextId()));
722 if (luid
!= null && getInfo(luid
) != null) {
726 story
= doSave(story
, pg
);
728 updateInfo(story
.getMeta());
730 Instance
.getTraceHandler().trace(
731 this.getClass().getSimpleName() + ": story saved (" + luid
738 * Delete the given {@link Story} from this {@link BasicLibrary}.
741 * the LUID of the target {@link Story}
743 * @throws IOException
744 * in case of I/O error
746 public synchronized void delete(String luid
) throws IOException
{
747 Instance
.getTraceHandler().trace(
748 this.getClass().getSimpleName() + ": deleting story " + luid
);
751 invalidateInfo(luid
);
753 Instance
.getTraceHandler().trace(
754 this.getClass().getSimpleName() + ": story deleted (" + luid
759 * Change the type (source) of the given {@link Story}.
762 * the {@link Story} LUID
766 * the optional progress reporter
768 * @throws IOException
769 * in case of I/O error or if the {@link Story} was not found
771 public synchronized void changeSource(String luid
, String newSource
,
772 Progress pg
) throws IOException
{
773 MetaData meta
= getInfo(luid
);
775 throw new IOException("Story not found: " + luid
);
778 changeSTA(luid
, newSource
, meta
.getTitle(), meta
.getAuthor(), pg
);
782 * Change the title (name) of the given {@link Story}.
785 * the {@link Story} LUID
789 * the optional progress reporter
791 * @throws IOException
792 * in case of I/O error or if the {@link Story} was not found
794 public synchronized void changeTitle(String luid
, String newTitle
,
795 Progress pg
) throws IOException
{
796 MetaData meta
= getInfo(luid
);
798 throw new IOException("Story not found: " + luid
);
801 changeSTA(luid
, meta
.getSource(), newTitle
, meta
.getAuthor(), pg
);
805 * Change the author of the given {@link Story}.
808 * the {@link Story} LUID
812 * the optional progress reporter
814 * @throws IOException
815 * in case of I/O error or if the {@link Story} was not found
817 public synchronized void changeAuthor(String luid
, String newAuthor
,
818 Progress pg
) throws IOException
{
819 MetaData meta
= getInfo(luid
);
821 throw new IOException("Story not found: " + luid
);
824 changeSTA(luid
, meta
.getSource(), meta
.getTitle(), newAuthor
, pg
);
828 * Change the Source, Title and Author of the {@link Story} in one single
832 * 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 protected synchronized void changeSTA(String luid
, String newSource
,
846 String newTitle
, String newAuthor
, Progress pg
) throws IOException
{
847 MetaData meta
= getInfo(luid
);
849 throw new IOException("Story not found: " + luid
);
852 meta
.setSource(newSource
);
853 meta
.setTitle(newTitle
);
854 meta
.setAuthor(newAuthor
);
859 * Save back the current state of the {@link MetaData} (LUID <b>MUST NOT</b>
860 * change) for this {@link Story}.
862 * By default, delete the old {@link Story} then recreate a new
865 * Note that this behaviour can lead to data loss in case of problems!
868 * the new {@link MetaData} (LUID <b>MUST NOT</b> change)
870 * the optional {@link Progress}
872 * @throws IOException
873 * in case of I/O error or if the {@link Story} was not found
875 protected synchronized void saveMeta(MetaData meta
, Progress pg
)
881 Progress pgGet
= new Progress();
882 Progress pgSet
= new Progress();
883 pg
.addProgress(pgGet
, 50);
884 pg
.addProgress(pgSet
, 50);
886 Story story
= getStory(meta
.getLuid(), pgGet
);
888 throw new IOException("Story not found: " + meta
.getLuid());
891 // TODO: this is not safe!
892 delete(meta
.getLuid());
894 save(story
, meta
.getLuid(), pgSet
);