update from master
authorNiki Roo <niki@nikiroo.be>
Wed, 13 May 2020 21:28:47 +0000 (23:28 +0200)
committerNiki Roo <niki@nikiroo.be>
Wed, 13 May 2020 21:28:47 +0000 (23:28 +0200)
bundles/Config.java
library/BasicLibrary.java
library/RemoteLibrary.java
library/RemoteLibraryServer.java
library/WebLibrary.java
library/WebLibraryServer.java
library/WebLibraryUrls.java [new file with mode: 0644]

index c96ed22a93e47cf91374e525f38011866f6413fe..86744b45b8d169d4d66e2e7dfa884d1d72f479a5 100644 (file)
@@ -102,9 +102,12 @@ public enum Config {
        @Meta(description = "Allow write access to the clients (download story, move story...) without RW subkeys", //
        format = Format.BOOLEAN, def = "true")
        SERVER_RW, //
-       @Meta(description = "If not empty, only the EXACT listed sources will be available for clients without BL subkeys",//
+       @Meta(description = "If not empty, only the EXACT listed sources will be available for clients without a WL subkey",//
        array = true, format = Format.STRING, def = "")
        SERVER_WHITELIST, //
+       @Meta(description = "Those sources will not be available for clients without a BL subkey",//
+       array = true, format = Format.STRING, def = "")
+       SERVER_BLACKLIST, //
        @Meta(description = "The subkeys that the server will allow, including the modes\nA subkey is used as a login for HTTP (clear text protocol) and HTTPS modes", //
        array = true, format = Format.STRING, def = "")
        SERVER_ALLOWED_SUBKEYS, //
index 78f0f6208d098dcaf410e85c631dc3808330a2bd..af7920bf14efdde77dbd2bebe77489f505b6fa30 100644 (file)
@@ -41,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;
 
                /**
index 9fa8c66190174bd1a92536b995d181a16bc8744a..3a60e02c9ed6fc845193574f897c3bd9b0ecc60a 100644 (file)
@@ -62,7 +62,7 @@ public class RemoteLibrary extends BasicLibrary {
         * Create a {@link RemoteLibrary} linked to the given server.
         * <p>
         * Note that the key is structured:
-        * <tt><b><i>xxx</i></b>(|<b><i>yyy</i></b>|<b>wl</b>)(|<b>rw</b>)</tt>
+        * <tt><b><i>xxx</i></b>(|<b><i>yyy</i></b>(|<b>wl</b>)(|<b>bl</b>)(|<b>rw</b>)</tt>
         * <p>
         * Note that anything before the first pipe (<tt>|</tt>) character is
         * considered to be the encryption key, anything after that character is
@@ -77,16 +77,16 @@ public class RemoteLibrary extends BasicLibrary {
         * <li><b><i>yyy</i></b>: the secondary key</li>
         * <li><b>rw</b>: flag to allow read and write access if it is not the
         * default on this server</li>
-        * <li><b>wl</b>: flag to allow access to all the stories (bypassing the
-        * whitelist if it exists)</li>
+        * <li><b>bl</b>: flag to bypass the blacklist (if it exists)</li>
+        * <li><b>wl</b>: flag to bypass the whitelist if it exists</li>
         * </ul>
         * <p>
         * Some examples:
         * <ul>
         * <li><b>my_key</b>: normal connection, will take the default server
         * options</li>
-        * <li><b>my_key|agzyzz|wl</b>: will ask to bypass the white list (if it
-        * exists)</li>
+        * <li><b>my_key|agzyzz|wl|bl</b>: will ask to bypass the black list and the
+        * white list (if it exists)</li>
         * <li><b>my_key|agzyzz|rw</b>: will ask read-write access (if the default
         * is read-only)</li>
         * <li><b>my_key|agzyzz|wl|rw</b>: will ask both read-write access and white
@@ -141,8 +141,7 @@ public class RemoteLibrary extends BasicLibrary {
        private Status getStatusDo() {
                final Status[] result = new Status[1];
 
-               result[0] = Status.INVALID;
-
+               result[0] = null;
                try {
                        new RemoteConnectAction() {
                                @Override
index c150a01b6f8a14f68279fc4bb7358f3374a85e10..e54f105d6bf9233c4f1c4383c948630b57b79555 100644 (file)
@@ -37,7 +37,8 @@ import be.nikiroo.utils.serial.server.ServerObject;
  * "r/w")</li>
  * <li>GET_METADATA *: will return the metadata of all the stories in the
  * library (array)</li> *
- * <li>GET_METADATA [luid]: will return the metadata of the story of LUID luid</li>
+ * <li>GET_METADATA [luid]: will return the metadata of the story of LUID
+ * luid</li>
  * <li>GET_STORY [luid]: will return the given story if it exists (or NULL if
  * not)</li>
  * <li>SAVE_STORY [luid]: save the story (that must be sent just after the
@@ -61,6 +62,7 @@ public class RemoteLibraryServer extends ServerObject {
        private Map<Long, String> commands = new HashMap<Long, String>();
        private Map<Long, Long> times = new HashMap<Long, Long>();
        private Map<Long, Boolean> wls = new HashMap<Long, Boolean>();
+       private Map<Long, Boolean> bls = new HashMap<Long, Boolean>();
        private Map<Long, Boolean> rws = new HashMap<Long, Boolean>();
 
        /**
@@ -79,7 +81,7 @@ public class RemoteLibraryServer extends ServerObject {
                                                .getInteger(Config.SERVER_PORT),
                                Instance.getInstance().getConfig()
                                                .getString(Config.SERVER_KEY));
-               
+
                setTraceHandler(Instance.getInstance().getTraceHandler());
        }
 
@@ -91,6 +93,7 @@ public class RemoteLibraryServer extends ServerObject {
                // defaults are positive (as previous versions without the feature)
                boolean rw = true;
                boolean wl = true;
+               boolean bl = true;
 
                String subkey = "";
                String command = "";
@@ -110,18 +113,26 @@ public class RemoteLibraryServer extends ServerObject {
                        }
                }
 
-               List<String> whitelist = Instance.getInstance().getConfig().getList(Config.SERVER_WHITELIST);
+               List<String> whitelist = Instance.getInstance().getConfig()
+                               .getList(Config.SERVER_WHITELIST);
                if (whitelist == null) {
                        whitelist = new ArrayList<String>();
                }
+               List<String> blacklist = Instance.getInstance().getConfig()
+                               .getList(Config.SERVER_BLACKLIST);
+               if (blacklist == null) {
+                       blacklist = new ArrayList<String>();
+               }
 
                if (whitelist.isEmpty()) {
                        wl = false;
                }
 
-               rw = Instance.getInstance().getConfig().getBoolean(Config.SERVER_RW, rw);
+               rw = Instance.getInstance().getConfig().getBoolean(Config.SERVER_RW,
+                               rw);
                if (!subkey.isEmpty()) {
-                       List<String> allowed = Instance.getInstance().getConfig().getList(Config.SERVER_ALLOWED_SUBKEYS);
+                       List<String> allowed = Instance.getInstance().getConfig()
+                                       .getList(Config.SERVER_ALLOWED_SUBKEYS);
                        if (allowed.contains(subkey)) {
                                if ((subkey + "|").contains("|rw|")) {
                                        rw = true;
@@ -130,10 +141,14 @@ public class RemoteLibraryServer extends ServerObject {
                                        wl = false; // |wl| = bypass whitelist
                                        whitelist = new ArrayList<String>();
                                }
+                               if ((subkey + "|").contains("|bl|")) {
+                                       bl = false; // |bl| = bypass blacklist
+                                       blacklist = new ArrayList<String>();
+                               }
                        }
                }
 
-               String mode = display(wl, rw);
+               String mode = display(wl, bl, rw);
 
                String trace = mode + "[ " + command + "] ";
                for (Object arg : args) {
@@ -144,20 +159,21 @@ public class RemoteLibraryServer extends ServerObject {
 
                Object rep = null;
                try {
-                       rep = doRequest(action, command, args, rw, whitelist);
+                       rep = doRequest(action, command, args, rw, whitelist, blacklist);
                } catch (IOException e) {
                        rep = new RemoteLibraryException(e, true);
                }
 
                commands.put(id, command);
                wls.put(id, wl);
+               bls.put(id, bl);
                rws.put(id, rw);
                times.put(id, (new Date().getTime() - start));
 
                return rep;
        }
 
-       private String display(boolean whitelist, boolean rw) {
+       private String display(boolean whitelist, boolean blacklist, boolean rw) {
                String mode = "";
                if (!rw) {
                        mode += "RO: ";
@@ -165,6 +181,9 @@ public class RemoteLibraryServer extends ServerObject {
                if (whitelist) {
                        mode += "WL: ";
                }
+               if (blacklist) {
+                       mode += "BL: ";
+               }
 
                return mode;
        }
@@ -172,27 +191,28 @@ public class RemoteLibraryServer extends ServerObject {
        @Override
        protected void onRequestDone(long id, long bytesReceived, long bytesSent) {
                boolean whitelist = wls.get(id);
+               boolean blacklist = bls.get(id);
                boolean rw = rws.get(id);
                wls.remove(id);
+               bls.remove(id);
                rws.remove(id);
 
                String rec = StringUtils.formatNumber(bytesReceived) + "b";
                String sent = StringUtils.formatNumber(bytesSent) + "b";
                long now = System.currentTimeMillis();
-               System.out.println(StringUtils.fromTime(now)
-                               + ": "
+               System.out.println(StringUtils.fromTime(now) + ": "
                                + String.format("%s[>%s]: (%s sent, %s rec) in %d ms",
-                                               display(whitelist, rw), commands.get(id), sent, rec,
-                                               times.get(id)));
+                                               display(whitelist, blacklist, rw), commands.get(id),
+                                               sent, rec, times.get(id)));
 
                commands.remove(id);
                times.remove(id);
        }
 
        private Object doRequest(ConnectActionServerObject action, String command,
-                       Object[] args, boolean rw, List<String> whitelist)
-                       throws NoSuchFieldException, NoSuchMethodException,
-                       ClassNotFoundException, IOException {
+                       Object[] args, boolean rw, List<String> whitelist,
+                       List<String> blacklist) throws NoSuchFieldException,
+                       NoSuchMethodException, ClassNotFoundException, IOException {
                if ("PING".equals(command)) {
                        return rw ? "r/w" : "r/o";
                } else if ("GET_METADATA".equals(command)) {
@@ -201,13 +221,15 @@ public class RemoteLibraryServer extends ServerObject {
                        if ("*".equals(args[0])) {
                                Progress pg = createPgForwarder(action);
 
-                               for (MetaData meta : Instance.getInstance().getLibrary().getMetas(pg)) {
+                               for (MetaData meta : Instance.getInstance().getLibrary()
+                                               .getMetas(pg)) {
                                        metas.add(removeCover(meta));
                                }
 
                                forcePgDoneSent(pg);
                        } else {
-                               MetaData meta = Instance.getInstance().getLibrary().getInfo((String) args[0]);
+                               MetaData meta = Instance.getInstance().getLibrary()
+                                               .getInfo((String) args[0]);
                                MetaData light;
                                if (meta.getCover() == null) {
                                        light = meta;
@@ -219,43 +241,38 @@ public class RemoteLibraryServer extends ServerObject {
                                metas.add(light);
                        }
 
-                       if (!whitelist.isEmpty()) {
-                               for (int i = 0; i < metas.size(); i++) {
-                                       if (!whitelist.contains(metas.get(i).getSource())) {
-                                               metas.remove(i);
-                                               i--;
-                                       }
+                       for (int i = 0; i < metas.size(); i++) {
+                               if (!isAllowed(metas.get(i), whitelist, blacklist)) {
+                                       metas.remove(i);
+                                       i--;
                                }
                        }
 
                        return metas.toArray(new MetaData[0]);
+
                } else if ("GET_STORY".equals(command)) {
-                       MetaData meta = Instance.getInstance().getLibrary().getInfo((String) args[0]);
-                       if (meta == null) {
+                       MetaData meta = Instance.getInstance().getLibrary()
+                                       .getInfo((String) args[0]);
+                       if (meta == null || !isAllowed(meta, whitelist, blacklist)) {
                                return null;
                        }
 
-                       if (!whitelist.isEmpty()) {
-                               if (!whitelist.contains(meta.getSource())) {
-                                       return null;
-                               }
-                       }
-
                        meta = meta.clone();
                        meta.setCover(null);
 
                        action.send(meta);
                        action.rec();
 
-                       Story story = Instance.getInstance().getLibrary().getStory((String) args[0], null);
+                       Story story = Instance.getInstance().getLibrary()
+                                       .getStory((String) args[0], null);
                        for (Object obj : breakStory(story)) {
                                action.send(obj);
                                action.rec();
                        }
                } else if ("SAVE_STORY".equals(command)) {
                        if (!rw) {
-                               throw new RemoteLibraryException("Read-Only remote library: "
-                                               + args[0], false);
+                               throw new RemoteLibraryException(
+                                               "Read-Only remote library: " + args[0], false);
                        }
 
                        List<Object> list = new ArrayList<Object>();
@@ -269,54 +286,64 @@ public class RemoteLibraryServer extends ServerObject {
                        }
 
                        Story story = rebuildStory(list);
-                       Instance.getInstance().getLibrary().save(story, (String) args[0], null);
+                       Instance.getInstance().getLibrary().save(story, (String) args[0],
+                                       null);
                        return story.getMeta().getLuid();
                } else if ("IMPORT".equals(command)) {
                        if (!rw) {
-                               throw new RemoteLibraryException("Read-Only remote library: "
-                                               + args[0], false);
+                               throw new RemoteLibraryException(
+                                               "Read-Only remote library: " + args[0], false);
                        }
 
                        Progress pg = createPgForwarder(action);
-                       MetaData meta = Instance.getInstance().getLibrary().imprt(new URL((String) args[0]), pg);
+                       MetaData meta = Instance.getInstance().getLibrary()
+                                       .imprt(new URL((String) args[0]), pg);
                        forcePgDoneSent(pg);
                        return meta.getLuid();
                } else if ("DELETE_STORY".equals(command)) {
                        if (!rw) {
-                               throw new RemoteLibraryException("Read-Only remote library: "
-                                               + args[0], false);
+                               throw new RemoteLibraryException(
+                                               "Read-Only remote library: " + args[0], false);
                        }
 
                        Instance.getInstance().getLibrary().delete((String) args[0]);
                } else if ("GET_COVER".equals(command)) {
-                       return Instance.getInstance().getLibrary().getCover((String) args[0]);
+                       return Instance.getInstance().getLibrary()
+                                       .getCover((String) args[0]);
                } else if ("GET_CUSTOM_COVER".equals(command)) {
                        if ("SOURCE".equals(args[0])) {
-                               return Instance.getInstance().getLibrary().getCustomSourceCover((String) args[1]);
+                               return Instance.getInstance().getLibrary()
+                                               .getCustomSourceCover((String) args[1]);
                        } else if ("AUTHOR".equals(args[0])) {
-                               return Instance.getInstance().getLibrary().getCustomAuthorCover((String) args[1]);
+                               return Instance.getInstance().getLibrary()
+                                               .getCustomAuthorCover((String) args[1]);
                        } else {
                                return null;
                        }
                } else if ("SET_COVER".equals(command)) {
                        if (!rw) {
-                               throw new RemoteLibraryException("Read-Only remote library: "
-                                               + args[0] + ", " + args[1], false);
+                               throw new RemoteLibraryException(
+                                               "Read-Only remote library: " + args[0] + ", " + args[1],
+                                               false);
                        }
 
                        if ("SOURCE".equals(args[0])) {
-                               Instance.getInstance().getLibrary().setSourceCover((String) args[1], (String) args[2]);
+                               Instance.getInstance().getLibrary()
+                                               .setSourceCover((String) args[1], (String) args[2]);
                        } else if ("AUTHOR".equals(args[0])) {
-                               Instance.getInstance().getLibrary().setAuthorCover((String) args[1], (String) args[2]);
+                               Instance.getInstance().getLibrary()
+                                               .setAuthorCover((String) args[1], (String) args[2]);
                        }
                } else if ("CHANGE_STA".equals(command)) {
                        if (!rw) {
-                               throw new RemoteLibraryException("Read-Only remote library: " + args[0] + ", " + args[1], false);
+                               throw new RemoteLibraryException(
+                                               "Read-Only remote library: " + args[0] + ", " + args[1],
+                                               false);
                        }
 
                        Progress pg = createPgForwarder(action);
-                       Instance.getInstance().getLibrary().changeSTA((String) args[0], (String) args[1], (String) args[2],
-                                       (String) args[3], pg);
+                       Instance.getInstance().getLibrary().changeSTA((String) args[0],
+                                       (String) args[1], (String) args[2], (String) args[3], pg);
                        forcePgDoneSent(pg);
                } else if ("EXIT".equals(command)) {
                        if (!rw) {
@@ -423,17 +450,16 @@ public class RemoteLibraryServer extends ServerObject {
                        int min = (Integer) a[0 + offset];
                        int max = (Integer) a[1 + offset];
                        int progress = (Integer) a[2 + offset];
-                       
+
                        Object meta = null;
                        if (a.length > (3 + offset)) {
                                meta = a[3 + offset];
                        }
-                       
+
                        String name = null;
                        if (a.length > (4 + offset)) {
                                name = a[4 + offset] == null ? "" : a[4 + offset].toString();
                        }
-                       
 
                        if (min >= 0 && min <= max) {
                                pg.setName(name);
@@ -477,22 +503,20 @@ public class RemoteLibraryServer extends ServerObject {
                        public void progress(Progress progress, String name) {
                                Object meta = pg.get("meta");
                                if (meta instanceof MetaData) {
-                                       meta = removeCover((MetaData)meta);
+                                       meta = removeCover((MetaData) meta);
                                }
-                               
+
                                int min = pg.getMin();
                                int max = pg.getMax();
-                               int rel = min
-                                               + (int) Math.round(pg.getRelativeProgress()
-                                                               * (max - min));
-                               
+                               int rel = min + (int) Math
+                                               .round(pg.getRelativeProgress() * (max - min));
+
                                boolean samePg = p[0] == min && p[1] == max && p[2] == rel;
-                               
+
                                // Do not re-send the same value twice over the wire,
                                // unless more than 2 seconds have elapsed (to maintain the
                                // connection)
-                               if (!samePg || !same(pMeta[0], meta)
-                                               || !same(pName[0], name) //
+                               if (!samePg || !same(pMeta[0], meta) || !same(pName[0], name) //
                                                || (new Date().getTime() - lastTime[0] > 2000)) {
                                        p[0] = min;
                                        p[1] = max;
@@ -517,7 +541,7 @@ public class RemoteLibraryServer extends ServerObject {
 
                return pg;
        }
-       
+
        private boolean same(Object obj1, Object obj2) {
                if (obj1 == null || obj2 == null)
                        return obj1 == null && obj2 == null;
@@ -537,7 +561,7 @@ public class RemoteLibraryServer extends ServerObject {
                        }
                }
        }
-       
+
        private MetaData removeCover(MetaData meta) {
                MetaData light = null;
                if (meta != null) {
@@ -548,7 +572,20 @@ public class RemoteLibraryServer extends ServerObject {
                                light.setCover(null);
                        }
                }
-               
+
                return light;
        }
+
+       private boolean isAllowed(MetaData meta, List<String> whitelist,
+                       List<String> blacklist) {
+               if (!whitelist.isEmpty() && !whitelist.contains(meta.getSource())) {
+                       return false;
+               }
+
+               if (blacklist.contains(meta.getSource())) {
+                       return false;
+               }
+
+               return true;
+       }
 }
index 7f775830edf8c7641eeb3303abdddf3fcdb48fbd..8c71ff590b41b85bba46e715b0ea8328af383592 100644 (file)
@@ -22,6 +22,7 @@ import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.utils.IOUtils;
 import be.nikiroo.utils.Image;
 import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.Version;
 
 /**
  * This {@link BasicLibrary} will access a remote server to list the available
@@ -107,16 +108,30 @@ public class WebLibrary extends BasicLibrary {
                this.rw = false;
        }
 
+       public Version getVersion() {
+               try {
+                       InputStream in = download(WebLibraryUrls.VERSION_URL);
+                       try {
+                               return new Version(IOUtils.readSmallStream(in));
+                       } finally {
+                               in.close();
+                       }
+               } catch (IOException e) {
+               }
+
+               return new Version();
+       }
+
        @Override
        public Status getStatus() {
                try {
-                       download("/");
+                       download(WebLibraryUrls.INDEX_URL).close();
                } catch (IOException e) {
                        try {
-                               download("/style.css");
+                               download(WebLibraryUrls.VERSION_URL).close();
                                return Status.UNAUTHORIZED;
                        } catch (IOException ioe) {
-                               return Status.INVALID;
+                               return Status.UNAVAILABLE;
                        }
                }
 
@@ -125,17 +140,18 @@ public class WebLibrary extends BasicLibrary {
 
        @Override
        public String getLibraryName() {
-               return (rw ? "[READ-ONLY] " : "") + host + ":" + port;
+               return (rw ? "[READ-ONLY] " : "") + host + ":" + port + " ("
+                               + getVersion() + ")";
        }
 
        @Override
        public Image getCover(String luid) throws IOException {
-               InputStream in = download("/story/" + luid + "/cover");
-               if (in != null) {
+               InputStream in = download(WebLibraryUrls.getStoryUrlCover(luid));
+               try {
                        return new Image(in);
+               } finally {
+                       in.close();
                }
-
-               return null;
        }
 
        @Override
@@ -169,7 +185,7 @@ public class WebLibrary extends BasicLibrary {
                // TODO: pg
 
                Story story;
-               InputStream in = download("/story/" + luid + "/json");
+               InputStream in = download(WebLibraryUrls.getStoryUrlJson(luid));
                try {
                        JSONObject json = new JSONObject(IOUtils.readSmallStream(in));
                        story = JsonIO.toStory(json);
@@ -184,7 +200,7 @@ public class WebLibrary extends BasicLibrary {
                        for (Paragraph para : chap) {
                                if (para.getType() == ParagraphType.IMAGE) {
                                        InputStream subin = download(
-                                                       "/story/" + luid + "/" + chapNum + "/" + number);
+                                                       WebLibraryUrls.getStoryUrl(luid, chapNum, number));
                                        try {
                                                para.setContentImage(new Image(subin));
                                        } finally {
@@ -204,7 +220,7 @@ public class WebLibrary extends BasicLibrary {
        @Override
        protected List<MetaData> getMetas(Progress pg) throws IOException {
                List<MetaData> metas = new ArrayList<MetaData>();
-               InputStream in = download("/list/luids");
+               InputStream in = download(WebLibraryUrls.LIST_URL_METADATA);
                JSONArray jsonArr = new JSONArray(IOUtils.readSmallStream(in));
                for (int i = 0; i < jsonArr.length(); i++) {
                        JSONObject json = jsonArr.getJSONObject(i);
@@ -276,7 +292,7 @@ public class WebLibrary extends BasicLibrary {
                                "Operation not supportorted on remote Libraries");
        }
 
-       // starts with "/"
+       // starts with "/", never NULL
        private InputStream download(String path) throws IOException {
                URL url = new URL(host + ":" + port + path);
 
index b4a6e4bb25f3776c819a43e9235f58b5c63b7788..97f2f0e8f69c6c07e217e1022ee4aa0fa96eaa4d 100644 (file)
 package be.nikiroo.fanfix.library;
 
 import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.security.KeyStore;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
-import javax.net.ssl.KeyManagerFactory;
-import javax.net.ssl.SSLServerSocketFactory;
-
 import org.json.JSONArray;
 import org.json.JSONObject;
 
 import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.bundles.Config;
-import be.nikiroo.fanfix.bundles.UiConfig;
 import be.nikiroo.fanfix.data.Chapter;
 import be.nikiroo.fanfix.data.JsonIO;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Paragraph;
 import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
 import be.nikiroo.fanfix.data.Story;
-import be.nikiroo.fanfix.library.web.WebLibraryServerIndex;
-import be.nikiroo.fanfix.reader.TextOutput;
-import be.nikiroo.utils.CookieUtils;
-import be.nikiroo.utils.IOUtils;
 import be.nikiroo.utils.Image;
+import be.nikiroo.utils.LoginResult;
 import be.nikiroo.utils.NanoHTTPD;
-import be.nikiroo.utils.NanoHTTPD.IHTTPSession;
 import be.nikiroo.utils.NanoHTTPD.Response;
 import be.nikiroo.utils.NanoHTTPD.Response.Status;
-import be.nikiroo.utils.TraceHandler;
-import be.nikiroo.utils.Version;
-
-public class WebLibraryServer implements Runnable {
-       static private String VIEWER_URL_BASE = "/view/story/";
-       static private String VIEWER_URL = VIEWER_URL_BASE + "{luid}/{chap}/{para}";
-       static private String STORY_URL_BASE = "/story/";
-       static private String STORY_URL = STORY_URL_BASE + "{luid}/{chap}/{para}";
-       static private String STORY_URL_COVER = STORY_URL_BASE + "{luid}/cover";
-       static private String LIST_URL = "/list/";
 
-       private class LoginResult {
-               private boolean success;
+public class WebLibraryServer extends WebLibraryServerHtml {
+       class WLoginResult extends LoginResult {
                private boolean rw;
                private boolean wl;
-               private String wookie;
-               private String token;
-               private boolean badLogin;
-               private boolean badToken;
+               private boolean bl;
 
-               public LoginResult(String who, String key, String subkey,
-                               boolean success, boolean rw, boolean wl) {
-                       this.success = success;
+               public WLoginResult(boolean badLogin, boolean badCookie) {
+                       super(badLogin, badCookie);
+               }
+
+               public WLoginResult(String who, String key, String subkey, boolean rw,
+                               boolean wl, boolean bl) {
+                       super(who, key, subkey, (rw ? "|rw" : "") + (wl ? "|wl" : "")
+                                       + (bl ? "|bl" : "") + "|");
                        this.rw = rw;
                        this.wl = wl;
-                       this.wookie = CookieUtils.generateCookie(who + key, 0);
-
-                       String opts = "";
-                       if (rw)
-                               opts += "|rw";
-                       if (!wl)
-                               opts += "|wl";
-
-                       this.token = wookie + "~"
-                                       + CookieUtils.generateCookie(wookie + subkey + opts, 0)
-                                       + "~" + opts;
-                       this.badLogin = !success;
+                       this.bl = bl;
                }
 
-               public LoginResult(String token, String who, String key,
+               public WLoginResult(String cookie, String who, String key,
                                List<String> subkeys) {
-
-                       if (token != null) {
-                               String hashes[] = token.split("~");
-                               if (hashes.length >= 2) {
-                                       String wookie = hashes[0];
-                                       String rehashed = hashes[1];
-                                       String opts = hashes.length > 2 ? hashes[2] : "";
-
-                                       if (CookieUtils.validateCookie(who + key, wookie)) {
-                                               if (subkeys == null) {
-                                                       subkeys = new ArrayList<String>();
-                                               }
-                                               subkeys = new ArrayList<String>(subkeys);
-                                               subkeys.add("");
-
-                                               for (String subkey : subkeys) {
-                                                       if (CookieUtils.validateCookie(
-                                                                       wookie + subkey + opts, rehashed)) {
-                                                               this.wookie = wookie;
-                                                               this.token = token;
-                                                               this.success = true;
-
-                                                               this.rw = opts.contains("|rw");
-                                                               this.wl = !opts.contains("|wl");
-                                                       }
-                                               }
-                                       }
-                               }
-
-                               this.badToken = !success;
-                       }
-
-                       // No token -> no bad token
-               }
-
-               public boolean isSuccess() {
-                       return success;
+                       super(cookie, who, key, subkeys,
+                                       subkeys == null || subkeys.isEmpty());
                }
 
                public boolean isRw() {
-                       return rw;
+                       return getOption().contains("|rw|");
                }
 
                public boolean isWl() {
-                       return wl;
+                       return getOption().contains("|wl|");
                }
 
-               public String getToken() {
-                       return token;
-               }
-
-               public boolean isBadLogin() {
-                       return badLogin;
-               }
-
-               public boolean isBadToken() {
-                       return badToken;
+               public boolean isBl() {
+                       return getOption().contains("|bl|");
                }
        }
 
-       private NanoHTTPD server;
        private Map<String, Story> storyCache = new HashMap<String, Story>();
        private LinkedList<String> storyCacheOrder = new LinkedList<String>();
        private long storyCacheSize = 0;
        private long maxStoryCacheSize;
-       private TraceHandler tracer = new TraceHandler();
+
+       private List<String> whitelist;
+       private List<String> blacklist;
 
        public WebLibraryServer(boolean secure) throws IOException {
-               Integer port = Instance.getInstance().getConfig()
-                               .getInteger(Config.SERVER_PORT);
-               if (port == null) {
-                       throw new IOException(
-                                       "Cannot start web server: port not specified");
-               }
+               super(secure);
 
                int cacheMb = Instance.getInstance().getConfig()
                                .getInteger(Config.SERVER_MAX_CACHE_MB, 100);
@@ -158,196 +81,10 @@ public class WebLibraryServer implements Runnable {
 
                setTraceHandler(Instance.getInstance().getTraceHandler());
 
-               SSLServerSocketFactory ssf = null;
-               if (secure) {
-                       String keystorePath = Instance.getInstance().getConfig()
-                                       .getString(Config.SERVER_SSL_KEYSTORE, "");
-                       String keystorePass = Instance.getInstance().getConfig()
-                                       .getString(Config.SERVER_SSL_KEYSTORE_PASS);
-
-                       if (secure && keystorePath.isEmpty()) {
-                               throw new IOException(
-                                               "Cannot start a secure web server: no keystore.jks file povided");
-                       }
-
-                       if (!keystorePath.isEmpty()) {
-                               File keystoreFile = new File(keystorePath);
-                               try {
-                                       KeyStore keystore = KeyStore
-                                                       .getInstance(KeyStore.getDefaultType());
-                                       InputStream keystoreStream = new FileInputStream(
-                                                       keystoreFile);
-                                       try {
-                                               keystore.load(keystoreStream,
-                                                               keystorePass.toCharArray());
-                                               KeyManagerFactory keyManagerFactory = KeyManagerFactory
-                                                               .getInstance(KeyManagerFactory
-                                                                               .getDefaultAlgorithm());
-                                               keyManagerFactory.init(keystore,
-                                                               keystorePass.toCharArray());
-                                               ssf = NanoHTTPD.makeSSLSocketFactory(keystore,
-                                                               keyManagerFactory);
-                                       } finally {
-                                               keystoreStream.close();
-                                       }
-                               } catch (Exception e) {
-                                       throw new IOException(e.getMessage());
-                               }
-                       }
-               }
-
-               server = new NanoHTTPD(port) {
-                       @Override
-                       public Response serve(final IHTTPSession session) {
-                               super.serve(session);
-
-                               String query = session.getQueryParameterString(); // a=a%20b&dd=2
-                               Method method = session.getMethod(); // GET, POST..
-                               String uri = session.getUri(); // /home.html
-
-                               // need them in real time (not just those sent by the UA)
-                               Map<String, String> cookies = new HashMap<String, String>();
-                               for (String cookie : session.getCookies()) {
-                                       cookies.put(cookie, session.getCookies().read(cookie));
-                               }
-
-                               List<String> whitelist = Instance.getInstance().getConfig()
-                                               .getList(Config.SERVER_WHITELIST);
-                               if (whitelist == null) {
-                                       whitelist = new ArrayList<String>();
-                               }
-
-                               LoginResult login = null;
-                               Map<String, String> params = session.getParms();
-                               String who = session.getRemoteHostName()
-                                               + session.getRemoteIpAddress();
-                               if (params.get("login") != null) {
-                                       login = login(who, params.get("password"),
-                                                       params.get("login"), whitelist);
-                               } else {
-                                       String token = cookies.get("token");
-                                       login = login(who, token, Instance.getInstance().getConfig()
-                                                       .getList(Config.SERVER_ALLOWED_SUBKEYS));
-                               }
-
-                               if (login.isSuccess()) {
-                                       if (!login.isWl()) {
-                                               whitelist.clear();
-                                       }
-
-                                       // refresh token
-                                       session.getCookies().set(new Cookie("token",
-                                                       login.getToken(), "30; path=/"));
-
-                                       // set options
-                                       String optionName = params.get("optionName");
-                                       if (optionName != null && !optionName.isEmpty()) {
-                                               String optionNo = params.get("optionNo");
-                                               String optionValue = params.get("optionValue");
-                                               if (optionNo != null || optionValue == null
-                                                               || optionValue.isEmpty()) {
-                                                       session.getCookies().delete(optionName);
-                                                       cookies.remove(optionName);
-                                               } else {
-                                                       session.getCookies().set(new Cookie(optionName,
-                                                                       optionValue, "; path=/"));
-                                                       cookies.put(optionName, optionValue);
-                                               }
-                                       }
-                               }
-
-                               Response rep = null;
-                               if (!login.isSuccess() && (uri.equals("/") //
-                                               || uri.startsWith(STORY_URL_BASE) //
-                                               || uri.startsWith(VIEWER_URL_BASE) //
-                                               || uri.startsWith(LIST_URL))) {
-                                       rep = loginPage(login, uri);
-                               }
-
-                               if (rep == null) {
-                                       try {
-                                               if (uri.equals("/")) {
-                                                       rep = root(session, cookies, whitelist);
-                                               } else if (uri.startsWith(LIST_URL)) {
-                                                       rep = getList(uri, whitelist);
-                                               } else if (uri.startsWith(STORY_URL_BASE)) {
-                                                       rep = getStoryPart(uri, whitelist);
-                                               } else if (uri.startsWith(VIEWER_URL_BASE)) {
-                                                       rep = getViewer(cookies, uri, whitelist);
-                                               } else if (uri.equals("/logout")) {
-                                                       session.getCookies().delete("token");
-                                                       cookies.remove("token");
-                                                       rep = loginPage(login, uri);
-                                               } else {
-                                                       if (uri.startsWith("/"))
-                                                               uri = uri.substring(1);
-                                                       InputStream in = IOUtils.openResource(
-                                                                       WebLibraryServerIndex.class, uri);
-                                                       if (in != null) {
-                                                               String mimeType = MIME_PLAINTEXT;
-                                                               if (uri.endsWith(".css")) {
-                                                                       mimeType = "text/css";
-                                                               } else if (uri.endsWith(".html")) {
-                                                                       mimeType = "text/html";
-                                                               } else if (uri.endsWith(".js")) {
-                                                                       mimeType = "text/javascript";
-                                                               }
-                                                               rep = newChunkedResponse(Status.OK, mimeType,
-                                                                               in);
-                                                       } else {
-                                                               getTraceHandler().trace("404: " + uri);
-                                                       }
-                                               }
-
-                                               if (rep == null) {
-                                                       rep = newFixedLengthResponse(Status.NOT_FOUND,
-                                                                       NanoHTTPD.MIME_PLAINTEXT, "Not Found");
-                                               }
-                                       } catch (Exception e) {
-                                               Instance.getInstance().getTraceHandler().error(
-                                                               new IOException("Cannot process web request",
-                                                                               e));
-                                               rep = newFixedLengthResponse(Status.INTERNAL_ERROR,
-                                                               NanoHTTPD.MIME_PLAINTEXT, "An error occured");
-                                       }
-                               }
-
-                               return rep;
-
-                               // Get status: for story, use "luid" + active map of current
-                               // luids
-                               // map must use a addRef/removeRef and delete at 0
-
-                               // http://localhost:2000/?token=ok
-
-                               //
-                               // MetaData meta = new MetaData();
-                               // meta.setTitle("Title");
-                               // meta.setLuid("000");
-                               //
-                               // JSONObject json = new JSONObject();
-                               // json.put("", MetaData.class.getName());
-                               // json.put("title", meta.getTitle());
-                               // json.put("luid", meta.getLuid());
-                               //
-                               // return newFixedLengthResponse(json.toString());
-                       }
-               };
-
-               if (ssf != null) {
-                       getTraceHandler().trace("Install SSL on the web server...");
-                       server.makeSecure(ssf, null);
-                       getTraceHandler().trace("Done.");
-               }
-       }
-
-       @Override
-       public void run() {
-               try {
-                       server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
-               } catch (IOException e) {
-                       tracer.error(new IOException("Cannot start the web server", e));
-               }
+               whitelist = Instance.getInstance().getConfig()
+                               .getList(Config.SERVER_WHITELIST, new ArrayList<String>());
+               blacklist = Instance.getInstance().getConfig()
+                               .getList(Config.SERVER_BLACKLIST, new ArrayList<String>());
        }
 
        /**
@@ -362,116 +99,71 @@ public class WebLibraryServer implements Runnable {
                new Thread(this).start();
        }
 
-       /**
-        * The traces handler for this {@link WebLibraryServer}.
-        * 
-        * @return the traces handler
-        */
-       public TraceHandler getTraceHandler() {
-               return tracer;
-       }
-
-       /**
-        * The traces handler for this {@link WebLibraryServer}.
-        * 
-        * @param tracer
-        *            the new traces handler
-        */
-       public void setTraceHandler(TraceHandler tracer) {
-               if (tracer == null) {
-                       tracer = new TraceHandler(false, false, false);
-               }
-
-               this.tracer = tracer;
+       @Override
+       protected WLoginResult login(boolean badLogin, boolean badCookie) {
+               return new WLoginResult(false, false);
        }
 
-       private LoginResult login(String who, String token, List<String> subkeys) {
+       @Override
+       protected WLoginResult login(String who, String cookie) {
+               List<String> subkeys = Instance.getInstance().getConfig()
+                               .getList(Config.SERVER_ALLOWED_SUBKEYS);
                String realKey = Instance.getInstance().getConfig()
                                .getString(Config.SERVER_KEY);
-               realKey = realKey == null ? "" : realKey;
-               return new LoginResult(token, who, realKey, subkeys);
+
+               return new WLoginResult(cookie, who, realKey, subkeys);
        }
 
        // allow rw/wl
-       private LoginResult login(String who, String key, String subkey,
-                       List<String> whitelist) {
+       @Override
+       protected WLoginResult login(String who, String key, String subkey) {
                String realKey = Instance.getInstance().getConfig()
-                               .getString(Config.SERVER_KEY);
+                               .getString(Config.SERVER_KEY, "");
 
                // I don't like NULLs...
-               realKey = realKey == null ? "" : realKey;
                key = key == null ? "" : key;
                subkey = subkey == null ? "" : subkey;
 
                if (!realKey.equals(key)) {
-                       return new LoginResult(null, null, null, false, false, false);
+                       return new WLoginResult(true, false);
                }
 
-               // defaults are positive (as previous versions without the feature)
+               // defaults are true (as previous versions without the feature)
                boolean rw = true;
                boolean wl = true;
-
-               if (whitelist.isEmpty()) {
-                       wl = false;
-               }
+               boolean bl = true;
 
                rw = Instance.getInstance().getConfig().getBoolean(Config.SERVER_RW,
                                rw);
-               if (!subkey.isEmpty()) {
-                       List<String> allowed = Instance.getInstance().getConfig()
-                                       .getList(Config.SERVER_ALLOWED_SUBKEYS);
-                       if (allowed != null && allowed.contains(subkey)) {
-                               if ((subkey + "|").contains("|rw|")) {
-                                       rw = true;
-                               }
-                               if ((subkey + "|").contains("|wl|")) {
-                                       wl = false; // |wl| = bypass whitelist
-                               }
-                       } else {
-                               return new LoginResult(null, null, null, false, false, false);
-                       }
-               }
 
-               return new LoginResult(who, key, subkey, true, rw, wl);
-       }
-
-       private Response loginPage(LoginResult login, String uri) {
-               StringBuilder builder = new StringBuilder();
-
-               appendPreHtml(builder, true);
+               List<String> allowed = Instance.getInstance().getConfig().getList(
+                               Config.SERVER_ALLOWED_SUBKEYS, new ArrayList<String>());
 
-               if (login.isBadLogin()) {
-                       builder.append("<div class='error'>Bad login or password</div>");
-               } else if (login.isBadToken()) {
-                       builder.append("<div class='error'>Your session timed out</div>");
-               }
+               if (!allowed.isEmpty()) {
+                       if (!allowed.contains(subkey)) {
+                               return new WLoginResult(true, false);
+                       }
 
-               if (uri.equals("/logout")) {
-                       uri = "/";
+                       if ((subkey + "|").contains("|rw|")) {
+                               rw = true;
+                       }
+                       if ((subkey + "|").contains("|wl|")) {
+                               wl = false; // |wl| = bypass whitelist
+                       }
+                       if ((subkey + "|").contains("|bl|")) {
+                               bl = false; // |bl| = bypass blacklist
+                       }
                }
 
-               builder.append(
-                               "<form method='POST' action='" + uri + "' class='login'>\n");
-               builder.append(
-                               "<p>You must be logged into the system to see the stories.</p>");
-               builder.append("\t<input type='text' name='login' />\n");
-               builder.append("\t<input type='password' name='password' />\n");
-               builder.append("\t<input type='submit' value='Login' />\n");
-               builder.append("</form>\n");
-
-               appendPostHtml(builder);
-
-               return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
-                               NanoHTTPD.MIME_HTML, builder.toString());
+               return new WLoginResult(who, key, subkey, rw, wl, bl);
        }
 
-       protected Response getList(String uri, List<String> whitelist)
+       @Override
+       protected Response getList(String uri, WLoginResult login)
                        throws IOException {
-               if (uri.equals("/list/luids")) {
-                       BasicLibrary lib = Instance.getInstance().getLibrary();
-                       List<MetaData> metas = lib.getList().filter(whitelist, null, null);
+               if (WebLibraryUrls.LIST_URL_METADATA.equals(uri)) {
                        List<JSONObject> jsons = new ArrayList<JSONObject>();
-                       for (MetaData meta : metas) {
+                       for (MetaData meta : metas(login)) {
                                jsons.add(JsonIO.toJson(meta));
                        }
 
@@ -484,185 +176,12 @@ public class WebLibraryServer implements Runnable {
                                NanoHTTPD.MIME_PLAINTEXT, null);
        }
 
-       private Response root(IHTTPSession session, Map<String, String> cookies,
-                       List<String> whitelist) throws IOException {
-               BasicLibrary lib = Instance.getInstance().getLibrary();
-               MetaResultList result = lib.getList();
-               result = new MetaResultList(result.filter(whitelist, null, null));
-               StringBuilder builder = new StringBuilder();
-
-               appendPreHtml(builder, true);
-
-               Map<String, String> params = session.getParms();
-
-               String filter = cookies.get("filter");
-               if (params.get("optionNo") != null)
-                       filter = null;
-               if (filter == null) {
-                       filter = "";
-               }
-
-               String browser = params.get("browser") == null ? ""
-                               : params.get("browser");
-               String browser2 = params.get("browser2") == null ? ""
-                               : params.get("browser2");
-               String browser3 = params.get("browser3") == null ? ""
-                               : params.get("browser3");
-
-               String filterSource = null;
-               String filterAuthor = null;
-               String filterTag = null;
-
-               // TODO: javascript in realtime, using visible=false + hide [submit]
-
-               builder.append("<form class='browser'>\n");
-               builder.append("<div class='breadcrumbs'>\n");
-
-               builder.append("\t<select name='browser'>");
-               appendOption(builder, 2, "", "", browser);
-               appendOption(builder, 2, "Sources", "sources", browser);
-               appendOption(builder, 2, "Authors", "authors", browser);
-               appendOption(builder, 2, "Tags", "tags", browser);
-               builder.append("\t</select>\n");
-
-               if (!browser.isEmpty()) {
-                       builder.append("\t<select name='browser2'>");
-                       if (browser.equals("sources")) {
-                               filterSource = browser2.isEmpty() ? filterSource : browser2;
-                               // TODO: if 1 group -> no group
-                               appendOption(builder, 2, "", "", browser2);
-                               Map<String, List<String>> sources = result.getSourcesGrouped();
-                               for (String source : sources.keySet()) {
-                                       appendOption(builder, 2, source, source, browser2);
-                               }
-                       } else if (browser.equals("authors")) {
-                               filterAuthor = browser2.isEmpty() ? filterAuthor : browser2;
-                               // TODO: if 1 group -> no group
-                               appendOption(builder, 2, "", "", browser2);
-                               Map<String, List<String>> authors = result.getAuthorsGrouped();
-                               for (String author : authors.keySet()) {
-                                       appendOption(builder, 2, author, author, browser2);
-                               }
-                       } else if (browser.equals("tags")) {
-                               filterTag = browser2.isEmpty() ? filterTag : browser2;
-                               appendOption(builder, 2, "", "", browser2);
-                               for (String tag : result.getTags()) {
-                                       appendOption(builder, 2, tag, tag, browser2);
-                               }
-                       }
-                       builder.append("\t</select>\n");
-               }
-
-               if (!browser2.isEmpty()) {
-                       if (browser.equals("sources")) {
-                               filterSource = browser3.isEmpty() ? filterSource : browser3;
-                               Map<String, List<String>> sourcesGrouped = result
-                                               .getSourcesGrouped();
-                               List<String> sources = sourcesGrouped.get(browser2);
-                               if (sources != null && !sources.isEmpty()) {
-                                       // TODO: single empty value
-                                       builder.append("\t<select name='browser3'>");
-                                       appendOption(builder, 2, "", "", browser3);
-                                       for (String source : sources) {
-                                               appendOption(builder, 2, source, source, browser3);
-                                       }
-                                       builder.append("\t</select>\n");
-                               }
-                       } else if (browser.equals("authors")) {
-                               filterAuthor = browser3.isEmpty() ? filterAuthor : browser3;
-                               Map<String, List<String>> authorsGrouped = result
-                                               .getAuthorsGrouped();
-                               List<String> authors = authorsGrouped.get(browser2);
-                               if (authors != null && !authors.isEmpty()) {
-                                       // TODO: single empty value
-                                       builder.append("\t<select name='browser3'>");
-                                       appendOption(builder, 2, "", "", browser3);
-                                       for (String author : authors) {
-                                               appendOption(builder, 2, author, author, browser3);
-                                       }
-                                       builder.append("\t</select>\n");
-                               }
-                       }
-               }
-
-               builder.append("\t<input type='submit' value='Select'/>\n");
-               builder.append("</div>\n");
-
-               // TODO: javascript in realtime, using visible=false + hide [submit]
-               builder.append("<div class='filter'>\n");
-               builder.append("\t<span class='label'>Filter: </span>\n");
-               builder.append(
-                               "\t<input name='optionName'  type='hidden' value='filter' />\n");
-               builder.append("\t<input name='optionValue' type='text'   value='"
-                               + filter + "' place-holder='...' />\n");
-               builder.append("\t<input name='optionNo'  type='submit' value='x' />");
-               builder.append(
-                               "\t<input name='submit' type='submit' value='Filter' />\n");
-               builder.append("</div>\n");
-               builder.append("</form>\n");
-
-               builder.append("\t<div class='books'>");
-               for (MetaData meta : result.getMetas()) {
-                       if (!filter.isEmpty() && !meta.getTitle().toLowerCase()
-                                       .contains(filter.toLowerCase())) {
-                               continue;
-                       }
-
-                       // TODO Sub sources
-                       if (filterSource != null
-                                       && !filterSource.equals(meta.getSource())) {
-                               continue;
-                       }
-
-                       // TODO: sub authors
-                       if (filterAuthor != null
-                                       && !filterAuthor.equals(meta.getAuthor())) {
-                               continue;
-                       }
-
-                       if (filterTag != null && !meta.getTags().contains(filterTag)) {
-                               continue;
-                       }
-
-                       builder.append("<div class='book_line'>");
-                       builder.append("<a href='");
-                       builder.append(getViewUrl(meta.getLuid(), 0, null));
-                       builder.append("'");
-                       builder.append(" class='link'>");
-
-                       if (lib.isCached(meta.getLuid())) {
-                               // â—‰ = &#9673;
-                               builder.append(
-                                               "<span class='cache_icon cached'>&#9673;</span>");
-                       } else {
-                               // â—‹ = &#9675;
-                               builder.append(
-                                               "<span class='cache_icon uncached'>&#9675;</span>");
-                       }
-                       builder.append("<span class='luid'>");
-                       builder.append(meta.getLuid());
-                       builder.append("</span>");
-                       builder.append("<span class='title'>");
-                       builder.append(meta.getTitle());
-                       builder.append("</span>");
-                       builder.append("<span class='author'>");
-                       if (meta.getAuthor() != null && !meta.getAuthor().isEmpty()) {
-                               builder.append("(").append(meta.getAuthor()).append(")");
-                       }
-                       builder.append("</span>");
-                       builder.append("</a></div>\n");
-               }
-               builder.append("</div>");
-
-               appendPostHtml(builder);
-               return NanoHTTPD.newFixedLengthResponse(builder.toString());
-       }
-
        // /story/luid/chapter/para <-- text/image
        // /story/luid/cover <-- image
        // /story/luid/metadata <-- json
        // /story/luid/json <-- json, whole chapter (no images)
-       private Response getStoryPart(String uri, List<String> whitelist) {
+       @Override
+       protected Response getStoryPart(String uri, WLoginResult login) {
                String[] cover = uri.split("/");
                int off = 2;
 
@@ -709,24 +228,24 @@ public class WebLibraryServer implements Runnable {
                InputStream in = null;
                try {
                        if ("cover".equals(chapterStr)) {
-                               Image img = getCover(luid, whitelist);
+                               Image img = cover(luid, login);
                                if (img != null) {
                                        in = img.newInputStream();
                                }
                                // TODO: get correct image type
                                mimeType = "image/png";
                        } else if ("metadata".equals(chapterStr)) {
-                               MetaData meta = meta(luid, whitelist);
+                               MetaData meta = meta(luid, login);
                                JSONObject json = JsonIO.toJson(meta);
                                mimeType = "application/json";
                                in = new ByteArrayInputStream(json.toString().getBytes());
                        } else if ("json".equals(chapterStr)) {
-                               Story story = story(luid, whitelist);
+                               Story story = story(luid, login);
                                JSONObject json = JsonIO.toJson(story);
                                mimeType = "application/json";
                                in = new ByteArrayInputStream(json.toString().getBytes());
                        } else {
-                               Story story = story(luid, whitelist);
+                               Story story = story(luid, login);
                                if (story != null) {
                                        if (chapter == 0) {
                                                StringBuilder builder = new StringBuilder();
@@ -768,359 +287,34 @@ public class WebLibraryServer implements Runnable {
                return newInputStreamResponse(mimeType, in);
        }
 
-       private Response getViewer(Map<String, String> cookies, String uri,
-                       List<String> whitelist) {
-               String[] cover = uri.split("/");
-               int off = 2;
-
-               if (cover.length < off + 2) {
-                       return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
-                                       NanoHTTPD.MIME_PLAINTEXT, null);
-               }
-
-               String type = cover[off + 0];
-               String luid = cover[off + 1];
-               String chapterStr = cover.length < off + 3 ? null : cover[off + 2];
-               String paragraphStr = cover.length < off + 4 ? null : cover[off + 3];
-
-               // 1-based (0 = desc)
-               int chapter = -1;
-               if (chapterStr != null) {
-                       try {
-                               chapter = Integer.parseInt(chapterStr);
-                               if (chapter < 0) {
-                                       throw new NumberFormatException();
-                               }
-                       } catch (NumberFormatException e) {
-                               return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
-                                               NanoHTTPD.MIME_PLAINTEXT, "Chapter is not valid");
-                       }
-               }
-
-               // 1-based
-               int paragraph = 0;
-               if (paragraphStr != null) {
-                       try {
-                               paragraph = Integer.parseInt(paragraphStr);
-                               if (paragraph <= 0) {
-                                       throw new NumberFormatException();
-                               }
-                       } catch (NumberFormatException e) {
-                               return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
-                                               NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid");
-                       }
-               }
-
-               try {
-                       Story story = story(luid, whitelist);
-                       if (story == null) {
-                               return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
-                                               NanoHTTPD.MIME_PLAINTEXT, "Story not found");
-                       }
-
-                       StringBuilder builder = new StringBuilder();
-                       appendPreHtml(builder, false);
-
-                       // For images documents, always go to the images if not chap 0 desc
-                       if (story.getMeta().isImageDocument()) {
-                               if (chapter > 0 && paragraph <= 0)
-                                       paragraph = 1;
-                       }
-
-                       Chapter chap = null;
-                       if (chapter <= 0) {
-                               chap = story.getMeta().getResume();
-                       } else {
-                               try {
-                                       chap = story.getChapters().get(chapter - 1);
-                               } catch (IndexOutOfBoundsException e) {
-                                       return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
-                                                       NanoHTTPD.MIME_PLAINTEXT, "Chapter not found");
-                               }
-                       }
-
-                       String first, previous, next, last;
-
-                       StringBuilder content = new StringBuilder();
-
-                       String disabledLeft = "";
-                       String disabledRight = "";
-                       String disabledZoomReal = "";
-                       String disabledZoomWidth = "";
-                       String disabledZoomHeight = "";
-
-                       if (paragraph <= 0) {
-                               first = getViewUrl(luid, 0, null);
-                               previous = getViewUrl(luid, (Math.max(chapter - 1, 0)), null);
-                               next = getViewUrl(luid,
-                                               (Math.min(chapter + 1, story.getChapters().size())),
-                                               null);
-                               last = getViewUrl(luid, story.getChapters().size(), null);
-
-                               StringBuilder desc = new StringBuilder();
-
-                               if (chapter <= 0) {
-                                       desc.append("<h1 class='title'>");
-                                       desc.append(story.getMeta().getTitle());
-                                       desc.append("</h1>\n");
-                                       desc.append("<div class='desc'>\n");
-                                       desc.append("\t<div class='cover'>\n");
-                                       desc.append("\t\t<img src='/story/" + luid + "/cover'/>\n");
-                                       desc.append("\t</div>\n");
-                                       desc.append("\t<table class='details'>\n");
-                                       Map<String, String> details = BasicLibrary
-                                                       .getMetaDesc(story.getMeta());
-                                       for (String key : details.keySet()) {
-                                               appendTableRow(desc, 2, key, details.get(key));
-                                       }
-                                       desc.append("\t</table>\n");
-                                       desc.append("</div>\n");
-                                       desc.append("<h1 class='title'>Description</h1>\n");
-                               }
-
-                               content.append("<div class='viewer text'>\n");
-                               content.append(desc);
-                               String description = new TextOutput(false).convert(chap,
-                                               chapter > 0);
-                               content.append(chap.getParagraphs().size() <= 0
-                                               ? "No content provided."
-                                               : description);
-                               content.append("</div>\n");
-
-                               if (chapter <= 0)
-                                       disabledLeft = " disabled='disbaled'";
-                               if (chapter >= story.getChapters().size())
-                                       disabledRight = " disabled='disbaled'";
-                       } else {
-                               first = getViewUrl(luid, chapter, 1);
-                               previous = getViewUrl(luid, chapter,
-                                               (Math.max(paragraph - 1, 1)));
-                               next = getViewUrl(luid, chapter,
-                                               (Math.min(paragraph + 1, chap.getParagraphs().size())));
-                               last = getViewUrl(luid, chapter, chap.getParagraphs().size());
-
-                               if (paragraph <= 1)
-                                       disabledLeft = " disabled='disbaled'";
-                               if (paragraph >= chap.getParagraphs().size())
-                                       disabledRight = " disabled='disbaled'";
-
-                               // First -> previous *chapter*
-                               if (chapter > 0)
-                                       disabledLeft = "";
-                               first = getViewUrl(luid, (Math.max(chapter - 1, 0)), null);
-                               if (paragraph <= 1) {
-                                       previous = first;
-                               }
-
-                               Paragraph para = null;
-                               try {
-                                       para = chap.getParagraphs().get(paragraph - 1);
-                               } catch (IndexOutOfBoundsException e) {
-                                       return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
-                                                       NanoHTTPD.MIME_PLAINTEXT,
-                                                       "Paragraph " + paragraph + " not found");
-                               }
-
-                               if (para.getType() == ParagraphType.IMAGE) {
-                                       String zoomStyle = "max-width: 100%;";
-                                       disabledZoomWidth = " disabled='disabled'";
-                                       String zoomOption = cookies.get("zoom");
-                                       if (zoomOption != null && !zoomOption.isEmpty()) {
-                                               if (zoomOption.equals("real")) {
-                                                       zoomStyle = "";
-                                                       disabledZoomWidth = "";
-                                                       disabledZoomReal = " disabled='disabled'";
-                                               } else if (zoomOption.equals("width")) {
-                                                       zoomStyle = "max-width: 100%;";
-                                               } else if (zoomOption.equals("height")) {
-                                                       // see height of navbar + optionbar
-                                                       zoomStyle = "max-height: calc(100% - 128px);";
-                                                       disabledZoomWidth = "";
-                                                       disabledZoomHeight = " disabled='disabled'";
-                                               }
-                                       }
-
-                                       content.append(String.format("" //
-                                                       + "<a class='viewer link' href='%s'>"
-                                                       + "<img class='viewer img' style='%s' src='%s'/>"
-                                                       + "</a>", //
-                                                       next, //
-                                                       zoomStyle, //
-                                                       getStoryUrl(luid, chapter, paragraph)));
-                               } else {
-                                       content.append(String.format("" //
-                                                       + "<div class='viewer text'>%s</div>", //
-                                                       para.getContent()));
-                               }
-                       }
-
-                       builder.append(String.format("" //
-                                       + "<div class='bar navbar'>\n" //
-                                       + "\t<a%s class='button first' href='%s'>&lt;&lt;</a>\n"//
-                                       + "\t<a%s class='button previous' href='%s'>&lt;</a>\n" //
-                                       + "\t<div class='gotobox itemsbox'>\n" //
-                                       + "\t\t<div class='button goto'>%d</div>\n" //
-                                       + "\t\t<div class='items goto'>\n", //
-                                       disabledLeft, first, //
-                                       disabledLeft, previous, //
-                                       paragraph > 0 ? paragraph : chapter //
-                       ));
-
-                       // List of chap/para links
-
-                       String blink = "/view/story/" + luid + "/";
-                       appendItemA(builder, 3, blink + "0", "Description",
-                                       paragraph == 0 && chapter == 0);
-
-                       if (paragraph > 0) {
-                               blink = blink + chapter + "/";
-                               for (int i = 1; i <= chap.getParagraphs().size(); i++) {
-                                       appendItemA(builder, 3, blink + i, "Image " + i,
-                                                       paragraph == i);
-                               }
-                       } else {
-                               int i = 1;
-                               for (Chapter c : story.getChapters()) {
-                                       String chapName = "Chapter " + c.getNumber();
-                                       if (c.getName() != null && !c.getName().isEmpty()) {
-                                               chapName += ": " + c.getName();
-                                       }
-
-                                       appendItemA(builder, 3, blink + i, chapName, chapter == i);
-
-                                       i++;
-                               }
-                       }
-
-                       builder.append(String.format("" //
-                                       + "\t\t</div>\n" //
-                                       + "\t</div>\n" //
-                                       + "\t<a%s class='button next' href='%s'>&gt;</a>\n" //
-                                       + "\t<a%s class='button last' href='%s'>&gt;&gt;</a>\n"//
-                                       + "</div>\n", //
-                                       disabledRight, next, //
-                                       disabledRight, last //
-                       ));
-
-                       builder.append(content);
-
-                       builder.append("<div class='bar optionbar ");
-                       if (paragraph > 0) {
-                               builder.append("s4");
-                       } else {
-                               builder.append("s1");
-                       }
-                       builder.append("'>\n");
-                       builder.append("        <a class='button back' href='/'>BACK</a>\n");
-
-                       if (paragraph > 0) {
-                               builder.append(String.format("" //
-                                               + "\t<a%s class='button zoomreal'   href='%s'>REAL</a>\n"//
-                                               + "\t<a%s class='button zoomwidth'  href='%s'>WIDTH</a>\n"//
-                                               + "\t<a%s class='button zoomheight' href='%s'>HEIGHT</a>\n"//
-                                               + "</div>\n", //
-                                               disabledZoomReal,
-                                               uri + "?optionName=zoom&optionValue=real", //
-                                               disabledZoomWidth,
-                                               uri + "?optionName=zoom&optionValue=width", //
-                                               disabledZoomHeight,
-                                               uri + "?optionName=zoom&optionValue=height" //
-                               ));
-                       }
-
-                       appendPostHtml(builder);
-                       return NanoHTTPD.newFixedLengthResponse(Status.OK,
-                                       NanoHTTPD.MIME_HTML, builder.toString());
-               } catch (IOException e) {
-                       Instance.getInstance().getTraceHandler()
-                                       .error(new IOException("Cannot get image: " + uri, e));
-                       return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR,
-                                       NanoHTTPD.MIME_PLAINTEXT, "Error when processing request");
-               }
-       }
-
-       private Response newInputStreamResponse(String mimeType, InputStream in) {
-               if (in == null) {
-                       return NanoHTTPD.newFixedLengthResponse(Status.NO_CONTENT, "",
-                                       null);
-               }
-               return NanoHTTPD.newChunkedResponse(Status.OK, mimeType, in);
-       }
-
-       private String getContentOf(String file) {
-               InputStream in = IOUtils.openResource(WebLibraryServerIndex.class,
-                               file);
-               if (in != null) {
-                       try {
-                               return IOUtils.readSmallStream(in);
-                       } catch (IOException e) {
-                               Instance.getInstance().getTraceHandler().error(
-                                               new IOException("Cannot get file: index.pre.html", e));
-                       }
-               }
-
-               return "";
-       }
-
-       private String getViewUrl(String luid, int chap, Integer para) {
-               return VIEWER_URL //
-                               .replace("{luid}", luid) //
-                               .replace("{chap}", Integer.toString(chap)) //
-                               .replace("/{para}",
-                                               para == null ? "" : "/" + Integer.toString(para));
-       }
-
-       private String getStoryUrl(String luid, int chap, Integer para) {
-               return STORY_URL //
-                               .replace("{luid}", luid) //
-                               .replace("{chap}", Integer.toString(chap)) //
-                               .replace("{para}", para == null ? "" : Integer.toString(para));
-       }
-
-       private String getStoryUrlCover(String luid) {
-               return STORY_URL_COVER //
-                               .replace("{luid}", luid);
-       }
-
-       private MetaData meta(String luid, List<String> whitelist)
-                       throws IOException {
+       @Override
+       protected List<MetaData> metas(WLoginResult login) throws IOException {
                BasicLibrary lib = Instance.getInstance().getLibrary();
-               MetaData meta = lib.getInfo(luid);
-               if (!whitelist.isEmpty() && !whitelist.contains(meta.getSource())) {
-                       return null;
-               }
-
-               return meta;
-       }
-
-       private Image getCover(String luid, List<String> whitelist)
-                       throws IOException {
-               MetaData meta = meta(luid, whitelist);
-               if (meta != null) {
-                       BasicLibrary lib = Instance.getInstance().getLibrary();
-                       return lib.getCover(meta.getLuid());
+               List<MetaData> metas = new ArrayList<MetaData>();
+               for (MetaData meta : lib.getList().getMetas()) {
+                       if (isAllowed(meta, login)) {
+                               metas.add(meta);
+                       }
                }
 
-               return null;
+               return metas;
        }
 
        // NULL if not whitelist OK or if not found
-       private Story story(String luid, List<String> whitelist)
-                       throws IOException {
+       @Override
+       protected Story story(String luid, WLoginResult login) throws IOException {
                synchronized (storyCache) {
                        if (storyCache.containsKey(luid)) {
                                Story story = storyCache.get(luid);
-                               if (!whitelist.isEmpty()
-                                               && !whitelist.contains(story.getMeta().getSource())) {
+                               if (!isAllowed(story.getMeta(), login))
                                        return null;
-                               }
 
                                return story;
                        }
                }
 
                Story story = null;
-               MetaData meta = meta(luid, whitelist);
+               MetaData meta = meta(luid, login);
                if (meta != null) {
                        BasicLibrary lib = Instance.getInstance().getLibrary();
                        story = lib.getStory(luid, null);
@@ -1145,94 +339,50 @@ public class WebLibraryServer implements Runnable {
                return story;
        }
 
-       private long sizeOf(Story story) {
-               long size = 0;
-               for (Chapter chap : story) {
-                       for (Paragraph para : chap) {
-                               if (para.getType() == ParagraphType.IMAGE) {
-                                       size += para.getContentImage().getSize();
-                               } else {
-                                       size += para.getContent().length();
-                               }
-                       }
-               }
+       private MetaData meta(String luid, WLoginResult login) throws IOException {
+               BasicLibrary lib = Instance.getInstance().getLibrary();
+               MetaData meta = lib.getInfo(luid);
+               if (!isAllowed(meta, login))
+                       return null;
 
-               return size;
+               return meta;
        }
 
-       private void appendPreHtml(StringBuilder builder, boolean banner) {
-               String favicon = "favicon.ico";
-               String icon = Instance.getInstance().getUiConfig()
-                               .getString(UiConfig.PROGRAM_ICON);
-               if (icon != null) {
-                       favicon = "icon_" + icon.replace("-", "_") + ".png";
-               }
-
-               builder.append(
-                               getContentOf("index.pre.html").replace("favicon.ico", favicon));
-
-               if (banner) {
-                       builder.append("<div class='banner'>\n");
-                       builder.append("\t<img class='ico' src='/") //
-                                       .append(favicon) //
-                                       .append("'/>\n");
-                       builder.append("\t<h1>Fanfix</h1>\n");
-                       builder.append("\t<h2>") //
-                                       .append(Version.getCurrentVersion()) //
-                                       .append("</h2>\n");
-                       builder.append("</div>\n");
+       private Image cover(String luid, WLoginResult login) throws IOException {
+               MetaData meta = meta(luid, login);
+               if (meta != null) {
+                       BasicLibrary lib = Instance.getInstance().getLibrary();
+                       return lib.getCover(meta.getLuid());
                }
-       }
 
-       private void appendPostHtml(StringBuilder builder) {
-               builder.append(getContentOf("index.post.html"));
+               return null;
        }
 
-       private void appendOption(StringBuilder builder, int depth, String name,
-                       String value, String selected) {
-               for (int i = 0; i < depth; i++) {
-                       builder.append("\t");
+       private boolean isAllowed(MetaData meta, WLoginResult login) {
+               if (login.isWl() && !whitelist.isEmpty()
+                               && !whitelist.contains(meta.getSource())) {
+                       return false;
                }
-               builder.append("<option value='").append(value).append("'");
-               if (value.equals(selected)) {
-                       builder.append(" selected='selected'");
-               }
-               builder.append(">").append(name).append("</option>\n");
-       }
-
-       private void appendTableRow(StringBuilder builder, int depth,
-                       String... tds) {
-               for (int i = 0; i < depth; i++) {
-                       builder.append("\t");
+               if (login.isBl() && blacklist.contains(meta.getSource())) {
+                       return false;
                }
 
-               int col = 1;
-               builder.append("<tr>");
-               for (String td : tds) {
-                       builder.append("<td class='col");
-                       builder.append(col++);
-                       builder.append("'>");
-                       builder.append(td);
-                       builder.append("</td>");
-               }
-               builder.append("</tr>\n");
+               return true;
        }
 
-       private void appendItemA(StringBuilder builder, int depth, String link,
-                       String name, boolean selected) {
-               for (int i = 0; i < depth; i++) {
-                       builder.append("\t");
+       private long sizeOf(Story story) {
+               long size = 0;
+               for (Chapter chap : story) {
+                       for (Paragraph para : chap) {
+                               if (para.getType() == ParagraphType.IMAGE) {
+                                       size += para.getContentImage().getSize();
+                               } else {
+                                       size += para.getContent().length();
+                               }
+                       }
                }
 
-               builder.append("<a href='");
-               builder.append(link);
-               builder.append("' class='item goto");
-               if (selected) {
-                       builder.append(" selected");
-               }
-               builder.append("'>");
-               builder.append(name);
-               builder.append("</a>\n");
+               return size;
        }
 
        public static void main(String[] args) throws IOException {
diff --git a/library/WebLibraryUrls.java b/library/WebLibraryUrls.java
new file mode 100644 (file)
index 0000000..be4cf25
--- /dev/null
@@ -0,0 +1,67 @@
+package be.nikiroo.fanfix.library;
+
+class WebLibraryUrls {
+       static public final String INDEX_URL = "/";
+
+       static public final String VERSION_URL = "/version";
+
+       static public final String LOGOUT_URL = "/logout";
+
+       static private final String VIEWER_URL_BASE = "/view/story/";
+       static private final String VIEWER_URL = VIEWER_URL_BASE
+                       + "{luid}/{chap}/{para}";
+
+       static private final String STORY_URL_BASE = "/story/";
+       static private final String STORY_URL = STORY_URL_BASE
+                       + "{luid}/{chap}/{para}";
+       static private final String STORY_URL_COVER = STORY_URL_BASE
+                       + "{luid}/cover";
+       static private final String STORY_URL_JSON = STORY_URL_BASE + "{luid}/json";
+
+       static private final String LIST_URL_BASE = "/list/";
+
+       static public final String LIST_URL_METADATA = LIST_URL_BASE + "metadata";
+
+       static public String getViewUrl(String luid, Integer chap, Integer para) {
+               return VIEWER_URL //
+                               .replace("{luid}", luid) //
+                               .replace("/{chap}", chap == null ? "" : "/" + chap) //
+                               .replace("/{para}",
+                                               (chap == null || para == null) ? "" : "/" + para);
+       }
+
+       static public String getStoryUrl(String luid, int chap, Integer para) {
+               return STORY_URL //
+                               .replace("{luid}", luid) //
+                               .replace("{chap}", Integer.toString(chap)) //
+                               .replace("{para}", para == null ? "" : Integer.toString(para));
+       }
+
+       static public String getStoryUrlCover(String luid) {
+               return STORY_URL_COVER //
+                               .replace("{luid}", luid);
+       }
+
+       static public String getStoryUrlJson(String luid) {
+               return STORY_URL_JSON //
+                               .replace("{luid}", luid);
+       }
+
+       static public boolean isSupportedUrl(String url) {
+               return INDEX_URL.equals(url) || VERSION_URL.equals(url)
+                               || LOGOUT_URL.equals(url) || isViewUrl(url) || isStoryUrl(url)
+                               || isListUrl(url);
+       }
+
+       static public boolean isViewUrl(String url) {
+               return url != null && url.startsWith(VIEWER_URL_BASE);
+       }
+
+       static public boolean isStoryUrl(String url) {
+               return url != null && url.startsWith(STORY_URL_BASE);
+       }
+
+       static public boolean isListUrl(String url) {
+               return url != null && url.startsWith(LIST_URL_BASE);
+       }
+}