f6daed620f86347a5ec84a44ee3e8e80b600eb09
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
;
11 import java
.util
.Map
.Entry
;
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 custom cover image associated to this source.
125 * By default, return NULL.
128 * the source to look for
130 * @return the custom cover or NULL if none
132 public Image
getCustomSourceCover(@SuppressWarnings("unused") String source
) {
137 * Fix the source cover to the given story cover.
140 * the source to change
144 public abstract void setSourceCover(String source
, String luid
);
147 * Return the list of stories (represented by their {@link MetaData}, which
148 * <b>MAY</b> not have the cover included).
151 * the optional {@link Progress}
153 * @return the list (can be empty but not NULL)
155 protected abstract List
<MetaData
> getMetas(Progress pg
);
158 * Invalidate the {@link Story} cache (when the content should be re-read
159 * because it was changed).
161 protected void deleteInfo() {
166 * Invalidate the {@link Story} cache (when the content is removed).
168 * All the cache can be deleted if NULL is passed as meta.
171 * the LUID of the {@link Story} to clear from the cache, or NULL
174 protected abstract void deleteInfo(String luid
);
177 * Invalidate the {@link Story} cache (when the content has changed, but we
178 * already have it) with the new given meta.
181 * the {@link Story} to clear from the cache
183 protected abstract void updateInfo(MetaData meta
);
186 * Return the next LUID that can be used.
188 * @return the next luid
190 protected abstract int getNextId();
193 * Delete the target {@link Story}.
196 * the LUID of the {@link Story}
198 * @throws IOException
199 * in case of I/O error or if the {@link Story} wa not found
201 protected abstract void doDelete(String luid
) throws IOException
;
204 * Actually save the story to the back-end.
207 * the {@link Story} to save
209 * the optional {@link Progress}
211 * @return the saved {@link Story} (which may have changed, especially
212 * regarding the {@link MetaData})
214 * @throws IOException
215 * in case of I/O error
217 protected abstract Story
doSave(Story story
, Progress pg
)
221 * Refresh the {@link BasicLibrary}, that is, make sure all metas are
225 * the optional progress reporter
227 public void refresh(Progress pg
) {
232 * List all the known types (sources) of stories.
234 * @return the sources
236 public synchronized List
<String
> getSources() {
237 List
<String
> list
= new ArrayList
<String
>();
238 for (MetaData meta
: getMetas(null)) {
239 String storySource
= meta
.getSource();
240 if (!list
.contains(storySource
)) {
241 list
.add(storySource
);
245 Collections
.sort(list
);
250 * List all the known authors of stories.
252 * @return the authors
254 public synchronized List
<String
> getAuthors() {
255 List
<String
> list
= new ArrayList
<String
>();
256 for (MetaData meta
: getMetas(null)) {
257 String storyAuthor
= meta
.getAuthor();
258 if (!list
.contains(storyAuthor
)) {
259 list
.add(storyAuthor
);
263 Collections
.sort(list
);
268 * Return the list of authors, grouped by starting letter(s) if needed.
270 * If the number of author is not too high, only one group with an empty
271 * name and all the authors will be returned.
273 * If not, the authors will be separated into groups:
275 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
277 * <li><tt>0-9</tt>: any authors whose name starts with a number</li>
278 * <li><tt>A-C</tt> (for instance): any author whose name starts with
279 * <tt>A</tt>, <tt>B</tt> or <tt>C</tt></li>
281 * Note that the letters used in the groups can vary (except <tt>*</tt> and
282 * <tt>0-9</tt>, which may only be present or not).
284 * @return the authors' names, grouped by letter(s)
286 public List
<Entry
<String
, List
<String
>>> getAuthorsGrouped() {
289 List
<Entry
<String
, List
<String
>>> groups
= new ArrayList
<Entry
<String
, List
<String
>>>();
290 List
<String
> authors
= getAuthors();
292 if (authors
.size() <= MAX
) {
293 groups
.add(new SimpleEntry
<String
, List
<String
>>("", authors
));
297 groups
.add(new SimpleEntry
<String
, List
<String
>>("*", getAuthorsGroup(
299 groups
.add(new SimpleEntry
<String
, List
<String
>>("0-9",
300 getAuthorsGroup(authors
, '0')));
302 for (char car
= 'A'; car
<= 'Z'; car
++) {
303 groups
.add(new SimpleEntry
<String
, List
<String
>>(Character
304 .toString(car
), getAuthorsGroup(authors
, car
)));
307 // do NOT collapse * and [0-9] with the rest
308 for (int i
= 2; i
+ 1 < groups
.size(); i
++) {
309 Entry
<String
, List
<String
>> now
= groups
.get(i
);
310 Entry
<String
, List
<String
>> next
= groups
.get(i
+ 1);
311 int currentTotal
= now
.getValue().size() + next
.getValue().size();
312 if (currentTotal
<= MAX
) {
313 String key
= now
.getKey().charAt(0) + "-"
314 + next
.getKey().charAt(next
.getKey().length() - 1);
315 List
<String
> all
= new ArrayList
<String
>();
316 all
.addAll(now
.getValue());
317 all
.addAll(next
.getValue());
318 groups
.set(i
, new SimpleEntry
<String
, List
<String
>>(key
, all
));
319 groups
.remove(i
+ 1);
324 for (int i
= 0; i
< groups
.size(); i
++) {
325 if (groups
.get(i
).getValue().size() == 0) {
335 * Get all the authors that start with the given character:
337 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
339 * <li><tt>0</tt>: any authors whose name starts with a number</li>
340 * <li><tt>A</tt> (any capital latin letter): any author whose name starts
341 * with <tt>A</tt></li>
345 * the full list of authors
347 * the starting character, <tt>*</tt>, <tt>0</tt> or a capital
349 * @return the authors that fulfill the starting letter
351 private List
<String
> getAuthorsGroup(List
<String
> authors
, char car
) {
352 List
<String
> accepted
= new ArrayList
<String
>();
353 for (String author
: authors
) {
355 for (int i
= 0; first
== '*' && i
< author
.length(); i
++) {
356 String san
= StringUtils
.sanitize(author
, true, true);
357 char c
= san
.charAt(i
);
358 if (c
>= '0' && c
<= '9') {
360 } else if (c
>= 'a' && c
<= 'z') {
361 first
= (char) (c
- 'a' + 'A');
362 } else if (c
>= 'A' && c
<= 'Z') {
368 accepted
.add(author
);
376 * List all the stories in the {@link BasicLibrary}.
378 * Cover images not included.
380 * @return the stories
382 public synchronized List
<MetaData
> getList() {
383 return getMetas(null);
387 * List all the stories of the given source type in the {@link BasicLibrary}
388 * , or all the stories if NULL is passed as a type.
390 * Cover images not included.
393 * the type of story to retrieve, or NULL for all
395 * @return the stories
397 public synchronized List
<MetaData
> getListBySource(String type
) {
398 List
<MetaData
> list
= new ArrayList
<MetaData
>();
399 for (MetaData meta
: getMetas(null)) {
400 String storyType
= meta
.getSource();
401 if (type
== null || type
.equalsIgnoreCase(storyType
)) {
406 Collections
.sort(list
);
411 * List all the stories of the given author in the {@link BasicLibrary}, or
412 * all the stories if NULL is passed as an author.
414 * Cover images not included.
417 * the author of the stories to retrieve, or NULL for all
419 * @return the stories
421 public synchronized List
<MetaData
> getListByAuthor(String author
) {
422 List
<MetaData
> list
= new ArrayList
<MetaData
>();
423 for (MetaData meta
: getMetas(null)) {
424 String storyAuthor
= meta
.getAuthor();
425 if (author
== null || author
.equalsIgnoreCase(storyAuthor
)) {
430 Collections
.sort(list
);
435 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
436 * cover image <b>MAY</b> not be included.
439 * the Library UID of the story
441 * @return the corresponding {@link Story}
443 public synchronized MetaData
getInfo(String luid
) {
445 for (MetaData meta
: getMetas(null)) {
446 if (luid
.equals(meta
.getLuid())) {
456 * Retrieve a specific {@link Story}.
459 * the Library UID of the story
461 * the optional progress reporter
463 * @return the corresponding {@link Story} or NULL if not found
465 public synchronized Story
getStory(String luid
, Progress pg
) {
470 Progress pgGet
= new Progress();
471 Progress pgProcess
= new Progress();
474 pg
.addProgress(pgGet
, 1);
475 pg
.addProgress(pgProcess
, 1);
478 for (MetaData meta
: getMetas(null)) {
479 if (meta
.getLuid().equals(luid
)) {
480 File file
= getFile(luid
, pgGet
);
483 SupportType type
= SupportType
.valueOfAllOkUC(meta
485 URL url
= file
.toURI().toURL();
487 story
= BasicSupport
.getSupport(type
, url
) //
490 // Because we do not want to clear the meta cache:
491 meta
.setCover(story
.getMeta().getCover());
492 meta
.setResume(story
.getMeta().getResume());
496 throw new IOException("Unknown type: " + meta
.getType());
498 } catch (IOException e
) {
499 // We should not have not-supported files in the
501 Instance
.getTraceHandler().error(
502 new IOException("Cannot load file from library: "
517 * Import the {@link Story} at the given {@link URL} into the
518 * {@link BasicLibrary}.
521 * the {@link URL} to import
523 * the optional progress reporter
525 * @return the imported {@link Story}
527 * @throws UnknownHostException
528 * if the host is not supported
529 * @throws IOException
530 * in case of I/O error
532 public Story
imprt(URL url
, Progress pg
) throws IOException
{
536 pg
.setMinMax(0, 1000);
537 Progress pgProcess
= new Progress();
538 Progress pgSave
= new Progress();
539 pg
.addProgress(pgProcess
, 800);
540 pg
.addProgress(pgSave
, 200);
542 BasicSupport support
= BasicSupport
.getSupport(url
);
543 if (support
== null) {
544 throw new UnknownHostException("" + url
);
547 Story story
= save(support
.process(pgProcess
), pgSave
);
554 * Import the story from one library to another, and keep the same LUID.
557 * the other library to import from
561 * the optional progress reporter
563 * @throws IOException
564 * in case of I/O error
566 public void imprt(BasicLibrary other
, String luid
, Progress pg
)
568 Progress pgGetStory
= new Progress();
569 Progress pgSave
= new Progress();
575 pg
.addProgress(pgGetStory
, 1);
576 pg
.addProgress(pgSave
, 1);
578 Story story
= other
.getStory(luid
, pgGetStory
);
580 story
= this.save(story
, luid
, pgSave
);
584 throw new IOException("Cannot find story in Library: " + luid
);
589 * Export the {@link Story} to the given target in the given format.
592 * the {@link Story} ID
594 * the {@link OutputType} to transform it to
596 * the target to save to
598 * the optional progress reporter
600 * @return the saved resource (the main saved {@link File})
602 * @throws IOException
603 * in case of I/O error
605 public File
export(String luid
, OutputType type
, String target
, Progress pg
)
607 Progress pgGetStory
= new Progress();
608 Progress pgOut
= new Progress();
611 pg
.addProgress(pgGetStory
, 1);
612 pg
.addProgress(pgOut
, 1);
615 BasicOutput out
= BasicOutput
.getOutput(type
, false, false);
617 throw new IOException("Output type not supported: " + type
);
620 Story story
= getStory(luid
, pgGetStory
);
622 throw new IOException("Cannot find story to export: " + luid
);
625 return out
.process(story
, target
, pgOut
);
629 * Save a {@link Story} to the {@link BasicLibrary}.
632 * the {@link Story} to save
634 * the optional progress reporter
636 * @return the same {@link Story}, whose LUID may have changed
638 * @throws IOException
639 * in case of I/O error
641 public Story
save(Story story
, Progress pg
) throws IOException
{
642 return save(story
, null, pg
);
646 * Save a {@link Story} to the {@link BasicLibrary} -- the LUID <b>must</b>
647 * be correct, or NULL to get the next free one.
649 * Will override any previous {@link Story} with the same LUID.
652 * the {@link Story} to save
654 * the <b>correct</b> LUID or NULL to get the next free one
656 * the optional progress reporter
658 * @return the same {@link Story}, whose LUID may have changed
660 * @throws IOException
661 * in case of I/O error
663 public synchronized Story
save(Story story
, String luid
, Progress pg
)
666 Instance
.getTraceHandler().trace(
667 this.getClass().getSimpleName() + ": saving story " + luid
);
669 // Do not change the original metadata, but change the original story
670 MetaData meta
= story
.getMeta().clone();
673 if (luid
== null || luid
.isEmpty()) {
674 meta
.setLuid(String
.format("%03d", getNextId()));
679 if (luid
!= null && getInfo(luid
) != null) {
683 story
= doSave(story
, pg
);
685 updateInfo(story
.getMeta());
687 Instance
.getTraceHandler().trace(
688 this.getClass().getSimpleName() + ": story saved (" + luid
695 * Delete the given {@link Story} from this {@link BasicLibrary}.
698 * the LUID of the target {@link Story}
700 * @throws IOException
701 * in case of I/O error
703 public synchronized void delete(String luid
) throws IOException
{
704 Instance
.getTraceHandler().trace(
705 this.getClass().getSimpleName() + ": deleting story " + luid
);
710 Instance
.getTraceHandler().trace(
711 this.getClass().getSimpleName() + ": story deleted (" + luid
716 * Change the type (source) of the given {@link Story}.
719 * the {@link Story} LUID
723 * the optional progress reporter
725 * @throws IOException
726 * in case of I/O error or if the {@link Story} was not found
728 public synchronized void changeSource(String luid
, String newSource
,
729 Progress pg
) throws IOException
{
730 MetaData meta
= getInfo(luid
);
732 throw new IOException("Story not found: " + luid
);
735 meta
.setSource(newSource
);
740 * Save back the current state of the {@link MetaData} (LUID <b>MUST NOT</b>
741 * change) for this {@link Story}.
743 * By default, delete the old {@link Story} then recreate a new
746 * Note that this behaviour can lead to data loss.
749 * the new {@link MetaData} (LUID <b>MUST NOT</b> change)
751 * the optional {@link Progress}
753 * @throws IOException
754 * in case of I/O error or if the {@link Story} was not found
756 protected synchronized void saveMeta(MetaData meta
, Progress pg
)
762 Progress pgGet
= new Progress();
763 Progress pgSet
= new Progress();
764 pg
.addProgress(pgGet
, 50);
765 pg
.addProgress(pgSet
, 50);
767 Story story
= getStory(meta
.getLuid(), pgGet
);
769 throw new IOException("Story not found: " + meta
.getLuid());
772 delete(meta
.getLuid());
775 save(story
, meta
.getLuid(), pgSet
);