X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fbe%2Fnikiroo%2Ffanfix%2Flibrary%2FBasicLibrary.java;h=c558384d380526819f36a8dac9553b89d33517ca;hb=d66deb8d8b30cff6b54db352eef34a3508939f84;hp=b11e1ec5276324743b2ea64fb0697d616567c7d4;hpb=ff05b8284e6e415b13d3543650075d0f7cd27ff5;p=nikiroo-utils.git diff --git a/src/be/nikiroo/fanfix/library/BasicLibrary.java b/src/be/nikiroo/fanfix/library/BasicLibrary.java index b11e1ec..c558384 100644 --- a/src/be/nikiroo/fanfix/library/BasicLibrary.java +++ b/src/be/nikiroo/fanfix/library/BasicLibrary.java @@ -1,12 +1,14 @@ package be.nikiroo.fanfix.library; -import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.net.URL; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.TreeMap; import be.nikiroo.fanfix.Instance; import be.nikiroo.fanfix.data.MetaData; @@ -14,8 +16,10 @@ import be.nikiroo.fanfix.data.Story; import be.nikiroo.fanfix.output.BasicOutput; import be.nikiroo.fanfix.output.BasicOutput.OutputType; import be.nikiroo.fanfix.supported.BasicSupport; -import be.nikiroo.fanfix.supported.BasicSupport.SupportType; +import be.nikiroo.fanfix.supported.SupportType; +import be.nikiroo.utils.Image; import be.nikiroo.utils.Progress; +import be.nikiroo.utils.StringUtils; /** * Manage a library of Stories: import, export, list, modify. @@ -29,6 +33,44 @@ import be.nikiroo.utils.Progress; * @author niki */ abstract public class BasicLibrary { + /** + * A {@link BasicLibrary} status. + * + * @author niki + */ + public enum Status { + /** The library is ready and r/w. */ + READ_WRITE, + /** The library is ready, but read-only. */ + READ_ONLY, + /** The library is invalid (not correctly set up). */ + INVALID, + /** You are not allowed to access this library. */ + UNAUTHORIZED, + /** The library is currently out of commission. */ + UNAVAILABLE; + + /** + * The library is available (you can query it). + *

+ * It does not specify if it is read-only or not. + * + * @return TRUE if it is + */ + public boolean isReady() { + return (this == READ_WRITE || this == READ_ONLY); + } + + /** + * This library can be modified (= you are allowed to modify it). + * + * @return TRUE if it is + */ + public boolean isWritable() { + return (this == READ_WRITE); + } + } + /** * Return a name for this library (the UI may display this). *

@@ -40,6 +82,15 @@ abstract public class BasicLibrary { return ""; } + /** + * The library status. + * + * @return the current status + */ + public Status getStatus() { + return Status.READ_WRITE; + } + /** * Retrieve the main {@link File} corresponding to the given {@link Story}, * which can be passed to an external reader or instance. @@ -52,8 +103,11 @@ abstract public class BasicLibrary { * the optional {@link Progress} * * @return the corresponding {@link Story} + * + * @throws IOException + * in case of IOException */ - public abstract File getFile(String luid, Progress pg); + public abstract File getFile(String luid, Progress pg) throws IOException; /** * Return the cover image associated to this story. @@ -62,20 +116,32 @@ abstract public class BasicLibrary { * the Library UID of the story * * @return the cover image + * + * @throws IOException + * in case of IOException */ - public abstract BufferedImage getCover(String luid); + public abstract Image getCover(String luid) throws IOException; /** * Return the cover image associated to this source. *

- * By default, return the cover of the first story with this source. + * By default, return the custom cover if any, and if not, return the cover + * of the first story with this source. * * @param source * the source * * @return the cover image or NULL + * + * @throws IOException + * in case of IOException */ - public BufferedImage getSourceCover(String source) { + public Image getSourceCover(String source) throws IOException { + Image custom = getCustomSourceCover(source); + if (custom != null) { + return custom; + } + List metas = getListBySource(source); if (metas.size() > 0) { return getCover(metas.get(0).getLuid()); @@ -85,14 +151,96 @@ abstract public class BasicLibrary { } /** - * Fix the source cover to the given story cover. + * Return the cover image associated to this author. + *

+ * By default, return the custom cover if any, and if not, return the cover + * of the first story with this author. + * + * @param author + * the author + * + * @return the cover image or NULL + * + * @throws IOException + * in case of IOException + */ + public Image getAuthorCover(String author) throws IOException { + Image custom = getCustomAuthorCover(author); + if (custom != null) { + return custom; + } + + List metas = getListByAuthor(author); + if (metas.size() > 0) { + return getCover(metas.get(0).getLuid()); + } + + return null; + } + + /** + * Return the custom cover image associated to this source. + *

+ * By default, return NULL. + * + * @param source + * the source to look for + * + * @return the custom cover or NULL if none + * + * @throws IOException + * in case of IOException + */ + @SuppressWarnings("unused") + public Image getCustomSourceCover(String source) throws IOException { + return null; + } + + /** + * Return the custom cover image associated to this author. + *

+ * By default, return NULL. + * + * @param author + * the author to look for + * + * @return the custom cover or NULL if none + * + * @throws IOException + * in case of IOException + */ + @SuppressWarnings("unused") + public Image getCustomAuthorCover(String author) throws IOException { + return null; + } + + /** + * Set the source cover to the given story cover. * * @param source * the source to change * @param luid * the story LUID + * + * @throws IOException + * in case of IOException */ - public abstract void setSourceCover(String source, String luid); + public abstract void setSourceCover(String source, String luid) + throws IOException; + + /** + * Set the author cover to the given story cover. + * + * @param author + * the author to change + * @param luid + * the story LUID + * + * @throws IOException + * in case of IOException + */ + public abstract void setAuthorCover(String author, String luid) + throws IOException; /** * Return the list of stories (represented by their {@link MetaData}, which @@ -102,14 +250,42 @@ abstract public class BasicLibrary { * the optional {@link Progress} * * @return the list (can be empty but not NULL) + * + * @throws IOException + * in case of IOException */ - protected abstract List getMetas(Progress pg); + protected abstract List getMetas(Progress pg) throws IOException; /** * Invalidate the {@link Story} cache (when the content should be re-read * because it was changed). */ - protected abstract void clearCache(); + protected void invalidateInfo() { + invalidateInfo(null); + } + + /** + * Invalidate the {@link Story} cache (when the content is removed). + *

+ * All the cache can be deleted if NULL is passed as meta. + * + * @param luid + * the LUID of the {@link Story} to clear from the cache, or NULL + * for all stories + */ + protected abstract void invalidateInfo(String luid); + + /** + * Invalidate the {@link Story} cache (when the content has changed, but we + * already have it) with the new given meta. + * + * @param meta + * the {@link Story} to clear from the cache + * + * @throws IOException + * in case of IOException + */ + protected abstract void updateInfo(MetaData meta) throws IOException; /** * Return the next LUID that can be used. @@ -147,25 +323,17 @@ abstract public class BasicLibrary { throws IOException; /** - * Refresh the {@link BasicLibrary}, that is, make sure all stories are + * Refresh the {@link BasicLibrary}, that is, make sure all metas are * loaded. * - * @param full - * force the full content of the stories to be loaded, not just - * the {@link MetaData} - * * @param pg * the optional progress reporter */ - public void refresh(boolean full, Progress pg) { - if (full) { - // TODO: progress - List metas = getMetas(pg); - for (MetaData meta : metas) { - getStory(meta.getLuid(), null); - } - } else { + public void refresh(Progress pg) { + try { getMetas(pg); + } catch (IOException e) { + // We will let it fail later } } @@ -173,8 +341,11 @@ abstract public class BasicLibrary { * List all the known types (sources) of stories. * * @return the sources + * + * @throws IOException + * in case of IOException */ - public synchronized List getSources() { + public synchronized List getSources() throws IOException { List list = new ArrayList(); for (MetaData meta : getMetas(null)) { String storySource = meta.getSource(); @@ -187,12 +358,60 @@ abstract public class BasicLibrary { return list; } + /** + * List all the known types (sources) of stories, grouped by directory + * ("Source_1/a" and "Source_1/b" will be grouped into "Source_1"). + *

+ * Note that an empty item in the list means a non-grouped source (type) -- + * e.g., you could have for Source_1: + *

+ * + * @return the grouped list + * + * @throws IOException + * in case of IOException + */ + public synchronized Map> getSourcesGrouped() + throws IOException { + Map> map = new TreeMap>(); + for (String source : getSources()) { + String name; + String subname; + + int pos = source.indexOf('/'); + if (pos > 0 && pos < source.length() - 1) { + name = source.substring(0, pos); + subname = source.substring(pos + 1); + + } else { + name = source; + subname = ""; + } + + List list = map.get(name); + if (list == null) { + list = new ArrayList(); + map.put(name, list); + } + list.add(subname); + } + + return map; + } + /** * List all the known authors of stories. * * @return the authors + * + * @throws IOException + * in case of IOException */ - public synchronized List getAuthors() { + public synchronized List getAuthors() throws IOException { List list = new ArrayList(); for (MetaData meta : getMetas(null)) { String storyAuthor = meta.getAuthor(); @@ -205,14 +424,141 @@ abstract public class BasicLibrary { return list; } + /** + * Return the list of authors, grouped by starting letter(s) if needed. + *

+ * If the number of author is not too high, only one group with an empty + * name and all the authors will be returned. + *

+ * If not, the authors will be separated into groups: + *

    + *
  • *: any author whose name doesn't contain letters nor numbers + *
  • + *
  • 0-9: any authors whose name starts with a number
  • + *
  • A-C (for instance): any author whose name starts with + * A, B or C
  • + *
+ * Note that the letters used in the groups can vary (except * and + * 0-9, which may only be present or not). + * + * @return the authors' names, grouped by letter(s) + * + * @throws IOException + * in case of IOException + */ + public Map> getAuthorsGrouped() throws IOException { + int MAX = 20; + + Map> groups = new TreeMap>(); + List authors = getAuthors(); + + // If all authors fit the max, just report them as is + if (authors.size() <= MAX) { + groups.put("", authors); + return groups; + } + + // Create groups A to Z, which can be empty here + for (char car = 'A'; car <= 'Z'; car++) { + groups.put(Character.toString(car), getAuthorsGroup(authors, car)); + } + + // Collapse them + List keys = new ArrayList(groups.keySet()); + for (int i = 0; i + 1 < keys.size(); i++) { + String keyNow = keys.get(i); + String keyNext = keys.get(i + 1); + + List now = groups.get(keyNow); + List next = groups.get(keyNext); + + int currentTotal = now.size() + next.size(); + if (currentTotal <= MAX) { + String key = keyNow.charAt(0) + "-" + + keyNext.charAt(keyNext.length() - 1); + + List all = new ArrayList(); + all.addAll(now); + all.addAll(next); + + groups.remove(keyNow); + groups.remove(keyNext); + groups.put(key, all); + + keys.set(i, key); // set the new key instead of key(i) + keys.remove(i + 1); // remove the next, consumed key + i--; // restart at key(i) + } + } + + // Add "special" groups + groups.put("*", getAuthorsGroup(authors, '*')); + groups.put("0-9", getAuthorsGroup(authors, '0')); + + // Prune empty groups + keys = new ArrayList(groups.keySet()); + for (String key : keys) { + if (groups.get(key).isEmpty()) { + groups.remove(key); + } + } + + return groups; + } + + /** + * Get all the authors that start with the given character: + *
    + *
  • *: any author whose name doesn't contain letters nor numbers + *
  • + *
  • 0: any authors whose name starts with a number
  • + *
  • A (any capital latin letter): any author whose name starts + * with A
  • + *
+ * + * @param authors + * the full list of authors + * @param car + * the starting character, *, 0 or a capital + * letter + * + * @return the authors that fulfil the starting letter + */ + private List getAuthorsGroup(List authors, char car) { + List accepted = new ArrayList(); + for (String author : authors) { + char first = '*'; + for (int i = 0; first == '*' && i < author.length(); i++) { + String san = StringUtils.sanitize(author, true, true); + char c = san.charAt(i); + if (c >= '0' && c <= '9') { + first = '0'; + } else if (c >= 'a' && c <= 'z') { + first = (char) (c - 'a' + 'A'); + } else if (c >= 'A' && c <= 'Z') { + first = c; + } + } + + if (first == car) { + accepted.add(author); + } + } + + return accepted; + } + /** * List all the stories in the {@link BasicLibrary}. *

- * Cover images not included. + * Cover images MAYBE not included. * * @return the stories + * + * @throws IOException + * in case of IOException */ - public synchronized List getList() { + public synchronized List getList() throws IOException { return getMetas(null); } @@ -226,8 +572,12 @@ abstract public class BasicLibrary { * the type of story to retrieve, or NULL for all * * @return the stories + * + * @throws IOException + * in case of IOException */ - public synchronized List getListBySource(String type) { + public synchronized List getListBySource(String type) + throws IOException { List list = new ArrayList(); for (MetaData meta : getMetas(null)) { String storyType = meta.getSource(); @@ -250,8 +600,12 @@ abstract public class BasicLibrary { * the author of the stories to retrieve, or NULL for all * * @return the stories + * + * @throws IOException + * in case of IOException */ - public synchronized List getListByAuthor(String author) { + public synchronized List getListByAuthor(String author) + throws IOException { List list = new ArrayList(); for (MetaData meta : getMetas(null)) { String storyAuthor = meta.getAuthor(); @@ -272,8 +626,11 @@ abstract public class BasicLibrary { * the Library UID of the story * * @return the corresponding {@link Story} + * + * @throws IOException + * in case of IOException */ - public synchronized MetaData getInfo(String luid) { + public synchronized MetaData getInfo(String luid) throws IOException { if (luid != null) { for (MetaData meta : getMetas(null)) { if (luid.equals(meta.getLuid())) { @@ -294,8 +651,53 @@ abstract public class BasicLibrary { * the optional progress reporter * * @return the corresponding {@link Story} or NULL if not found + * + * @throws IOException + * in case of IOException + */ + public synchronized Story getStory(String luid, Progress pg) + throws IOException { + Progress pgMetas = new Progress(); + Progress pgStory = new Progress(); + if (pg != null) { + pg.setMinMax(0, 100); + pg.addProgress(pgMetas, 10); + pg.addProgress(pgStory, 90); + } + + MetaData meta = null; + for (MetaData oneMeta : getMetas(pgMetas)) { + if (oneMeta.getLuid().equals(luid)) { + meta = oneMeta; + break; + } + } + + pgMetas.done(); + + Story story = getStory(luid, meta, pgStory); + pgStory.done(); + + return story; + } + + /** + * Retrieve a specific {@link Story}. + * + * @param luid + * the meta of the story + * @param pg + * the optional progress reporter + * + * @return the corresponding {@link Story} or NULL if not found + * + * @throws IOException + * in case of IOException */ - public synchronized Story getStory(String luid, Progress pg) { + public synchronized Story getStory(String luid, + @SuppressWarnings("javadoc") MetaData meta, Progress pg) + throws IOException { + if (pg == null) { pg = new Progress(); } @@ -308,32 +710,31 @@ abstract public class BasicLibrary { pg.addProgress(pgProcess, 1); Story story = null; - for (MetaData meta : getMetas(null)) { - if (meta.getLuid().equals(luid)) { - File file = getFile(luid, pgGet); - pgGet.done(); - try { - SupportType type = SupportType.valueOfAllOkUC(meta - .getType()); - URL url = file.toURI().toURL(); - if (type != null) { - story = BasicSupport.getSupport(type).process(url, - pgProcess); - } else { - throw new IOException("Unknown type: " + meta.getType()); - } - } catch (IOException e) { - // We should not have not-supported files in the - // library - Instance.syserr(new IOException( - "Cannot load file from library: " + file, e)); - } finally { - pgProcess.done(); - pg.done(); - } + File file = getFile(luid, pgGet); + pgGet.done(); + try { + SupportType type = SupportType.valueOfAllOkUC(meta.getType()); + URL url = file.toURI().toURL(); + if (type != null) { + story = BasicSupport.getSupport(type, url) // + .process(pgProcess); - break; + // Because we do not want to clear the meta cache: + meta.setCover(story.getMeta().getCover()); + meta.setResume(story.getMeta().getResume()); + story.setMeta(meta); + // + } else { + throw new IOException("Unknown type: " + meta.getType()); } + } catch (IOException e) { + // We should not have not-supported files in the + // library + Instance.getInstance().getTraceHandler().error(new IOException( + String.format("Cannot load file of type '%s' from library: %s", meta.getType(), file), e)); + } finally { + pgProcess.done(); + pg.done(); } return story; @@ -348,18 +749,32 @@ abstract public class BasicLibrary { * @param pg * the optional progress reporter * - * @return the imported {@link Story} + * @return the imported Story {@link MetaData} * + * @throws UnknownHostException + * if the host is not supported * @throws IOException * in case of I/O error */ - public Story imprt(URL url, Progress pg) throws IOException { + public MetaData imprt(URL url, Progress pg) throws IOException { + if (pg == null) + pg = new Progress(); + + pg.setMinMax(0, 1000); + Progress pgProcess = new Progress(); + Progress pgSave = new Progress(); + pg.addProgress(pgProcess, 800); + pg.addProgress(pgSave, 200); + BasicSupport support = BasicSupport.getSupport(url); if (support == null) { - throw new IOException("URL not supported: " + url.toString()); + throw new UnknownHostException("" + url); } - return save(support.process(url, pg), null); + Story story = save(support.process(pgProcess), pgSave); + pg.done(); + + return story.getMeta(); } /** @@ -424,7 +839,7 @@ abstract public class BasicLibrary { pg.addProgress(pgOut, 1); } - BasicOutput out = BasicOutput.getOutput(type, false); + BasicOutput out = BasicOutput.getOutput(type, false, false); if (out == null) { throw new IOException("Output type not supported: " + type); } @@ -474,6 +889,9 @@ abstract public class BasicLibrary { */ public synchronized Story save(Story story, String luid, Progress pg) throws IOException { + + Instance.getInstance().getTraceHandler().trace(this.getClass().getSimpleName() + ": saving story " + luid); + // Do not change the original metadata, but change the original story MetaData meta = story.getMeta().clone(); story.setMeta(meta); @@ -484,13 +902,16 @@ abstract public class BasicLibrary { meta.setLuid(luid); } - if (getInfo(luid) != null) { + if (luid != null && getInfo(luid) != null) { delete(luid); } - doSave(story, pg); + story = doSave(story, pg); + + updateInfo(story.getMeta()); - clearCache(); + Instance.getInstance().getTraceHandler() + .trace(this.getClass().getSimpleName() + ": story saved (" + luid + ")"); return story; } @@ -505,8 +926,14 @@ abstract public class BasicLibrary { * in case of I/O error */ public synchronized void delete(String luid) throws IOException { + Instance.getInstance().getTraceHandler().trace(this.getClass().getSimpleName() + ": deleting story " + luid); + doDelete(luid); - clearCache(); + invalidateInfo(luid); + + Instance.getInstance().getTraceHandler() + .trace(this.getClass().getSimpleName() + ": story deleted (" + luid + + ")"); } /** @@ -529,8 +956,86 @@ abstract public class BasicLibrary { throw new IOException("Story not found: " + luid); } + changeSTA(luid, newSource, meta.getTitle(), meta.getAuthor(), pg); + } + + /** + * Change the title (name) of the given {@link Story}. + * + * @param luid + * the {@link Story} LUID + * @param newTitle + * the new title + * @param pg + * the optional progress reporter + * + * @throws IOException + * in case of I/O error or if the {@link Story} was not found + */ + public synchronized void changeTitle(String luid, String newTitle, + Progress pg) throws IOException { + MetaData meta = getInfo(luid); + if (meta == null) { + throw new IOException("Story not found: " + luid); + } + + changeSTA(luid, meta.getSource(), newTitle, meta.getAuthor(), pg); + } + + /** + * Change the author of the given {@link Story}. + * + * @param luid + * the {@link Story} LUID + * @param newAuthor + * the new author + * @param pg + * the optional progress reporter + * + * @throws IOException + * in case of I/O error or if the {@link Story} was not found + */ + public synchronized void changeAuthor(String luid, String newAuthor, + Progress pg) throws IOException { + MetaData meta = getInfo(luid); + if (meta == null) { + throw new IOException("Story not found: " + luid); + } + + changeSTA(luid, meta.getSource(), meta.getTitle(), newAuthor, pg); + } + + /** + * Change the Source, Title and Author of the {@link Story} in one single + * go. + * + * @param luid + * the {@link Story} LUID + * @param newSource + * the new source + * @param newTitle + * the new title + * @param newAuthor + * the new author + * @param pg + * the optional progress reporter + * + * @throws IOException + * in case of I/O error or if the {@link Story} was not found + */ + protected synchronized void changeSTA(String luid, String newSource, + String newTitle, String newAuthor, Progress pg) throws IOException { + MetaData meta = getInfo(luid); + if (meta == null) { + throw new IOException("Story not found: " + luid); + } + meta.setSource(newSource); + meta.setTitle(newTitle); + meta.setAuthor(newAuthor); saveMeta(meta, pg); + + invalidateInfo(luid); } /** @@ -540,7 +1045,7 @@ abstract public class BasicLibrary { * By default, delete the old {@link Story} then recreate a new * {@link Story}. *

- * Note that this behaviour can lead to data loss. + * Note that this behaviour can lead to data loss in case of problems! * * @param meta * the new {@link MetaData} (LUID MUST NOT change) @@ -566,8 +1071,8 @@ abstract public class BasicLibrary { throw new IOException("Story not found: " + meta.getLuid()); } + // TODO: this is not safe! delete(meta.getLuid()); - story.setMeta(meta); save(story, meta.getLuid(), pgSet);