Add a test for Library (before changing it)
[fanfix.git] / src / be / nikiroo / fanfix / Library.java
index 4db3868b5928701ecda2a53e6f522294aee00cc9..3868bd3fee03d946214db55f8594f14bf34a5887 100644 (file)
@@ -1,6 +1,8 @@
 package be.nikiroo.fanfix;
 
+import java.awt.image.BufferedImage;
 import java.io.File;
+import java.io.FileFilter;
 import java.io.IOException;
 import java.net.URL;
 import java.util.ArrayList;
@@ -15,6 +17,7 @@ import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.fanfix.output.BasicOutput;
 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
+import be.nikiroo.fanfix.output.InfoCover;
 import be.nikiroo.fanfix.supported.BasicSupport;
 import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
 import be.nikiroo.fanfix.supported.InfoReader;
@@ -26,12 +29,17 @@ import be.nikiroo.utils.Progress;
  * <p>
  * Each {@link Story} object will be associated with a (local to the library)
  * unique ID, the LUID, which will be used to identify the {@link Story}.
+ * <p>
+ * Most of the {@link Library} functions work on either the LUID or a partial
+ * (cover not included) {@link MetaData} object.
  * 
  * @author niki
  */
 public class Library {
-       private File baseDir;
-       private Map<MetaData, File> stories;
+       protected File baseDir;
+       protected boolean localSpeed;
+       protected Map<MetaData, File> stories;
+
        private int lastId;
        private OutputType text;
        private OutputType image;
@@ -47,8 +55,10 @@ public class Library {
         *            the {@link OutputType} to save the images-focused stories into
         */
        public Library(File dir, OutputType text, OutputType image) {
+               this();
+
                this.baseDir = dir;
-               this.stories = new HashMap<MetaData, File>();
+
                this.lastId = 0;
                this.text = text;
                this.image = image;
@@ -57,37 +67,101 @@ public class Library {
        }
 
        /**
-        * List all the known types of stories.
+        * Create a new {@link Library} with no link to the local machine.
+        * <p>
+        * Reserved for extensions.
+        */
+       protected Library() {
+               this.stories = new HashMap<MetaData, File>();
+       }
+
+       /**
+        * Refresh the {@link Library}, that is, make sure all stories are loaded.
+        * 
+        * @param pg
+        *            the optional progress reporter
+        */
+       public void refresh(Progress pg) {
+               getStories(pg);
+       }
+
+       /**
+        * List all the known types (sources) of stories.
         * 
         * @return the types
         */
        public synchronized List<String> getTypes() {
                List<String> list = new ArrayList<String>();
-               for (Entry<MetaData, File> entry : getStories().entrySet()) {
-                       String storyType = entry.getValue().getParentFile().getName();
+               for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
+                       String storyType = entry.getKey().getSource();
                        if (!list.contains(storyType)) {
                                list.add(storyType);
                        }
                }
 
+               Collections.sort(list);
+               return list;
+       }
+
+       /**
+        * List all the known authors of stories.
+        * 
+        * @return the authors
+        */
+       public synchronized List<String> getAuthors() {
+               List<String> list = new ArrayList<String>();
+               for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
+                       String storyAuthor = entry.getKey().getAuthor();
+                       if (!list.contains(storyAuthor)) {
+                               list.add(storyAuthor);
+                       }
+               }
+
+               Collections.sort(list);
+               return list;
+       }
+
+       /**
+        * List all the stories of the given author in the {@link Library}, or all
+        * the stories if NULL is passed as an author.
+        * <p>
+        * Cover images not included.
+        * 
+        * @param author
+        *            the author of the stories to retrieve, or NULL for all
+        * 
+        * @return the stories
+        */
+       public synchronized List<MetaData> getListByAuthor(String author) {
+               List<MetaData> list = new ArrayList<MetaData>();
+               for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
+                       String storyAuthor = entry.getKey().getAuthor();
+                       if (author == null || author.equalsIgnoreCase(storyAuthor)) {
+                               list.add(entry.getKey());
+                       }
+               }
+
+               Collections.sort(list);
                return list;
        }
 
        /**
         * List all the stories of the given source type in the {@link Library}, or
         * all the stories if NULL is passed as a type.
+        * <p>
+        * Cover images not included.
         * 
         * @param type
         *            the type of story to retrieve, or NULL for all
         * 
         * @return the stories
         */
-       public synchronized List<MetaData> getList(String type) {
+       public synchronized List<MetaData> getListBySource(String type) {
                List<MetaData> list = new ArrayList<MetaData>();
-               for (Entry<MetaData, File> entry : getStories().entrySet()) {
-                       String storyType = entry.getValue().getParentFile().getName();
+               for (MetaData meta : getStories(null).keySet()) {
+                       String storyType = meta.getSource();
                        if (type == null || type.equalsIgnoreCase(storyType)) {
-                               list.add(entry.getKey());
+                               list.add(meta);
                        }
                }
 
@@ -96,7 +170,8 @@ public class Library {
        }
 
        /**
-        * Retrieve a {@link File} corresponding to the given {@link Story}.
+        * Retrieve a {@link MetaData} corresponding to the given {@link Story},
+        * cover image <b>MAY</b> not be included.
         * 
         * @param luid
         *            the Library UID of the story
@@ -105,7 +180,7 @@ public class Library {
         */
        public synchronized MetaData getInfo(String luid) {
                if (luid != null) {
-                       for (Entry<MetaData, File> entry : getStories().entrySet()) {
+                       for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
                                if (luid.equals(entry.getKey().getLuid())) {
                                        return entry.getKey();
                                }
@@ -125,7 +200,7 @@ public class Library {
         */
        public synchronized File getFile(String luid) {
                if (luid != null) {
-                       for (Entry<MetaData, File> entry : getStories().entrySet()) {
+                       for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
                                if (luid.equals(entry.getKey().getLuid())) {
                                        return entry.getValue();
                                }
@@ -135,6 +210,31 @@ public class Library {
                return null;
        }
 
+       /**
+        * Return the cover image associated to this story.
+        * 
+        * @param luid
+        *            the Library UID of the story
+        * 
+        * @return the cover image
+        */
+       public synchronized BufferedImage getCover(String luid) {
+               MetaData meta = getInfo(luid);
+               if (meta != null) {
+                       getFile(luid); // to help remote implementation
+                       try {
+                               File infoFile = new File(getExpectedFile(meta).getPath()
+                                               + ".info");
+                               meta = readMeta(infoFile, true).getKey();
+                               return meta.getCover();
+                       } catch (IOException e) {
+                               Instance.syserr(e);
+                       }
+               }
+
+               return null;
+       }
+
        /**
         * Retrieve a specific {@link Story}.
         * 
@@ -147,25 +247,26 @@ public class Library {
         */
        public synchronized Story getStory(String luid, Progress pg) {
                if (luid != null) {
-                       for (Entry<MetaData, File> entry : getStories().entrySet()) {
+                       for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
                                if (luid.equals(entry.getKey().getLuid())) {
+                                       MetaData meta = entry.getKey();
+                                       File file = getFile(luid); // to help remote implementation
                                        try {
-                                               SupportType type = SupportType.valueOfAllOkUC(entry
-                                                               .getKey().getType());
-                                               URL url = entry.getValue().toURI().toURL();
+                                               SupportType type = SupportType.valueOfAllOkUC(meta
+                                                               .getType());
+                                               URL url = file.toURI().toURL();
                                                if (type != null) {
                                                        return BasicSupport.getSupport(type).process(url,
                                                                        pg);
                                                } else {
                                                        throw new IOException("Unknown type: "
-                                                                       + entry.getKey().getType());
+                                                                       + meta.getType());
                                                }
                                        } catch (IOException e) {
                                                // We should not have not-supported files in the
                                                // library
                                                Instance.syserr(new IOException(
-                                                               "Cannot load file from library: "
-                                                                               + entry.getValue().getPath(), e));
+                                                               "Cannot load file from library: " + file, e));
                                        }
                                }
                        }
@@ -282,14 +383,14 @@ public class Library {
                story.setMeta(key);
 
                if (luid == null || luid.isEmpty()) {
-                       getStories(); // refresh lastId if needed
+                       getStories(null); // refresh lastId if needed
                        key.setLuid(String.format("%03d", (++lastId)));
                } else {
                        key.setLuid(luid);
                }
 
-               getDir(key).mkdirs();
-               if (!getDir(key).exists()) {
+               getExpectedDir(key.getSource()).mkdirs();
+               if (!getExpectedDir(key.getSource()).exists()) {
                        throw new IOException("Cannot create library dir");
                }
 
@@ -301,7 +402,7 @@ public class Library {
                }
 
                BasicOutput it = BasicOutput.getOutput(out, true);
-               it.process(story, getFile(key).getPath(), pg);
+               it.process(story, getExpectedFile(key).getPath(), pg);
 
                // empty cache
                stories.clear();
@@ -320,63 +421,149 @@ public class Library {
        public synchronized boolean delete(String luid) {
                boolean ok = false;
 
+               List<File> files = getFiles(luid);
+               if (!files.isEmpty()) {
+                       for (File file : files) {
+                               IOUtils.deltree(file);
+                       }
+
+                       ok = true;
+
+                       // clear cache
+                       stories.clear();
+               }
+
+               return ok;
+       }
+
+       /**
+        * Change the type (source) of the given {@link Story}.
+        * 
+        * @param luid
+        *            the {@link Story} LUID
+        * @param newType
+        *            the new type
+        * 
+        * @return TRUE if the {@link Story} was found
+        */
+       public synchronized boolean changeType(String luid, String newType) {
                MetaData meta = getInfo(luid);
-               File file = getStories().get(meta);
+               if (meta != null) {
+                       meta.setSource(newType);
+                       File newDir = getExpectedDir(meta.getSource());
+                       if (!newDir.exists()) {
+                               newDir.mkdir();
+                       }
 
-               if (file != null) {
-                       if (file.delete()) {
-                               String readerExt = getOutputType(meta)
-                                               .getDefaultExtension(true);
-                               String fileExt = getOutputType(meta).getDefaultExtension(false);
-
-                               String path = file.getAbsolutePath();
-                               if (readerExt != null && !readerExt.equals(fileExt)) {
-                                       path = path
-                                                       .substring(0, path.length() - readerExt.length())
-                                                       + fileExt;
-                                       file = new File(path);
-                                       IOUtils.deltree(file);
+                       List<File> files = getFiles(luid);
+                       for (File file : files) {
+                               if (file.getName().endsWith(".info")) {
+                                       try {
+                                               String name = file.getName().replaceFirst("\\.info$",
+                                                               "");
+                                               InfoCover.writeInfo(newDir, name, meta);
+                                               file.delete();
+                                       } catch (IOException e) {
+                                               Instance.syserr(e);
+                                       }
+                               } else {
+                                       file.renameTo(new File(newDir, file.getName()));
                                }
+                       }
 
-                               File infoFile = new File(path + ".info");
-                               if (!infoFile.exists()) {
-                                       infoFile = new File(path.substring(0, path.length()
-                                                       - fileExt.length())
-                                                       + ".info");
-                               }
-                               infoFile.delete();
-
-                               String coverExt = "."
-                                               + Instance.getConfig().getString(
-                                                               Config.IMAGE_FORMAT_COVER);
-                               File coverFile = new File(path + coverExt);
-                               if (!coverFile.exists()) {
-                                       coverFile = new File(path.substring(0, path.length()
-                                                       - fileExt.length()));
+                       // clear cache
+                       stories.clear();
+
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * The library is accessed locally or at local speed (for operations like
+        * {@link Library#getFile(String)}).
+        * <p>
+        * It could be cached, too, it is only about the access speed.
+        * 
+        * @return TRUE if it is accessed locally
+        */
+       public boolean isLocalSpeed() {
+               return localSpeed;
+       }
+
+       /**
+        * Return the list of files/dirs on disk for this {@link Story}.
+        * <p>
+        * If the {@link Story} is not found, and empty list is returned.
+        * 
+        * @param luid
+        *            the {@link Story} LUID
+        * 
+        * @return the list of {@link File}s
+        */
+       private List<File> getFiles(String luid) {
+               List<File> files = new ArrayList<File>();
+
+               MetaData meta = getInfo(luid);
+               File file = getFile(luid); // to help remote implementation
+
+               if (file != null) {
+                       files.add(file);
+
+                       String readerExt = getOutputType(meta).getDefaultExtension(true);
+                       String fileExt = getOutputType(meta).getDefaultExtension(false);
+
+                       String path = file.getAbsolutePath();
+                       if (readerExt != null && !readerExt.equals(fileExt)) {
+                               path = path.substring(0, path.length() - readerExt.length())
+                                               + fileExt;
+                               file = new File(path);
+
+                               if (file.exists()) {
+                                       files.add(file);
                                }
-                               coverFile.delete();
+                       }
 
-                               ok = true;
+                       File infoFile = new File(path + ".info");
+                       if (!infoFile.exists()) {
+                               infoFile = new File(path.substring(0,
+                                               path.length() - fileExt.length())
+                                               + ".info");
                        }
 
-                       // clear cache
-                       stories.clear();
+                       if (infoFile.exists()) {
+                               files.add(infoFile);
+                       }
+
+                       String coverExt = "."
+                                       + Instance.getConfig().getString(Config.IMAGE_FORMAT_COVER);
+                       File coverFile = new File(path + coverExt);
+                       if (!coverFile.exists()) {
+                               coverFile = new File(path.substring(0,
+                                               path.length() - fileExt.length())
+                                               + coverExt);
+                       }
+
+                       if (coverFile.exists()) {
+                               files.add(coverFile);
+                       }
                }
 
-               return ok;
+               return files;
        }
 
        /**
         * The directory (full path) where the {@link Story} related to this
         * {@link MetaData} should be located on disk.
         * 
-        * @param key
-        *            the {@link Story} {@link MetaData}
+        * @param type
+        *            the type (source)
         * 
         * @return the target directory
         */
-       private File getDir(MetaData key) {
-               String source = key.getSource().replaceAll("[^a-zA-Z0-9._+-]", "_");
+       private File getExpectedDir(String type) {
+               String source = type.replaceAll("[^a-zA-Z0-9._+-]", "_");
                return new File(baseDir, source);
        }
 
@@ -389,69 +576,122 @@ public class Library {
         * 
         * @return the target
         */
-       private File getFile(MetaData key) {
-               String title = key.getTitle().replaceAll("[^a-zA-Z0-9._+-]", "_");
-               return new File(getDir(key), key.getLuid() + "_" + title);
+       private File getExpectedFile(MetaData key) {
+               String title = key.getTitle();
+               if (title == null) {
+                       title = "";
+               }
+               title = title.replaceAll("[^a-zA-Z0-9._+-]", "_");
+               return new File(getExpectedDir(key.getSource()), key.getLuid() + "_"
+                               + title);
        }
 
        /**
         * Return all the known stories in this {@link Library} object.
         * 
+        * @param pg
+        *            the optional progress reporter
+        * 
         * @return the stories
         */
-       private synchronized Map<MetaData, File> getStories() {
+       protected synchronized Map<MetaData, File> getStories(Progress pg) {
+               if (pg == null) {
+                       pg = new Progress();
+               } else {
+                       pg.setMinMax(0, 100);
+               }
+
                if (stories.isEmpty()) {
                        lastId = 0;
 
-                       String ext = ".info";
-                       for (File dir : baseDir.listFiles()) {
-                               if (dir.isDirectory()) {
-                                       for (File file : dir.listFiles()) {
+                       File[] dirs = baseDir.listFiles(new FileFilter() {
+                               public boolean accept(File file) {
+                                       return file != null && file.isDirectory();
+                               }
+                       });
+
+                       Progress pgDirs = new Progress(0, 100 * dirs.length);
+                       pg.addProgress(pgDirs, 100);
+
+                       for (File dir : dirs) {
+                               File[] files = dir.listFiles(new FileFilter() {
+                                       public boolean accept(File file) {
+                                               return file != null
+                                                               && file.getPath().toLowerCase()
+                                                                               .endsWith(".info");
+                                       }
+                               });
+
+                               Progress pgFiles = new Progress(0, files.length);
+                               pgDirs.addProgress(pgFiles, 100);
+                               pgDirs.setName("Loading from: " + dir.getName());
+
+                               for (File file : files) {
+                                       pgFiles.setName(file.getName());
+                                       try {
+                                               Entry<MetaData, File> entry = readMeta(file, false);
                                                try {
-                                                       if (file.getPath().toLowerCase().endsWith(ext)) {
-                                                               MetaData meta = InfoReader.readMeta(file);
-                                                               try {
-                                                                       int id = Integer.parseInt(meta.getLuid());
-                                                                       if (id > lastId) {
-                                                                               lastId = id;
-                                                                       }
-
-                                                                       // Replace .info with whatever is needed:
-                                                                       String path = file.getPath();
-                                                                       path = path.substring(0, path.length()
-                                                                                       - ext.length());
-
-                                                                       String newExt = getOutputType(meta)
-                                                                                       .getDefaultExtension(true);
-
-                                                                       file = new File(path + newExt);
-                                                                       //
-
-                                                                       stories.put(meta, file);
-
-                                                               } catch (Exception e) {
-                                                                       // not normal!!
-                                                                       Instance.syserr(new IOException(
-                                                                                       "Cannot understand the LUID of "
-                                                                                                       + file.getPath() + ": "
-                                                                                                       + meta.getLuid(), e));
-                                                               }
+                                                       int id = Integer.parseInt(entry.getKey().getLuid());
+                                                       if (id > lastId) {
+                                                               lastId = id;
                                                        }
-                                               } catch (IOException e) {
-                                                       // We should not have not-supported files in the
-                                                       // library
-                                                       Instance.syserr(new IOException(
-                                                                       "Cannot load file from library: "
-                                                                                       + file.getPath(), e));
+
+                                                       stories.put(entry.getKey(), entry.getValue());
+                                               } catch (Exception e) {
+                                                       // not normal!!
+                                                       throw new IOException(
+                                                                       "Cannot understand the LUID of "
+                                                                                       + file.getPath() + ": "
+                                                                                       + entry.getKey().getLuid(), e);
                                                }
+                                       } catch (IOException e) {
+                                               // We should not have not-supported files in the
+                                               // library
+                                               Instance.syserr(new IOException(
+                                                               "Cannot load file from library: "
+                                                                               + file.getPath(), e));
                                        }
+                                       pgFiles.add(1);
                                }
+
+                               pgFiles.setName(null);
                        }
+
+                       pgDirs.setName("Loading directories");
                }
 
                return stories;
        }
 
+       private Entry<MetaData, File> readMeta(File infoFile, boolean withCover)
+                       throws IOException {
+
+               final MetaData meta = InfoReader.readMeta(infoFile, withCover);
+
+               // Replace .info with whatever is needed:
+               String path = infoFile.getPath();
+               path = path.substring(0, path.length() - ".info".length());
+
+               String newExt = getOutputType(meta).getDefaultExtension(true);
+
+               File targetFile = new File(path + newExt);
+
+               final File ffile = targetFile;
+               return new Entry<MetaData, File>() {
+                       public File setValue(File value) {
+                               return null;
+                       }
+
+                       public File getValue() {
+                               return ffile;
+                       }
+
+                       public MetaData getKey() {
+                               return meta;
+                       }
+               };
+       }
+
        /**
         * Return the {@link OutputType} for this {@link Story}.
         *