weblib: fix delete and imprt
[fanfix.git] / src / be / nikiroo / fanfix / library / BasicLibrary.java
index 430fb2026a256510bb7705eea22ce799005ab48e..8b72f19b3a92ad1c1d7ada09d915440596330ee2 100644 (file)
@@ -1,12 +1,12 @@
 package be.nikiroo.fanfix.library;
 
-import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.IOException;
 import java.net.URL;
-import java.util.ArrayList;
-import java.util.Collections;
+import java.net.UnknownHostException;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.data.MetaData;
@@ -14,8 +14,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.
@@ -35,14 +37,36 @@ abstract public class BasicLibrary {
         * @author niki
         */
        public enum Status {
-               /** The library is ready. */
-               READY,
-               /** The library is invalid (not correctly set up). */
-               INVALID,
+               /** The library is ready and r/w. */
+               READ_WRITE,
+               /** The library is ready, but read-only. */
+               READ_ONLY,
                /** You are not allowed to access this library. */
-               UNAUTORIZED,
-               /** The library is currently out of commission. */
-               UNAVAILABLE,
+               UNAUTHORIZED,
+               /** The library is invalid, and will never work as is. */
+               INVALID,
+               /** The library is currently out of commission, but may work later. */
+               UNAVAILABLE;
+
+               /**
+                * The library is available (you can query it).
+                * <p>
+                * It does <b>not</b> 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);
+               }
        }
 
        /**
@@ -62,7 +86,7 @@ abstract public class BasicLibrary {
         * @return the current status
         */
        public Status getStatus() {
-               return Status.READY;
+               return Status.READ_WRITE;
        }
 
        /**
@@ -72,13 +96,16 @@ 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}
         * 
         * @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.
@@ -87,21 +114,80 @@ 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;
+
+       /**
+        * 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) non-story covers
 
        /**
         * Return the cover image associated to this source.
         * <p>
-        * 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 Image getSourceCover(String source) throws IOException {
+               Image custom = getCustomSourceCover(source);
+               if (custom != null) {
+                       return custom;
+               }
+
+               List<MetaData> metas = getList().filter(source, null, null);
+               if (metas.size() > 0) {
+                       return getCover(metas.get(0).getLuid());
+               }
+
+               return null;
+       }
+
+       /**
+        * Return the cover image associated to this author.
+        * <p>
+        * 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 BufferedImage getSourceCover(String source) {
-               List<MetaData> metas = getListBySource(source);
+       public Image getAuthorCover(String author) throws IOException {
+               Image custom = getCustomAuthorCover(author);
+               if (custom != null) {
+                       return custom;
+               }
+
+               List<MetaData> metas = getList().filter(null, author, null);
                if (metas.size() > 0) {
                        return getCover(metas.get(0).getLuid());
                }
@@ -110,38 +196,122 @@ abstract public class BasicLibrary {
        }
 
        /**
-        * Fix the source cover to the given story cover.
+        * Return the custom cover image associated to this source.
+        * <p>
+        * 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.
+        * <p>
+        * 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
         * <b>MAY</b> not have the cover included).
+        * <p>
+        * The returned list <b>MUST</b> be a copy, not the original one.
         * 
         * @param pg
         *            the optional {@link Progress}
         * 
         * @return the list (can be empty but not NULL)
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       protected abstract List<MetaData> getMetas(Progress pg);
+       protected abstract List<MetaData> 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).
+        * <p>
+        * 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.
         * 
         * @return the next luid
         */
-       protected abstract int getNextId();
+       protected abstract String getNextId();
 
        /**
         * Delete the target {@link Story}.
@@ -179,102 +349,104 @@ abstract public class BasicLibrary {
         *            the optional progress reporter
         */
        public void refresh(Progress pg) {
-               getMetas(pg);
+               try {
+                       getMetas(pg);
+               } catch (IOException e) {
+                       // We will let it fail later
+               }
        }
 
        /**
-        * List all the known types (sources) of stories.
+        * Check if the {@link Story} denoted by this Library UID is present in the
+        * cache (if we have no cache, we default to </tt>true</tt>).
         * 
-        * @return the sources
+        * @param luid
+        *            the Library UID
+        * 
+        * @return TRUE if it is
         */
-       public synchronized List<String> getSources() {
-               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;
+       public boolean isCached(@SuppressWarnings("unused") String luid) {
+               // By default, everything is cached
+               return true;
        }
 
        /**
-        * List all the known authors of stories.
+        * Clear the {@link Story} from the cache, if needed.
+        * <p>
+        * The next time we try to retrieve the {@link Story}, it may be required to
+        * cache it again.
+        * 
+        * @param luid
+        *            the story to clear
         * 
-        * @return the authors
+        * @throws IOException
+        *             in case of I/O error
         */
-       public synchronized List<String> getAuthors() {
-               List<String> list = new ArrayList<String>();
-               for (MetaData meta : getMetas(null)) {
-                       String storyAuthor = meta.getAuthor();
-                       if (!list.contains(storyAuthor)) {
-                               list.add(storyAuthor);
-                       }
-               }
+       @SuppressWarnings("unused")
+       public void clearFromCache(String luid) throws IOException {
+               // By default, this is a noop.
+       }
 
-               Collections.sort(list);
-               return list;
+       /**
+        * @return the same as getList()
+        * @throws IOException
+        *             in case of I/O error
+        * @deprecated please use {@link BasicLibrary#getList()} and
+        *             {@link MetaResultList#getSources()} instead.
+        */
+       @Deprecated
+       public List<String> getSources() throws IOException {
+               return getList().getSources();
        }
 
        /**
-        * List all the stories in the {@link BasicLibrary}.
-        * <p>
-        * Cover images not included.
-        * 
-        * @return the stories
+        * @return the same as getList()
+        * @throws IOException
+        *             in case of I/O error
+        * @deprecated please use {@link BasicLibrary#getList()} and
+        *             {@link MetaResultList#getSourcesGrouped()} instead.
         */
-       public synchronized List<MetaData> getList() {
-               return getMetas(null);
+       @Deprecated
+       public Map<String, List<String>> getSourcesGrouped() throws IOException {
+               return getList().getSourcesGrouped();
        }
 
        /**
-        * List all the stories of the given source type in the {@link BasicLibrary}
-        * , 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
+        * @return the same as getList()
+        * @throws IOException
+        *             in case of I/O error
+        * @deprecated please use {@link BasicLibrary#getList()} and
+        *             {@link MetaResultList#getAuthors()} instead.
         */
-       public synchronized List<MetaData> getListBySource(String type) {
-               List<MetaData> list = new ArrayList<MetaData>();
-               for (MetaData meta : getMetas(null)) {
-                       String storyType = meta.getSource();
-                       if (type == null || type.equalsIgnoreCase(storyType)) {
-                               list.add(meta);
-                       }
-               }
+       @Deprecated
+       public List<String> getAuthors() throws IOException {
+               return getList().getAuthors();
+       }
 
-               Collections.sort(list);
-               return list;
+       /**
+        * @return the same as getList()
+        * @throws 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 {
+               return getList().getAuthorsGrouped();
        }
 
        /**
-        * List all the stories of the given author in the {@link BasicLibrary}, or
-        * all the stories if NULL is passed as an author.
+        * List all the stories in the {@link BasicLibrary}.
         * <p>
-        * Cover images not included.
-        * 
-        * @param author
-        *            the author of the stories to retrieve, or NULL for all
+        * Cover images <b>MAYBE</b> not included.
         * 
         * @return the stories
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public synchronized List<MetaData> getListByAuthor(String author) {
-               List<MetaData> list = new ArrayList<MetaData>();
-               for (MetaData meta : getMetas(null)) {
-                       String storyAuthor = meta.getAuthor();
-                       if (author == null || author.equalsIgnoreCase(storyAuthor)) {
-                               list.add(meta);
-                       }
-               }
-
-               Collections.sort(list);
-               return list;
+       public MetaResultList getList() throws IOException {
+               return getList(null);
        }
 
        /**
@@ -282,11 +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) {
+       public MetaData getInfo(String luid) throws IOException {
                if (luid != null) {
                        for (MetaData meta : getMetas(null)) {
                                if (luid.equals(meta.getLuid())) {
@@ -307,8 +482,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 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 LUID of the story
+        * @param meta
+        *            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, MetaData meta, Progress pg)
+                       throws IOException {
+
                if (pg == null) {
                        pg = new Progress();
                }
@@ -321,37 +541,38 @@ 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);
-                                               // Because we do not want to clear the meta cache:
-                                               meta.setCover(story.getMeta().getCover());
-                                               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.getTraceHandler().error(
-                                                       new IOException("Cannot load file from library: "
-                                                                       + file, e));
-                               } finally {
-                                       pgProcess.done();
-                                       pg.done();
+               File file = null;
+
+               if (luid != null && meta != null) {
+                       file = getFile(luid, pgGet);
+               }
+
+               pgGet.done();
+               try {
+                       if (file != null) {
+                               SupportType type = SupportType.valueOfAllOkUC(meta.getType());
+                               if (type == null) {
+                                       throw new IOException("Unknown type: " + meta.getType());
                                }
 
-                               break;
+                               URL url = file.toURI().toURL();
+                               story = BasicSupport.getSupport(type, url) //
+                                               .process(pgProcess);
+
+                               // Because we do not want to clear the meta cache:
+                               meta.setCover(story.getMeta().getCover());
+                               meta.setResume(story.getMeta().getResume());
+                               story.setMeta(meta);
                        }
+               } 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;
@@ -366,18 +587,54 @@ 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 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
         */
-       public Story imprt(URL url, Progress pg) throws IOException {
+       MetaData imprt(URL url, String luid, 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), luid, pgSave);
+               pg.done();
+
+               return story.getMeta();
        }
 
        /**
@@ -442,7 +699,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);
                }
@@ -492,24 +749,39 @@ abstract public class BasicLibrary {
         */
        public synchronized Story save(Story story, String luid, Progress pg)
                        throws IOException {
+               if (pg == null) {
+                       pg = new Progress();
+               }
+
+               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);
                }
 
-               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 + ")");
 
+               pg.setName(meta.getTitle());
+               pg.done();
                return story;
        }
 
@@ -523,8 +795,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);
+
                doDelete(luid);
-               clearCache();
+               invalidateInfo(luid);
+
+               Instance.getInstance().getTraceHandler()
+                               .trace(this.getClass().getSimpleName() + ": story deleted ("
+                                               + luid + ")");
        }
 
        /**
@@ -547,7 +826,83 @@ 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);
        }
 
@@ -558,7 +913,7 @@ abstract public class BasicLibrary {
         * By default, delete the old {@link Story} then recreate a new
         * {@link Story}.
         * <p>
-        * 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 <b>MUST NOT</b> change)
@@ -584,11 +939,56 @@ 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);
 
                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;
+       }
 }