Merge branch 'master' into subtree
[nikiroo-utils.git] / library / BasicLibrary.java
index 7f286079ed1fe87c88f2ec798a85c26774950089..f77d0edcef5ae64b186942be2a6048fa7eca7745 100644 (file)
@@ -4,11 +4,9 @@ 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.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.TreeMap;
 
 import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.data.MetaData;
@@ -43,11 +41,11 @@ abstract public class BasicLibrary {
                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. */
+               /** The library is invalid, and will never work as is. */
+               INVALID,
+               /** The library is currently out of commission, but may work later. */
                UNAVAILABLE;
 
                /**
@@ -98,7 +96,7 @@ abstract public class BasicLibrary {
         * Do <b>NOT</b> alter this file.
         * 
         * @param luid
-        *            the Library UID of the story
+        *            the Library UID of the story, can be NULL
         * @param pg
         *            the optional {@link Progress}
         * 
@@ -122,13 +120,25 @@ abstract public class BasicLibrary {
         */
        public abstract Image getCover(String luid) throws IOException;
 
-       // TODO: ensure it is the main used interface
-       public synchronized MetaResultList getList(Progress pg) throws IOException {
+       /**
+        * Retrieve the list of {@link MetaData} known by this {@link BasicLibrary}
+        * in a easy-to-filter version.
+        * 
+        * @param pg
+        *            the optional {@link Progress}
+        * @return the list of {@link MetaData} as a {@link MetaResultList} you can
+        *         query
+        * @throws IOException
+        *             in case of I/O eror
+        */
+       public MetaResultList getList(Progress pg) throws IOException {
+               // TODO: ensure it is the main used interface
+
                return new MetaResultList(getMetas(pg));
        }
-       
-       //TODO: make something for (normal and custom) not-story covers
-       
+
+       // TODO: make something for (normal and custom) non-story covers
+
        /**
         * Return the cover image associated to this source.
         * <p>
@@ -301,7 +311,7 @@ abstract public class BasicLibrary {
         * 
         * @return the next luid
         */
-       protected abstract int getNextId();
+       protected abstract String getNextId();
 
        /**
         * Delete the target {@link Story}.
@@ -338,28 +348,28 @@ abstract public class BasicLibrary {
         * @param pg
         *            the optional progress reporter
         */
-       public synchronized void refresh(Progress pg) {
+       public void refresh(Progress pg) {
                try {
                        getMetas(pg);
                } catch (IOException e) {
                        // We will let it fail later
                }
        }
-       
+
        /**
         * Check if the {@link Story} denoted by this Library UID is present in the
-        * cache (if we have no cache, we default to </t>true</tt>).
+        * cache (if we have no cache, we default to </tt>true</tt>).
         * 
         * @param luid
         *            the Library UID
         * 
         * @return TRUE if it is
         */
-       public boolean isCached(String luid) {
+       public boolean isCached(@SuppressWarnings("unused") String luid) {
                // By default, everything is cached
                return true;
        }
-       
+
        /**
         * Clear the {@link Story} from the cache, if needed.
         * <p>
@@ -372,219 +382,57 @@ abstract public class BasicLibrary {
         * @throws IOException
         *             in case of I/O error
         */
+       @SuppressWarnings("unused")
        public void clearFromCache(String luid) throws IOException {
                // By default, this is a noop.
        }
 
        /**
-        * List all the known types (sources) of stories.
-        * 
-        * @return the sources
-        * 
+        * @return the same as getList()
         * @throws IOException
-        *             in case of IOException
+        *             in case of I/O error
+        * @deprecated please use {@link BasicLibrary#getList()} and
+        *             {@link MetaResultList#getSources()} instead.
         */
-       public synchronized List<String> getSources() throws IOException {
-               List<String> list = new ArrayList<String>();
-               for (MetaData meta : getMetas(null)) {
-                       String storySource = meta.getSource();
-                       if (!list.contains(storySource)) {
-                               list.add(storySource);
-                       }
-               }
-
-               Collections.sort(list);
-               return list;
+       @Deprecated
+       public List<String> getSources() throws IOException {
+               return getList().getSources();
        }
 
        /**
-        * List all the known types (sources) of stories, grouped by directory
-        * ("Source_1/a" and "Source_1/b" will be grouped into "Source_1").
-        * <p>
-        * Note that an empty item in the list means a non-grouped source (type) --
-        * e.g., you could have for Source_1:
-        * <ul>
-        * <li><tt></tt>: empty, so source is "Source_1"</li>
-        * <li><tt>a</tt>: empty, so source is "Source_1/a"</li>
-        * <li><tt>b</tt>: empty, so source is "Source_1/b"</li>
-        * </ul>
-        * 
-        * @return the grouped list
-        * 
+        * @return the same as getList()
         * @throws IOException
-        *             in case of IOException
+        *             in case of I/O error
+        * @deprecated please use {@link BasicLibrary#getList()} and
+        *             {@link MetaResultList#getSourcesGrouped()} instead.
         */
-       public synchronized Map<String, List<String>> getSourcesGrouped()
-                       throws IOException {
-               Map<String, List<String>> map = new TreeMap<String, List<String>>();
-               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<String> list = map.get(name);
-                       if (list == null) {
-                               list = new ArrayList<String>();
-                               map.put(name, list);
-                       }
-                       list.add(subname);
-               }
-
-               return map;
+       @Deprecated
+       public Map<String, List<String>> getSourcesGrouped() throws IOException {
+               return getList().getSourcesGrouped();
        }
 
        /**
-        * List all the known authors of stories.
-        * 
-        * @return the authors
-        * 
+        * @return the same as getList()
         * @throws IOException
-        *             in case of IOException
+        *             in case of I/O error
+        * @deprecated please use {@link BasicLibrary#getList()} and
+        *             {@link MetaResultList#getAuthors()} instead.
         */
-       public synchronized List<String> getAuthors() throws IOException {
-               List<String> list = new ArrayList<String>();
-               for (MetaData meta : getMetas(null)) {
-                       String storyAuthor = meta.getAuthor();
-                       if (!list.contains(storyAuthor)) {
-                               list.add(storyAuthor);
-                       }
-               }
-
-               Collections.sort(list);
-               return list;
+       @Deprecated
+       public List<String> getAuthors() throws IOException {
+               return getList().getAuthors();
        }
 
        /**
-        * Return the list of authors, grouped by starting letter(s) if needed.
-        * <p>
-        * If the number of author is not too high, only one group with an empty
-        * name and all the authors will be returned.
-        * <p>
-        * If not, the authors will be separated into groups:
-        * <ul>
-        * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
-        * </li>
-        * <li><tt>0-9</tt>: any authors whose name starts with a number</li>
-        * <li><tt>A-C</tt> (for instance): any author whose name starts with
-        * <tt>A</tt>, <tt>B</tt> or <tt>C</tt></li>
-        * </ul>
-        * Note that the letters used in the groups can vary (except <tt>*</tt> and
-        * <tt>0-9</tt>, which may only be present or not).
-        * 
-        * @return the authors' names, grouped by letter(s)
-        * 
+        * @return the same as getList()
         * @throws IOException
-        *             in case of IOException
+        *             in case of I/O error
+        * @deprecated please use {@link BasicLibrary#getList()} and
+        *             {@link MetaResultList#getAuthorsGrouped()} instead.
         */
+       @Deprecated
        public Map<String, List<String>> getAuthorsGrouped() throws IOException {
-               int MAX = 20;
-
-               Map<String, List<String>> groups = new TreeMap<String, List<String>>();
-               List<String> 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<String> keys = new ArrayList<String>(groups.keySet());
-               for (int i = 0; i + 1 < keys.size(); i++) {
-                       String keyNow = keys.get(i);
-                       String keyNext = keys.get(i + 1);
-
-                       List<String> now = groups.get(keyNow);
-                       List<String> next = groups.get(keyNext);
-
-                       int currentTotal = now.size() + next.size();
-                       if (currentTotal <= MAX) {
-                               String key = keyNow.charAt(0) + "-"
-                                               + keyNext.charAt(keyNext.length() - 1);
-
-                               List<String> all = new ArrayList<String>();
-                               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<String>(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:
-        * <ul>
-        * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
-        * </li>
-        * <li><tt>0</tt>: any authors whose name starts with a number</li>
-        * <li><tt>A</tt> (any capital latin letter): any author whose name starts
-        * with <tt>A</tt></li>
-        * </ul>
-        * 
-        * @param authors
-        *            the full list of authors
-        * @param car
-        *            the starting character, <tt>*</tt>, <tt>0</tt> or a capital
-        *            letter
-        * 
-        * @return the authors that fulfil the starting letter
-        */
-       private List<String> getAuthorsGroup(List<String> authors, char car) {
-               List<String> accepted = new ArrayList<String>();
-               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;
+               return getList().getAuthorsGrouped();
        }
 
        /**
@@ -606,14 +454,14 @@ abstract public class BasicLibrary {
         * cover image <b>MAY</b> not be included.
         * 
         * @param luid
-        *            the Library UID of the story
+        *            the Library UID of the story, can be NULL
         * 
-        * @return the corresponding {@link Story}
+        * @return the corresponding {@link Story} or NULL if not found
         * 
         * @throws IOException
         *             in case of IOException
         */
-       public synchronized MetaData getInfo(String luid) throws IOException {
+       public MetaData getInfo(String luid) throws IOException {
                if (luid != null) {
                        for (MetaData meta : getMetas(null)) {
                                if (luid.equals(meta.getLuid())) {
@@ -627,6 +475,8 @@ abstract public class BasicLibrary {
 
        /**
         * Retrieve a specific {@link Story}.
+        * <p>
+        * Note that it will update both the cover and the resume in <tt>meta</tt>.
         * 
         * @param luid
         *            the Library UID of the story
@@ -638,8 +488,7 @@ abstract public class BasicLibrary {
         * @throws IOException
         *             in case of IOException
         */
-       public synchronized Story getStory(String luid, Progress pg)
-                       throws IOException {
+       public Story getStory(String luid, Progress pg) throws IOException {
                Progress pgMetas = new Progress();
                Progress pgStory = new Progress();
                if (pg != null) {
@@ -666,8 +515,12 @@ abstract public class BasicLibrary {
 
        /**
         * Retrieve a specific {@link Story}.
+        * <p>
+        * Note that it will update both the cover and the resume in <tt>meta</tt>.
         * 
         * @param luid
+        *            the LUID of the story
+        * @param meta
         *            the meta of the story
         * @param pg
         *            the optional progress reporter
@@ -677,8 +530,7 @@ abstract public class BasicLibrary {
         * @throws IOException
         *             in case of IOException
         */
-       public synchronized Story getStory(String luid,
-                       @SuppressWarnings("javadoc") MetaData meta, Progress pg)
+       public synchronized Story getStory(String luid, MetaData meta, Progress pg)
                        throws IOException {
 
                if (pg == null) {
@@ -693,12 +545,21 @@ abstract public class BasicLibrary {
                pg.addProgress(pgProcess, 1);
 
                Story story = null;
-               File file = getFile(luid, pgGet);
+               File file = null;
+
+               if (luid != null && meta != null) {
+                       file = getFile(luid, pgGet);
+               }
+
                pgGet.done();
                try {
-                       SupportType type = SupportType.valueOfAllOkUC(meta.getType());
-                       URL url = file.toURI().toURL();
-                       if (type != null) {
+                       if (file != null) {
+                               SupportType type = SupportType.valueOfAllOkUC(meta.getType());
+                               if (type == null) {
+                                       throw new IOException("Unknown type: " + meta.getType());
+                               }
+
+                               URL url = file.toURI().toURL();
                                story = BasicSupport.getSupport(type, url) //
                                                .process(pgProcess);
 
@@ -706,15 +567,13 @@ abstract public class BasicLibrary {
                                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));
+                       // 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();
@@ -740,6 +599,28 @@ abstract public class BasicLibrary {
         *             in case of I/O error
         */
        public MetaData imprt(URL url, Progress pg) throws IOException {
+               return imprt(url, null, pg);
+       }
+
+       /**
+        * Import the {@link Story} at the given {@link URL} into the
+        * {@link BasicLibrary}.
+        * 
+        * @param url
+        *            the {@link URL} to import
+        * @param luid
+        *            the LUID to use
+        * @param pg
+        *            the optional progress reporter
+        * 
+        * @return the imported Story {@link MetaData}
+        * 
+        * @throws UnknownHostException
+        *             if the host is not supported
+        * @throws IOException
+        *             in case of I/O error
+        */
+       MetaData imprt(URL url, String luid, Progress pg) throws IOException {
                if (pg == null)
                        pg = new Progress();
 
@@ -754,8 +635,7 @@ abstract public class BasicLibrary {
                        throw new UnknownHostException("" + url);
                }
 
-               Story story = save(support.process(pgProcess), pgSave);
-               pg.setName(story.getMeta().getTitle());
+               Story story = save(support.process(pgProcess), luid, pgSave);
                pg.done();
 
                return story.getMeta();
@@ -876,17 +756,18 @@ abstract public class BasicLibrary {
                if (pg == null) {
                        pg = new Progress();
                }
-               
-               Instance.getInstance().getTraceHandler().trace(this.getClass().getSimpleName() + ": saving story " + luid);
+
+               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);
 
                pg.setName("Saving story");
-               
+
                if (luid == null || luid.isEmpty()) {
-                       meta.setLuid(String.format("%03d", getNextId()));
+                       meta.setLuid(getNextId());
                } else {
                        meta.setLuid(luid);
                }
@@ -900,7 +781,8 @@ abstract public class BasicLibrary {
                updateInfo(story.getMeta());
 
                Instance.getInstance().getTraceHandler()
-                               .trace(this.getClass().getSimpleName() + ": story saved (" + luid + ")");
+                               .trace(this.getClass().getSimpleName() + ": story saved ("
+                                               + luid + ")");
 
                pg.setName(meta.getTitle());
                pg.done();
@@ -917,14 +799,15 @@ 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);
+               Instance.getInstance().getTraceHandler().trace(
+                               this.getClass().getSimpleName() + ": deleting story " + luid);
 
                doDelete(luid);
                invalidateInfo(luid);
 
                Instance.getInstance().getTraceHandler()
-                               .trace(this.getClass().getSimpleName() + ": story deleted (" + luid
-                                               + ")");
+                               .trace(this.getClass().getSimpleName() + ": story deleted ("
+                                               + luid + ")");
        }
 
        /**
@@ -1067,4 +950,49 @@ abstract public class BasicLibrary {
 
                pg.done();
        }
+
+       /**
+        * Describe a {@link Story} from its {@link MetaData} and return a list of
+        * title/value that represent this {@link Story}.
+        * 
+        * @param meta
+        *            the {@link MetaData} to represent
+        * 
+        * @return the information, translated and sorted
+        */
+       static public Map<String, String> getMetaDesc(MetaData meta) {
+               Map<String, String> metaDesc = new LinkedHashMap<String, String>();
+
+               // TODO: i18n
+
+               StringBuilder tags = new StringBuilder();
+               for (String tag : meta.getTags()) {
+                       if (tags.length() > 0) {
+                               tags.append(", ");
+                       }
+                       tags.append(tag);
+               }
+
+               // TODO: i18n
+               metaDesc.put("Author", meta.getAuthor());
+               metaDesc.put("Published on", meta.getPublisher());
+               metaDesc.put("Publication date", meta.getDate());
+               metaDesc.put("Creation date", meta.getCreationDate());
+               String count = "";
+               if (meta.getWords() > 0) {
+                       count = StringUtils.formatNumber(meta.getWords());
+               }
+               if (meta.isImageDocument()) {
+                       metaDesc.put("Number of images", count);
+               } else {
+                       metaDesc.put("Number of words", count);
+               }
+               metaDesc.put("Source", meta.getSource());
+               metaDesc.put("Subject", meta.getSubject());
+               metaDesc.put("Language", meta.getLang());
+               metaDesc.put("Tags", tags.toString());
+               metaDesc.put("URL", meta.getUrl());
+
+               return metaDesc;
+       }
 }