Resources system rewrite + new "--save-config DIR" option
[jvcard.git] / src / be / nikiroo / jvcard / remote / Sync.java
index 70bf93c3f7e942e74bc0576ab6b50459aa5ed4a3..609041eaaceef39e449df33a7d85781b99f6c2ab 100644 (file)
@@ -12,19 +12,21 @@ import java.io.OutputStreamWriter;
 import java.net.Socket;
 import java.net.UnknownHostException;
 import java.security.InvalidParameterException;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.MissingResourceException;
-import java.util.ResourceBundle;
+import java.util.Map;
 
 import be.nikiroo.jvcard.Card;
 import be.nikiroo.jvcard.Contact;
 import be.nikiroo.jvcard.Data;
+import be.nikiroo.jvcard.launcher.CardResult;
+import be.nikiroo.jvcard.launcher.CardResult.MergeCallback;
 import be.nikiroo.jvcard.parsers.Format;
 import be.nikiroo.jvcard.parsers.Vcard21Parser;
-import be.nikiroo.jvcard.remote.Command.Verb;
-import be.nikiroo.jvcard.resources.Bundles;
 import be.nikiroo.jvcard.resources.StringUtils;
+import be.nikiroo.jvcard.resources.bundles.RemoteBundle;
+import be.nikiroo.jvcard.resources.enums.RemotingOption;
 
 /**
  * This class will synchronise {@link Card}s between a local instance an a
@@ -35,7 +37,7 @@ import be.nikiroo.jvcard.resources.StringUtils;
  */
 public class Sync {
        /** The time in ms after which we declare that 2 timestamps are different */
-       static private final int GRACE_TIME = 2000;
+       static private final int GRACE_TIME = 2001;
 
        /** Directory where to store local cache of remote {@link Card}s. */
        static private File cacheDir;
@@ -113,7 +115,7 @@ public class Sync {
        }
 
        /**
-        * Check if the synchronisation is available for this resource.
+        * Check if the remote server already know about this resource.
         * 
         * @return TRUE if it is possible to contact the remote server and that this
         *         server has the resource available
@@ -123,7 +125,7 @@ public class Sync {
                        SimpleSocket s = new SimpleSocket(new Socket(host, port),
                                        "check avail client");
                        s.open(true);
-                       s.sendCommand(Verb.LIST);
+                       s.sendCommand(Command.LIST_CARD);
                        List<String> timestampedFiles = s.receiveBlock();
                        s.close();
 
@@ -140,26 +142,52 @@ public class Sync {
                return false;
        }
 
-       // return: synced or not
-       //TODO jDoc
-       public boolean sync(Card card, boolean force) throws UnknownHostException,
-                       IOException {
-
+       /**
+        * Synchronise the current resource if needed, then return the locally
+        * cached version of said resource.
+        * 
+        * <p>
+        * A synchronisation is deemed necessary if one of the following is true:
+        * <ul>
+        * <li><tt>force</tt> is TRUE</li>
+        * <li><tt>CLIENT_AUTO_SYNC</tt> is TRUE in the configuration file</li>
+        * <li>the {@link Card} exists locally but not on the remote server</li>
+        * <li>the {@link Card} exists on the remote server but not locally</li>
+        * </ul>
+        * </p>
+        * 
+        * @param force
+        *            force the synchronisation to occur
+        * @param callback
+        *            the {@link MergeCallback} to call in case of conflict
+        * 
+        * @return the synchronised (or not) {@link Card}
+        * 
+        * @throws UnknownHostException
+        *             in case of server name resolution failure
+        * @throws IOException
+        *             in case of IO error
+        */
+       public CardResult sync(boolean force, MergeCallback callback)
+                       throws UnknownHostException, IOException {
                long tsOriginal = getLastModified();
 
+               Card local = new Card(getCache(cacheDir), Format.VCard21);
+
                // do NOT update unless we are in autoSync or forced mode or we don't
                // have the file on cache
                if (!autoSync && !force && tsOriginal != -1) {
-                       return false;
+                       return new CardResult(local, true, false, false);
                }
 
                SimpleSocket s = new SimpleSocket(new Socket(host, port), "sync client");
 
                // get the server time stamp
                long tsServer = -1;
+               boolean serverChanges = false;
                try {
                        s.open(true);
-                       s.sendCommand(Verb.LIST);
+                       s.sendCommand(Command.LIST_CARD);
                        List<String> timestampedFiles = s.receiveBlock();
 
                        for (String timestampedFile : timestampedFiles) {
@@ -171,134 +199,314 @@ public class Sync {
                                        break;
                                }
                        }
+
+                       // Error cases:
+                       // - file not present neither in cache nor on server
+                       // - remote < previous
+                       if ((tsServer == -1 && tsOriginal == -1)
+                                       || (tsServer != -1 && tsOriginal != -1 && ((tsOriginal - tsServer) > GRACE_TIME))) {
+                               throw new IOException(
+                                               "The timestamps between server and client are invalid");
+                       }
+
+                       // Check changes
+                       serverChanges = (tsServer - tsOriginal) > GRACE_TIME;
+                       boolean localChanges = false;
+                       Card original = null;
+                       if (tsOriginal != -1) {
+                               original = new Card(getCache(cacheDirOrig), Format.VCard21);
+                               localChanges = !local.isEquals(original, true);
+                       }
+
+                       Command action = null;
+
+                       // Sync to server if:
+                       if (localChanges) {
+                               action = Command.PUT_CARD;
+                       }
+
+                       // Sync from server if:
+                       if (serverChanges) {
+                               action = Command.HASH_CONTACT;
+                       }
+
+                       // Sync from/to server if
+                       if (serverChanges && localChanges) {
+                               action = Command.HELP;
+                       }
+
+                       // POST the whole file if:
+                       if (tsServer == -1) {
+                               action = Command.POST_CARD;
+                       }
+
+                       // GET the whole file if:
+                       if (tsOriginal == -1) {
+                               action = Command.GET_CARD;
+                       }
+
+                       System.err.println("remote: " + (tsServer / 1000) % 1000 + " ("
+                                       + tsServer + ")");
+                       System.err.println("previous: " + (tsOriginal / 1000) % 1000 + " ("
+                                       + tsOriginal + ")");
+                       System.err.println("local changes: " + localChanges);
+                       System.err.println("server changes: " + serverChanges);
+                       System.err.println("choosen action: " + action);
+
+                       if (action != null) {
+                               s.sendCommand(Command.SELECT, name);
+                               if (tsServer != StringUtils.toTime(s.receiveLine())) {
+                                       System.err.println("DEBUG: it changed. retry.");
+                                       s.sendCommand(Command.SELECT);
+                                       s.close();
+                                       return sync(force, callback);
+                               }
+
+                               switch (action) {
+                               case GET_CARD: {
+                                       s.sendCommand(Command.GET_CARD);
+                                       List<String> data = s.receiveBlock();
+                                       setLastModified(data.remove(0));
+                                       local.replaceListContent(Vcard21Parser.parseContact(data));
+
+                                       if (local.isDirty())
+                                               local.save();
+                                       local.saveAs(getCache(cacheDirOrig), Format.VCard21);
+                                       break;
+                               }
+                               case POST_CARD: {
+                                       s.sendCommand(Command.POST_CARD);
+                                       s.sendBlock(Vcard21Parser.toStrings(local));
+                                       local.saveAs(getCache(cacheDirOrig), Format.VCard21);
+                                       setLastModified(s.receiveLine());
+                                       break;
+                               }
+                               case PUT_CARD: {
+                                       String serverLastModifTime = updateToServer(s, original,
+                                                       local);
+
+                                       local.saveAs(getCache(cacheDirOrig), Format.VCard21);
+
+                                       setLastModified(serverLastModifTime);
+                                       break;
+                               }
+                               case HASH_CONTACT: {
+                                       String serverLastModifTime = updateFromServer(s, local);
+
+                                       local.save();
+                                       local.saveAs(getCache(cacheDirOrig), Format.VCard21);
+
+                                       setLastModified(serverLastModifTime);
+                                       break;
+                               }
+                               case HELP: {
+                                       // note: we are holding the server here, so it could throw
+                                       // us away if we take too long
+
+                                       // TODO: check if those files are deleted
+                                       File mergeF = File.createTempFile("contact-merge", ".vcf");
+                                       File serverF = File
+                                                       .createTempFile("contact-server", ".vcf");
+                                       original.saveAs(serverF, Format.VCard21);
+
+                                       Card server = new Card(serverF, Format.VCard21);
+                                       updateFromServer(s, server);
+
+                                       // Do an auto sync
+                                       server.saveAs(mergeF, Format.VCard21);
+                                       Card merge = new Card(mergeF, Format.VCard21);
+                                       List<Contact> added = new LinkedList<Contact>();
+                                       List<Contact> removed = new LinkedList<Contact>();
+                                       original.compare(local, added, removed, removed, added);
+                                       for (Contact c : removed)
+                                               merge.getById(c.getId()).delete();
+                                       for (Contact c : added)
+                                               merge.add(Vcard21Parser.clone(c));
+
+                                       merge.save();
+
+                                       // defer to client:
+                                       if (callback == null) {
+                                               throw new IOException(
+                                                               "Conflicting changes detected and merge operation not allowed");
+                                       }
+
+                                       merge = callback.merge(original, local, server, merge);
+                                       if (merge == null) {
+                                               throw new IOException(
+                                                               "Conflicting changes detected and merge operation cancelled");
+                                       }
+
+                                       // TODO: something like:
+                                       // String serverLastModifTime = updateToServer(s, original,
+                                       // merge);
+                                       // ...but without starting with original since it is not
+                                       // true here
+                                       s.sendCommand(Command.POST_CARD);
+                                       s.sendBlock(Vcard21Parser.toStrings(merge));
+                                       String serverLastModifTime = s.receiveLine();
+                                       //
+
+                                       merge.saveAs(getCache(cacheDir), Format.VCard21);
+                                       merge.saveAs(getCache(cacheDirOrig), Format.VCard21);
+
+                                       setLastModified(serverLastModifTime);
+
+                                       local = merge;
+
+                                       break;
+                               }
+                               default:
+                                       // will not happen
+                                       break;
+                               }
+
+                               s.sendCommand(Command.SELECT);
+                       }
                } catch (IOException e) {
-                       s.close();
-                       throw e;
+                       return new CardResult(e);
                } catch (Exception e) {
-                       e.printStackTrace();
+                       return new CardResult(new IOException(e));
+               } finally {
                        s.close();
-                       return false;
                }
 
-               // Error cases:
-               // - file not preset neither in cache nor on server
-               // - remote < previous
-               if ((tsServer == -1 && tsOriginal == -1)
-                               || (tsServer != -1 && tsOriginal != -1 && ((tsOriginal - tsServer) > GRACE_TIME))) {
-                       throw new IOException(
-                                       "The timestamps between server and client are invalid");
-               }
+               return new CardResult(local, true, true, serverChanges);
+       }
 
-               // Check changes
-               boolean serverChanges = (tsServer - tsOriginal) > GRACE_TIME;
-               boolean localChanges = false;
-               Card local = null;
-               Card original = null;
-               if (tsOriginal != -1) {
-                       local = new Card(getCache(cacheDir), Format.VCard21);
-                       original = new Card(getCache(cacheDirOrig), Format.VCard21);
-                       localChanges = !local.isEquals(original, true);
+       /**
+        * Will update the currently selected {@link Card} on the remote server to
+        * be in the same state as <tt>local</tt>, assuming the server is currently
+        * in <tt>original</tt> state.
+        * 
+        * @param s
+        *            the {@link SimpleSocket} to work on, which <b>MUST</b> be in
+        *            SELECT mode
+        * @param original
+        *            the original {@link Card} as it was before the client made
+        *            changes to it
+        * @param local
+        *            the {@link Card} to which state we want the server in
+        * 
+        * @return the last modified time from the remote server (which is basically
+        *         "now")
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       private String updateToServer(SimpleSocket s, Card original, Card local)
+                       throws IOException {
+               List<Contact> added = new LinkedList<Contact>();
+               List<Contact> removed = new LinkedList<Contact>();
+               List<Contact> from = new LinkedList<Contact>();
+               List<Contact> to = new LinkedList<Contact>();
+               original.compare(local, added, removed, from, to);
+
+               s.sendCommand(Command.PUT_CARD);
+
+               for (Contact c : removed) {
+                       s.sendCommand(Command.DELETE_CONTACT, c.getId());
+               }
+               for (Contact c : added) {
+                       s.sendCommand(Command.POST_CONTACT, c.getId());
+                       s.sendBlock(Vcard21Parser.toStrings(c, -1));
+               }
+               if (from.size() > 0) {
+                       for (int index = 0; index < from.size(); index++) {
+                               Contact f = from.get(index);
+                               Contact t = to.get(index);
+
+                               List<Data> subadded = new LinkedList<Data>();
+                               List<Data> subremoved = new LinkedList<Data>();
+                               f.compare(t, subadded, subremoved, subremoved, subadded);
+                               s.sendCommand(Command.PUT_CONTACT, f.getId());
+                               for (Data d : subremoved) {
+                                       s.sendCommand(Command.DELETE_DATA, d.getContentState(true));
+                               }
+                               for (Data d : subadded) {
+                                       s.sendCommand(Command.POST_DATA, d.getContentState(true));
+                                       s.sendBlock(Vcard21Parser.toStrings(d));
+                               }
+                               s.sendCommand(Command.PUT_CONTACT);
+                       }
                }
 
-               Verb action = null;
+               s.sendCommand(Command.PUT_CARD);
 
-               // Sync to server if:
-               if (localChanges) {
-                       action = Verb.PUT_CARD;
+               return s.receiveLine();
+       }
+
+       /**
+        * Will update the given {@link Card} object (not {@link File}) to the
+        * currently selected {@link Card} on the remote server.
+        * 
+        * @param s
+        *            the {@link SimpleSocket} to work on, which <b>MUST</b> be in
+        *            SELECT mode
+        * @param local
+        *            the {@link Card} to update
+        * 
+        * @return the last modified time from the remote server
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       private String updateFromServer(SimpleSocket s, Card local)
+                       throws IOException {
+               s.sendCommand(Command.PUT_CARD);
+
+               s.sendCommand(Command.LIST_CONTACT);
+               Map<String, String> remote = new HashMap<String, String>();
+               for (String line : s.receiveBlock()) {
+                       int indexSp = line.indexOf(" ");
+                       String hash = line.substring(0, indexSp);
+                       String uid = line.substring(indexSp + 1);
+
+                       remote.put(uid, hash);
                }
 
-               // Sync from/to server if
-               if (serverChanges && localChanges) {
-                       action = Verb.PUT_CARD;
+               List<Contact> deleted = new LinkedList<Contact>();
+               List<Contact> changed = new LinkedList<Contact>();
+               List<String> added = new LinkedList<String>();
+
+               for (Contact c : local) {
+                       String hash = remote.get(c.getId());
+                       if (hash == null) {
+                               deleted.add(c);
+                       } else if (!hash.equals(c.getContentState(true))) {
+                               changed.add(c);
+                       }
                }
 
-               // Sync from server if:
-               if (serverChanges) {
-                       // TODO: only sends changed cstate if serverChanges
-                       action = Verb.GET_CARD;
+               for (String uid : remote.keySet()) {
+                       if (local.getById(uid) == null)
+                               added.add(uid);
                }
 
-               // PUT the whole file if:
-               if (tsServer == -1) {
-                       action = Verb.POST_CARD;
+               // process:
+
+               for (Contact c : deleted) {
+                       c.delete();
                }
 
-               // GET the whole file if:
-               if (tsOriginal == -1) {
-                       action = Verb.GET_CARD;
+               for (String uid : added) {
+                       s.sendCommand(Command.GET_CONTACT, uid);
+                       for (Contact cc : Vcard21Parser.parseContact(s.receiveBlock())) {
+                               local.add(cc);
+                       }
                }
 
-               System.err.println("remote: " + (tsServer / 1000) % 1000 + " ("
-                               + tsServer + ")");
-               System.err.println("previous: " + (tsOriginal / 1000) % 1000 + " ("
-                               + tsOriginal + ")");
-               System.err.println("local changes: " + localChanges);
-               System.err.println("server changes: " + serverChanges);
-               System.err.println("choosen action: " + action);
-
-               if (action != null) {
-                       switch (action) {
-                       case GET_CARD:
-                               s.sendCommand(Verb.GET_CARD, name);
-                               List<String> data = s.receiveBlock();
-                               setLastModified(data.remove(0));
-                               Card server = new Card(Vcard21Parser.parseContact(data));
-                               card.replaceListContent(server);
-
-                               if (card.isDirty())
-                                       card.save();
-                               card.saveAs(getCache(cacheDirOrig), Format.VCard21);
-                               break;
-                       case POST_CARD:
-                               s.sendCommand(Verb.POST_CARD, name);
-                               s.sendBlock(Vcard21Parser.toStrings(card));
-                               card.saveAs(getCache(cacheDirOrig), Format.VCard21);
-                               setLastModified(s.receiveLine());
-                               break;
-                       case PUT_CARD:
-                               List<Contact> added = new LinkedList<Contact>();
-                               List<Contact> removed = new LinkedList<Contact>();
-                               List<Contact> from = new LinkedList<Contact>();
-                               List<Contact> to = new LinkedList<Contact>();
-                               original.compare(local, added, removed, from, to);
-                               s.sendCommand(Verb.PUT_CARD, name);
-                               for (Contact c : removed) {
-                                       s.sendCommand(Verb.DELETE_CONTACT, c.getId());
-                               }
-                               for (Contact c : added) {
-                                       s.sendCommand(Verb.POST_CONTACT, c.getId());
-                                       s.sendBlock(Vcard21Parser.toStrings(c, -1));
-                               }
-                               if (from.size() > 0) {
-                                       for (int index = 0; index < from.size(); index++) {
-                                               Contact f = from.get(index);
-                                               Contact t = to.get(index);
-
-                                               List<Data> subadded = new LinkedList<Data>();
-                                               List<Data> subremoved = new LinkedList<Data>();
-                                               f.compare(t, subadded, subremoved, subremoved, subadded);
-                                               s.sendCommand(Verb.PUT_CONTACT, name);
-                                               for (Data d : subremoved) {
-                                                       s.sendCommand(Verb.DELETE_DATA, d.getContentState());
-                                               }
-                                               for (Data d : subadded) {
-                                                       s.sendCommand(Verb.POST_DATA, d.getContentState());
-                                                       s.sendBlock(Vcard21Parser.toStrings(d));
-                                               }
-                                       }
-                               }
-                               s.sendCommand(Verb.PUT_CARD);
-                               break;
-                       default:
-                               // TODO
-                               throw new IOException(action
-                                               + " operation not supported yet :(");
+               for (Contact c : changed) {
+                       c.delete();
+                       s.sendCommand(Command.GET_CONTACT, c.getId());
+                       for (Contact cc : Vcard21Parser.parseContact(s.receiveBlock())) {
+                               local.add(cc);
                        }
                }
 
-               s.close();
+               s.sendCommand(Command.PUT_CARD);
 
-               return true;
+               return s.receiveLine();
        }
 
        /**
@@ -379,10 +587,10 @@ public class Sync {
         */
        static private void config() {
                String dir = null;
-               ResourceBundle bundle = Bundles.getBundle("remote");
+               RemoteBundle bundle = new RemoteBundle();
 
                try {
-                       dir = bundle.getString("CLIENT_CACHE_DIR").trim();
+                       dir = bundle.getString(RemotingOption.CLIENT_CACHE_DIR);
 
                        cacheDir = new File(dir + File.separator + "current");
                        cacheDir.mkdir();
@@ -396,14 +604,8 @@ public class Sync {
                                                + dir);
                        }
 
-                       String autoStr = bundle.getString("CLIENT_AUTO_SYNC");
-                       if (autoStr != null && autoStr.trim().equalsIgnoreCase("true")) {
-                               autoSync = true;
-                       }
-
-               } catch (MissingResourceException e) {
-                       throw new InvalidParameterException(
-                                       "Cannot access remote.properties configuration file");
+                       autoSync = bundle
+                                       .getBoolean(RemotingOption.CLIENT_AUTO_SYNC, false);
                } catch (Exception e) {
                        throw new InvalidParameterException(
                                        "Cannot open or create cache store at: " + dir);