Remote support ~complete (need more tests at least)
authorNiki Roo <niki@nikiroo.be>
Thu, 24 Mar 2016 04:22:02 +0000 (05:22 +0100)
committerNiki Roo <niki@nikiroo.be>
Thu, 24 Mar 2016 04:22:02 +0000 (05:22 +0100)
12 files changed:
src/be/nikiroo/jvcard/BaseClass.java
src/be/nikiroo/jvcard/Card.java
src/be/nikiroo/jvcard/launcher/CardResult.java [new file with mode: 0644]
src/be/nikiroo/jvcard/launcher/Main.java
src/be/nikiroo/jvcard/launcher/Optional.java
src/be/nikiroo/jvcard/parsers/Vcard21Parser.java
src/be/nikiroo/jvcard/remote/Server.java
src/be/nikiroo/jvcard/remote/Sync.java
src/be/nikiroo/jvcard/tui/KeyAction.java
src/be/nikiroo/jvcard/tui/MainWindow.java
src/be/nikiroo/jvcard/tui/panes/FileList.java
src/be/nikiroo/jvcard/tui/panes/MainContent.java

index df0fcf00de78f998ad87d03842dc4947f86a966a..1dd76442324ca845d2f603e5dd04c532bd7bdd32 100644 (file)
@@ -1,5 +1,6 @@
 package be.nikiroo.jvcard;
 
+import java.security.InvalidParameterException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -16,12 +17,17 @@ import be.nikiroo.jvcard.resources.StringUtils;
  * sends all commands down to the initial list, but will mark itself and its
  * children as dirty or not when needed.
  * 
- * All child elements can identify their parent.
+ * <p>
+ * All child elements can identify their parent, and must not be added to 2
+ * different objects without without first being removed from the previous one.
+ * </p>
  * 
+ * <p>
  * The dirty state is bubbling up (when dirty = true) or down (when dirty =
  * false) -- so, making changes to a child element will also mark its parent as
  * "dirty", and marking an element as pristine will also affect all its child
  * elements.
+ * </p>
  * 
  * @author niki
  *
@@ -119,8 +125,8 @@ public abstract class BaseClass<E extends BaseClass<?>> implements List<E> {
         * If not equals, the differences will be represented by the given
         * {@link List}s if they are not NULL.
         * <ul>
-        * <li><tt>added</tt>will represent the elements in <tt>list</tt> but not in
-        * <tt>this</tt></li>
+        * <li><tt>added</tt> will represent the elements in <tt>list</tt> but not
+        * in <tt>this</tt></li>
         * <li><tt>removed</tt> will represent the elements in <tt>this</tt> but not
         * in <tt>list</tt></li>
         * <li><tt>from<tt> will represent the elements in <tt>list</tt> that are
@@ -250,22 +256,25 @@ public abstract class BaseClass<E extends BaseClass<?>> implements List<E> {
        }
 
        /**
-        * Get the recursive state of the current object, i.e., its children. It
-        * represents the full state information about this object's children. It
-        * may not contains spaces nor new lines.
+        * Get the recursive state of the current object, i.e., its children
+        * included. It represents the full state information about this object's
+        * children. It may not contains spaces nor new lines.
         * 
         * <p>
         * Not that this state is <b>lossy</b>. You cannot retrieve the data from
-        * the state, it can only be used as an ID to check if thw data are
-        * identical.
+        * the state, it can only be used as an ID to check if data are identical.
         * </p>
         * 
+        * @param self
+        *            also include state information about the current object itself
+        *            (as opposed to its children)
+        * 
         * @return a {@link String} representing the current content state of this
         *         object, i.e., its children included
         */
-       public String getContentState() {
+       public String getContentState(boolean self) {
                StringBuilder builder = new StringBuilder();
-               buildContentStateRaw(builder);
+               buildContentStateRaw(builder, self);
                return StringUtils.getHash(builder.toString());
        }
 
@@ -291,6 +300,23 @@ public abstract class BaseClass<E extends BaseClass<?>> implements List<E> {
                return null;
        }
 
+       /**
+        * Return a {@link String} that can be used to identify this object in DEBUG
+        * mode, i.e., a "toString" method that can identify the object's content
+        * but still be readable in a log.
+        * 
+        * @param depth
+        *            the depth into which to descend (0 = only this object, not its
+        *            children)
+        * 
+        * @return the debug {@link String}
+        */
+       public String getDebugInfo(int depth) {
+               StringBuilder builder = new StringBuilder();
+               getDebugInfo(builder, depth, 0);
+               return builder.toString();
+       }
+
        /**
         * Return the current ID of this object -- it is allowed to change over time
         * (so, do not cache it).
@@ -317,19 +343,59 @@ public abstract class BaseClass<E extends BaseClass<?>> implements List<E> {
        abstract public String getState();
 
        /**
-        * Get the recursive state of the current object, i.e., its children. It
-        * represents the full state information about this object's children.
+        * Get the recursive state of the current object, i.e., its children
+        * included. It represents the full state information about this object's
+        * children.
         * 
         * It is not hashed.
         * 
         * @param builder
         *            the {@link StringBuilder} that will represent the current
         *            content state of this object, i.e., its children included
+        * @param self
+        *            also include state information about the current object itself
+        *            (as opposed to its children)
         */
-       void buildContentStateRaw(StringBuilder builder) {
-               builder.append(getState());
+       void buildContentStateRaw(StringBuilder builder, boolean self) {
+               Collections.sort(this.list, comparator);
+               if (self)
+                       builder.append(getState());
                for (E child : this) {
-                       child.buildContentStateRaw(builder);
+                       child.buildContentStateRaw(builder, true);
+               }
+       }
+
+       /**
+        * Populate a {@link StringBuilder} that can be used to identify this object
+        * in DEBUG mode, i.e., a "toString" method that can identify the object's
+        * content but still be readable in a log.
+        * 
+        * @param depth
+        *            the depth into which to descend (0 = only this object, not its
+        *            children)
+        * 
+        * @param tab
+        *            the current tabulation increment
+        */
+       void getDebugInfo(StringBuilder builder, int depth, int tab) {
+               for (int i = 0; i < tab; i++)
+                       builder.append("        ");
+               builder.append(getContentState(false) + "       " + getId());
+
+               if (depth > 0)
+                       builder.append(": [");
+
+               if (depth > 0) {
+                       for (E child : this) {
+                               builder.append("\n");
+                               child.getDebugInfo(builder, depth - 1, tab + 1);
+                       }
+               }
+               if (depth > 0) {
+                       builder.append("\n");
+                       for (int i = 0; i < tab; i++)
+                               builder.append("        ");
+                       builder.append("]");
                }
        }
 
@@ -374,6 +440,12 @@ public abstract class BaseClass<E extends BaseClass<?>> implements List<E> {
         *            the element to remove from this
         */
        private void _leave(E child) {
+               if (child.parent != null && child.parent != this) {
+                       throw new InvalidParameterException(
+                                       "You are removing this child from its rightful parent, it must be yours to do so");
+               }
+
+               child.parent = null;
                setDirty();
        }
 
@@ -394,6 +466,11 @@ public abstract class BaseClass<E extends BaseClass<?>> implements List<E> {
         *            the element to add to this
         */
        private void _enter(E child, boolean initialLoad) {
+               if (child.parent != null && child.parent != this) {
+                       throw new InvalidParameterException(
+                                       "You are stealing this child from its rightful parent, you must remove it first");
+               }
+
                child.setParent(this);
                if (!initialLoad) {
                        setDirty();
index d2567a496237c159546fa40f46015cebc241223b..547535804b8f37fa80fb5dc3ca22f89afc22857e 100644 (file)
@@ -22,7 +22,6 @@ public class Card extends BaseClass<Contact> {
        private String name;
        private Format format;
        private long lastModified;
-       private boolean remote;
 
        /**
         * Create a new {@link Card} from the given {@link File} and {@link Format}.
@@ -73,7 +72,7 @@ public class Card extends BaseClass<Contact> {
         * @throws InvalidParameterException
         *             if format is NULL
         */
-       public Card(List<Contact> contacts) throws IOException {
+       public Card(List<Contact> contacts) {
                super(contacts);
 
                lastModified = -1;
@@ -197,25 +196,6 @@ public class Card extends BaseClass<Contact> {
                return lastModified;
        }
 
-       /**
-        * Check if this {@link Card} is remote.
-        * 
-        * @return TRUE if this {@link Card} is remote
-        */
-       public boolean isRemote() {
-               return remote;
-       }
-
-       /**
-        * Set the remote option on this {@link Card}.
-        * 
-        * @param remote
-        *            TRUE if this {@link Card} is remote
-        */
-       public void setRemote(boolean remote) {
-               this.remote = remote;
-       }
-
        @Override
        public String toString() {
                return toString(Format.VCard21);
diff --git a/src/be/nikiroo/jvcard/launcher/CardResult.java b/src/be/nikiroo/jvcard/launcher/CardResult.java
new file mode 100644 (file)
index 0000000..68a5ae7
--- /dev/null
@@ -0,0 +1,118 @@
+package be.nikiroo.jvcard.launcher;
+
+import java.io.IOException;
+
+import be.nikiroo.jvcard.Card;
+
+/**
+ * This class is a placeholder for a {@link Card} result and some information
+ * about it.
+ * 
+ * @author niki
+ *
+ */
+public class CardResult {
+       /**
+        * This interface represents the merge callback when the {@link Card}
+        * synchronisation is not able to process fully automatically.
+        * 
+        * @author niki
+        *
+        */
+       public interface MergeCallback {
+               /**
+                * This method will be called when the local cache and the server both
+                * have changes. You need to review the proposed changes, or do your own
+                * merge, and return the final result. You can also cancel the merge
+                * operation by returning NULL.
+                * 
+                * @param previous
+                *            the previous version of the {@link Card}
+                * @param local
+                *            the local cache version of the {@link Card}
+                * @param server
+                *            the remote server version of the {@link Card}
+                * @param autoMerged
+                *            the automatic merge result you should manually check
+                * 
+                * @return the final merged result, or NULL for cancel
+                */
+               public Card merge(Card previous, Card local, Card server,
+                               Card autoMerged);
+       }
+
+       private Card card;
+       private boolean remote;
+       private boolean synced;
+       private boolean changed;
+       private IOException exception;
+
+       /**
+        * Create a new {@link CardResult}.
+        * 
+        * @param card
+        *            the target {@link Card}
+        * @param remtote
+        *            TRUE if it is linked to a remote server
+        * @param synced
+        *            TRUE if it was synchronised
+        */
+       public CardResult(Card card, boolean remote, boolean synced, boolean changed) {
+               this.card = card;
+               this.synced = synced;
+               this.changed = changed;
+       }
+
+       /**
+        * Create a new {@link CardResult}.
+        * 
+        * @param exception
+        *            the synchronisation exception that occurred
+        */
+       public CardResult(IOException exception) {
+               this(null, true, false, false);
+               this.exception = exception;
+       }
+
+       /**
+        * Check if this {@link Card} is linked to a remote jVCard server.
+        * 
+        * @return TRUE if it is
+        */
+       public boolean isRemote() {
+               return remote;
+       }
+
+       /**
+        * Check if the {@link Card} was synchronised.
+        * 
+        * @return TRUE if it was
+        */
+       public boolean isSynchronised() {
+               return synced;
+       }
+
+       /**
+        * Check if this {@link Card} changed after the synchronisation.
+        * 
+        * @return TRUE if it has
+        */
+       public boolean isChanged() {
+               return remote && changed;
+       }
+
+       /**
+        * Return the {@link Card}
+        * 
+        * @return the {@link Card}
+        * 
+        * @throws IOException
+        *             in case of synchronisation issues
+        */
+       public Card getCard() throws IOException {
+               if (exception != null)
+                       throw exception;
+
+               return card;
+       }
+}
index 997cdecc4842b0759b183bbdc243c32137c3a0fa..06c55c655819b8517c13fa55dd44141556e90e46 100644 (file)
@@ -9,6 +9,7 @@ import java.util.LinkedList;
 import java.util.List;
 
 import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.launcher.CardResult.MergeCallback;
 import be.nikiroo.jvcard.parsers.Format;
 import be.nikiroo.jvcard.remote.Command;
 import be.nikiroo.jvcard.remote.SimpleSocket;
@@ -243,13 +244,18 @@ public class Main {
         * @param input
         *            a filename or a remote jvcard url with named resource (e.g.:
         *            <tt>jvcard://localhost:4444/coworkers.vcf</tt>)
+        * @param callback
+        *            the {@link MergeCallback} to call in case of conflict, or NULL
+        *            to disallow conflict management (the {@link Card} will not be
+        *            allowed to synchronise in case of conflicts)
         * 
         * @return the {@link Card}
         * 
         * @throws IOException
         *             in case of IO error or remoting not available
         */
-       static public Card getCard(String input) throws IOException {
+       static public CardResult getCard(String input, MergeCallback callback)
+                       throws IOException {
                boolean remote = false;
                Format format = Format.Abook;
                String ext = input;
@@ -265,12 +271,13 @@ public class Main {
                        remote = true;
                }
 
-               Card card = null;
+               CardResult card = null;
                try {
                        if (remote) {
-                               card = Optional.syncCard(input);
+                               card = Optional.syncCard(input, callback);
                        } else {
-                               card = new Card(new File(input), format);
+                               card = new CardResult(new Card(new File(input), format), false,
+                                               false, false);
                        }
                } catch (IOException ioe) {
                        throw ioe;
index b737c019502375fca91795019f4c0260477ab534..9eb0d6b4085d997f964503671e026a529cfca0a9 100644 (file)
@@ -6,6 +6,7 @@ import java.lang.reflect.Method;
 import java.util.List;
 
 import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.launcher.CardResult.MergeCallback;
 
 /**
  * This class let you call "optional" methods, that is, methods and classes that
@@ -104,6 +105,10 @@ class Optional {
         * @param input
         *            the jvcard:// with resource name URL (e.g.:
         *            <tt>jvcard://localhost:4444/coworkers</tt>)
+        * @param callback
+        *            the {@link MergeCallback} to call in case of conflict, or NULL
+        *            to disallow conflict management (the {@link Card} will not be
+        *            allowed to synchronise in case of conflicts)
         * 
         * @throws SecurityException
         *             in case of internal error
@@ -123,17 +128,17 @@ class Optional {
         *             in case of IO error
         */
        @SuppressWarnings("unchecked")
-       static public Card syncCard(String input) throws ClassNotFoundException,
-                       NoSuchMethodException, SecurityException, InstantiationException,
-                       IllegalAccessException, IllegalArgumentException,
-                       InvocationTargetException, IOException {
+       static public CardResult syncCard(String input, MergeCallback callback)
+                       throws ClassNotFoundException, NoSuchMethodException,
+                       SecurityException, InstantiationException, IllegalAccessException,
+                       IllegalArgumentException, InvocationTargetException, IOException {
                @SuppressWarnings("rawtypes")
                Class syncClass = Class.forName("be.nikiroo.jvcard.remote.Sync");
-               Method sync = syncClass.getDeclaredMethod("sync",
-                               new Class[] { boolean.class });
+               Method sync = syncClass.getDeclaredMethod("sync", new Class[] {
+                               boolean.class, MergeCallback.class });
 
                Object o = syncClass.getConstructor(String.class).newInstance(input);
-               Card card = (Card) sync.invoke(o, false);
+               CardResult card = (CardResult) sync.invoke(o, false, callback);
 
                return card;
        }
index 7d0f5e86beb98e17efb7cef1c4520fef3faa798b..61f901606496031c6001425e3a66a138680eaa57 100644 (file)
@@ -239,6 +239,31 @@ public class Vcard21Parser {
                return lines;
        }
 
+       /**
+        * Clone the given {@link Card} by exporting then importing it again in VCF.
+        * 
+        * @param c
+        *            the {@link Card} to clone
+        * 
+        * @return the clone {@link Contact}
+        */
+       public static Card clone(Card c) {
+               return new Card(parseContact(toStrings(c)));
+       }
+
+       /**
+        * Clone the given {@link Contact} by exporting then importing it again in
+        * VCF.
+        * 
+        * @param c
+        *            the {@link Contact} to clone
+        * 
+        * @return the clone {@link Contact}
+        */
+       public static Contact clone(Contact c) {
+               return parseContact(toStrings(c, -1)).get(0);
+       }
+
        /**
         * Check if the given line is a continuation line or not.
         * 
index 08091f6ea352e59cd96e5ec2a59032650eaea65c..60f323ad15df89fd148cf06a0cce8fab125cf2aa 100644 (file)
@@ -453,7 +453,7 @@ public class Server implements Runnable {
                        if (contact == null) {
                                s.sendBlock();
                        } else {
-                               s.sendLine(contact.getContentState());
+                               s.sendLine(contact.getContentState(true));
                        }
                        break;
                }
@@ -464,7 +464,8 @@ public class Server implements Runnable {
                                                || (contact.getPreferredDataValue("FN") + contact
                                                                .getPreferredDataValue("N")).toLowerCase()
                                                                .contains(cmd.getParam().toLowerCase())) {
-                                       s.send(contact.getContentState() + " " + contact.getId());
+                                       s.send(contact.getContentState(true) + " "
+                                                       + contact.getId());
                                }
                        }
                        s.sendBlock();
@@ -527,7 +528,7 @@ public class Server implements Runnable {
                        String cstate = cmd.getParam();
                        Data data = null;
                        for (Data d : contact) {
-                               if (cstate.equals(d.getContentState()))
+                               if (cstate.equals(d.getContentState(true)))
                                        data = d;
                        }
 
@@ -543,7 +544,7 @@ public class Server implements Runnable {
                        String cstate = cmd.getParam();
                        Data data = null;
                        for (Data d : contact) {
-                               if (cstate.equals(d.getContentState()))
+                               if (cstate.equals(d.getContentState(true)))
                                        data = d;
                        }
 
@@ -559,7 +560,7 @@ public class Server implements Runnable {
                case HASH_DATA: {
                        for (Data data : contact) {
                                if (data.getId().equals(cmd.getParam())) {
-                                       s.send(data.getContentState());
+                                       s.send(data.getContentState(true));
                                }
                        }
                        s.sendBlock();
@@ -571,7 +572,7 @@ public class Server implements Runnable {
                                                || cmd.getParam().length() == 0
                                                || data.getName().toLowerCase()
                                                                .contains(cmd.getParam().toLowerCase())) {
-                                       s.send(data.getContentState() + " " + data.getName());
+                                       s.send(data.getContentState(true) + " " + data.getName());
                                }
                        }
                        s.sendBlock();
index 771798c353b651f3dd3d1a9243d118cfa7418a24..996366537c90a1ed55227fb64c494d45a294fedf 100644 (file)
@@ -22,6 +22,8 @@ import java.util.ResourceBundle;
 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.resources.Bundles;
@@ -157,6 +159,8 @@ public class Sync {
         * 
         * @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}
         * 
@@ -165,23 +169,23 @@ public class Sync {
         * @throws IOException
         *             in case of IO error
         */
-       public Card sync(boolean force) throws UnknownHostException, IOException {
-
+       public CardResult sync(boolean force, MergeCallback callback)
+                       throws UnknownHostException, IOException {
                long tsOriginal = getLastModified();
 
                Card local = new Card(getCache(cacheDir), Format.VCard21);
-               local.setRemote(true);
 
                // 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 local;
+                       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(Command.LIST_CARD);
@@ -207,7 +211,7 @@ public class Sync {
                        }
 
                        // Check changes
-                       boolean serverChanges = (tsServer - tsOriginal) > GRACE_TIME;
+                       serverChanges = (tsServer - tsOriginal) > GRACE_TIME;
                        boolean localChanges = false;
                        Card original = null;
                        if (tsOriginal != -1) {
@@ -256,7 +260,7 @@ public class Sync {
                                        System.err.println("DEBUG: it changed. retry.");
                                        s.sendCommand(Command.SELECT);
                                        s.close();
-                                       return sync(force);
+                                       return sync(force, callback);
                                }
 
                                switch (action) {
@@ -264,8 +268,7 @@ public class Sync {
                                        s.sendCommand(Command.GET_CARD);
                                        List<String> data = s.receiveBlock();
                                        setLastModified(data.remove(0));
-                                       Card server = new Card(Vcard21Parser.parseContact(data));
-                                       local.replaceListContent(server);
+                                       local.replaceListContent(Vcard21Parser.parseContact(data));
 
                                        if (local.isDirty())
                                                local.save();
@@ -298,12 +301,10 @@ public class Sync {
                                        break;
                                }
                                case HELP: {
-                                       if (true)
-                                               throw new IOException("two-way sync not supported yet");
-
                                        // 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");
@@ -312,15 +313,40 @@ public class Sync {
                                        Card server = new Card(serverF, Format.VCard21);
                                        updateFromServer(s, server);
 
-                                       // TODO: auto merge into mergeF (from original, local,
-                                       // server)
-                                       local.saveAs(mergeF, Format.VCard21);
+                                       // Do an auto sync
+                                       server.saveAs(mergeF, Format.VCard21);
                                        Card merge = new Card(mergeF, Format.VCard21);
-
-                                       // TODO: ask client if ok or to change it herself
-
-                                       String serverLastModifTime = updateToServer(s, original,
-                                                       merge);
+                                       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);
@@ -331,20 +357,22 @@ public class Sync {
 
                                        break;
                                }
+                               default:
+                                       // will not happen
+                                       break;
                                }
 
                                s.sendCommand(Command.SELECT);
                        }
                } catch (IOException e) {
-                       throw e;
+                       return new CardResult(e);
                } catch (Exception e) {
-                       e.printStackTrace();
-                       return local;
+                       return new CardResult(new IOException(e));
                } finally {
                        s.close();
                }
 
-               return local;
+               return new CardResult(local, true, true, serverChanges);
        }
 
        /**
@@ -392,14 +420,15 @@ public class Sync {
                                List<Data> subadded = new LinkedList<Data>();
                                List<Data> subremoved = new LinkedList<Data>();
                                f.compare(t, subadded, subremoved, subremoved, subadded);
-                               s.sendCommand(Command.PUT_CONTACT, name);
+                               s.sendCommand(Command.PUT_CONTACT, f.getId());
                                for (Data d : subremoved) {
-                                       s.sendCommand(Command.DELETE_DATA, d.getContentState());
+                                       s.sendCommand(Command.DELETE_DATA, d.getContentState(true));
                                }
                                for (Data d : subadded) {
-                                       s.sendCommand(Command.POST_DATA, d.getContentState());
+                                       s.sendCommand(Command.POST_DATA, d.getContentState(true));
                                        s.sendBlock(Vcard21Parser.toStrings(d));
                                }
+                               s.sendCommand(Command.PUT_CONTACT);
                        }
                }
 
@@ -445,7 +474,7 @@ public class Sync {
                        String hash = remote.get(c.getId());
                        if (hash == null) {
                                deleted.add(c);
-                       } else if (!hash.equals(c.getContentState())) {
+                       } else if (!hash.equals(c.getContentState(true))) {
                                changed.add(c);
                        }
                }
index f39b10324f0eeb8d3fcd62d44fc664a28e791e85..2c10e6297d03864eb1a4e6be9a6d77e08cb41d2d 100644 (file)
@@ -58,6 +58,8 @@ public class KeyAction {
        private StringId id;
        private KeyStroke key;
        private Mode mode;
+       private String message;
+       private boolean error;
 
        public KeyAction(Mode mode, KeyStroke key, StringId id) {
                this.id = id;
@@ -78,17 +80,56 @@ public class KeyAction {
        }
 
        /**
-        * Return the key used to trigger this {@link KeyAction} or '\0' if none.
-        * Also check the special key ({@link KeyAction#getKkey}) if any.
+        * Return the key used to trigger this {@link KeyAction}.
         * 
-        * @return the shortcut character to use to invoke this {@link KeyAction} or
-        *         '\0'
+        * @return the shortcut {@link KeyStroke} to use to invoke this
+        *         {@link KeyAction}
         */
        public KeyStroke getKey() {
                return key;
        }
 
-       // check if the given key should trigger this action
+       /**
+        * Return the associated message if any.
+        * 
+        * @return the associated message or NULL
+        */
+       public String getMessage() {
+               return message;
+       }
+
+       /**
+        * Set a message to display to the user. This message will be get after
+        * {@link KeyAction#getObject()} has been called.
+        * 
+        * @param message
+        *            the message
+        * @param error
+        *            TRUE for an error message, FALSE for information
+        */
+       public void setMessage(String message, boolean error) {
+               this.message = message;
+               this.error = error;
+       }
+
+       /**
+        * Check if the included message ({@link KeyAction#getMessage()}) is an
+        * error message or an information message.
+        * 
+        * @return TRUE for error, FALSE for information
+        */
+       public boolean isError() {
+               return error;
+       }
+
+       /**
+        * Check if the given {@link KeyStroke} should trigger this action.
+        * 
+        * @param mkey
+        *            the {@link KeyStroke} to check against
+        * 
+        * @return TRUE if it should
+        */
        public boolean match(KeyStroke mkey) {
                if (mkey == null || key == null)
                        return false;
@@ -103,16 +144,6 @@ public class KeyAction {
                return false;
        }
 
-       /**
-        * Return the kind of key this {@link KeyAction } is linked to. Will be
-        * {@link KeyType#NormalKey} if only normal keys can invoke this
-        * {@link KeyAction}. Also check the normal key ({@link KeyAction#getKey})
-        * if any.
-        * 
-        * @return the special shortcut key to use to invoke this {@link KeyAction}
-        *         or {@link KeyType#NormalKey}
-        */
-
        /**
         * The mode to change to when this action is completed.
         * 
@@ -122,10 +153,21 @@ public class KeyAction {
                return mode;
        }
 
+       /**
+        * Get the associated {@link StringId} or NULL if the action must not be
+        * displayed in the action bar.
+        * 
+        * @return the {@link StringId} or NULL
+        */
        public StringId getStringId() {
                return id;
        }
 
+       /**
+        * Get the associated object as a {@link Card} if it is a {@link Card}.
+        * 
+        * @return the associated {@link Card} or NULL
+        */
        public Card getCard() {
                Object o = getObject();
                if (o instanceof Card)
@@ -133,6 +175,12 @@ public class KeyAction {
                return null;
        }
 
+       /**
+        * Get the associated object as a {@link Contact} if it is a {@link Contact}
+        * .
+        * 
+        * @return the associated {@link Contact} or NULL
+        */
        public Contact getContact() {
                Object o = getObject();
                if (o instanceof Contact)
@@ -140,6 +188,11 @@ public class KeyAction {
                return null;
        }
 
+       /**
+        * Get the associated object as a {@link Data} if it is a {@link Data}.
+        * 
+        * @return the associated {@link Data} or NULL
+        */
        public Data getData() {
                Object o = getObject();
                if (o instanceof Data)
@@ -147,7 +200,23 @@ public class KeyAction {
                return null;
        }
 
-       // override this one if needed, DO NOT process here as it will be call a lot
+       /**
+        * Return the associated target object. You should use
+        * {@link KeyAction#getCard()}, {@link KeyAction#getContact()} or
+        * {@link KeyAction#getData()} instead if you know the kind of object it is.
+        * 
+        * <p>
+        * 
+        * You are expected to override this method to return your object, the 3
+        * afore-mentioned methods will use this one as the source.
+        * 
+        * <p>
+        * 
+        * <b>DO NOT</b> process data here, this method will be called often; this
+        * should only be a <b>getter</b> method.
+        * 
+        * @return the associated object
+        */
        public Object getObject() {
                return null;
        }
index 236289904a07297401e35754a8f0d1babc98e12e..d7cb80d8b2e624ad2b6453914c0ff13a1e878b1f 100644 (file)
@@ -1,5 +1,6 @@
 package be.nikiroo.jvcard.tui;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedList;
@@ -221,6 +222,16 @@ public class MainWindow extends BasicWindow {
                if (contentStack.size() > 0)
                        prev = contentStack.remove(contentStack.size() - 1);
 
+               if (prev != null) {
+                       try {
+                               String mess = prev.wakeup();
+                               if (mess != null)
+                                       setMessage(mess, false);
+                       } catch (IOException e) {
+                               setMessage(e.getMessage(), true);
+                       }
+               }
+
                pushContent(prev);
 
                return removed;
@@ -610,6 +621,12 @@ public class MainWindow extends BasicWindow {
 
                        handled = true;
 
+                       action.getObject(); // see {@link KeyAction#getMessage()}
+                       String mess = action.getMessage();
+                       if (mess != null) {
+                               setMessage(mess, action.isError());
+                       }
+
                        if (action.onAction()) {
                                handleAction(action, null);
                        }
@@ -658,6 +675,10 @@ public class MainWindow extends BasicWindow {
                case CONTACT_LIST:
                        if (action.getCard() != null) {
                                pushContent(new ContactList(action.getCard()));
+                       } else if (action.getObject() != null
+                                       && action.getObject() instanceof MainContent) {
+                               MainContent mergeContent = (MainContent) action.getObject();
+                               pushContent(mergeContent);
                        }
                        break;
                case CONTACT_DETAILS:
index e7632e02344514fa4ecdb8a29dc89b77d6b7f75f..7065aaf679a6a0290c1ec7de401a716dd7a736bf 100644 (file)
@@ -1,12 +1,16 @@
 package be.nikiroo.jvcard.tui.panes;
 
+import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
 
 import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.launcher.CardResult;
+import be.nikiroo.jvcard.launcher.CardResult.MergeCallback;
 import be.nikiroo.jvcard.launcher.Main;
+import be.nikiroo.jvcard.parsers.Format;
 import be.nikiroo.jvcard.resources.StringUtils;
 import be.nikiroo.jvcard.resources.Trans;
 import be.nikiroo.jvcard.tui.KeyAction;
@@ -18,7 +22,12 @@ import com.googlecode.lanterna.input.KeyType;
 
 public class FileList extends MainContentList {
        private List<String> files;
-       private List<Card> cards;
+       private List<CardResult> cards;
+
+       private FileList merger;
+       private String mergeRemoteState;
+       private String mergeSourceFile;
+       private File mergeTargetFile;
 
        public FileList(List<String> files) {
                setFiles(files);
@@ -33,7 +42,7 @@ public class FileList extends MainContentList {
        public void setFiles(List<String> files) {
                clearItems();
                this.files = files;
-               cards = new ArrayList<Card>();
+               cards = new ArrayList<CardResult>();
 
                for (String file : files) {
                        addItem(file); // TODO
@@ -62,8 +71,12 @@ public class FileList extends MainContentList {
                List<TextPart> parts = new LinkedList<TextPart>();
 
                String count = "";
-               if (cards.get(index) != null)
-                       count += cards.get(index).size();
+               if (cards.get(index) != null) {
+                       try {
+                               count += cards.get(index).getCard().size();
+                       } catch (IOException e) {
+                       }
+               }
 
                String name = files.get(index).replaceAll("\\\\", "/");
                int indexSl = name.lastIndexOf('/');
@@ -92,32 +105,147 @@ public class FileList extends MainContentList {
                // TODO del, save...
                actions.add(new KeyAction(Mode.CONTACT_LIST, KeyType.Enter,
                                Trans.StringId.KEY_ACTION_VIEW_CARD) {
+                       private Object obj = null;
+
                        @Override
                        public Object getObject() {
-                               int index = getSelectedIndex();
-
-                               if (index < 0 || index >= cards.size())
-                                       return null;
+                               if (obj == null) {
+                                       int index = getSelectedIndex();
+                                       if (index < 0 || index >= cards.size())
+                                               return null;
+
+                                       try {
+                                               if (cards.get(index) != null) {
+                                                       obj = cards.get(index).getCard();
+                                               } else {
+                                                       String file = files.get(index);
+
+                                                       CardResult card = null;
+                                                       final Card arr[] = new Card[4];
+                                                       try {
+                                                               card = Main.getCard(file, new MergeCallback() {
+                                                                       @Override
+                                                                       public Card merge(Card previous,
+                                                                                       Card local, Card server,
+                                                                                       Card autoMerged) {
+                                                                               arr[0] = previous;
+                                                                               arr[1] = local;
+                                                                               arr[2] = server;
+                                                                               arr[3] = autoMerged;
+
+                                                                               return null;
+                                                                       }
+                                                               });
+
+                                                               obj = card.getCard(); // throw IOE if problem
+                                                       } catch (IOException e) {
+                                                               if (arr[0] == null)
+                                                                       throw e;
+
+                                                               // merge management: set all merge vars in
+                                                               // merger,
+                                                               // make sure it has cards but mergeTargetFile
+                                                               // does not exist
+                                                               // (create then delete if needed)
+                                                               // TODO: i18n
+                                                               setMessage(
+                                                                               "Merge error, please check/fix the merged contact",
+                                                                               true);
+
+                                                               // TODO: i18n + filename with numbers in it to
+                                                               // fix
+                                                               File a = File.createTempFile("Merge result ",
+                                                                               ".vcf");
+                                                               File p = File.createTempFile(
+                                                                               "Previous common version ", ".vcf");
+                                                               File l = File.createTempFile("Local ", ".vcf");
+                                                               File s = File.createTempFile("Remote ", ".vcf");
+                                                               arr[3].saveAs(a, Format.VCard21);
+                                                               arr[0].saveAs(p, Format.VCard21);
+                                                               arr[1].saveAs(l, Format.VCard21);
+                                                               arr[2].saveAs(s, Format.VCard21);
+                                                               List<String> mfiles = new LinkedList<String>();
+                                                               mfiles.add(a.getAbsolutePath());
+                                                               mfiles.add(p.getAbsolutePath());
+                                                               mfiles.add(l.getAbsolutePath());
+                                                               mfiles.add(s.getAbsolutePath());
+                                                               merger = new FileList(mfiles);
+                                                               merger.mergeRemoteState = arr[2]
+                                                                               .getContentState(false);
+                                                               merger.mergeSourceFile = files.get(index);
+                                                               merger.mergeTargetFile = a;
+
+                                                               obj = merger;
+                                                               return obj;
+                                                       }
+
+                                                       cards.set(index, card);
+
+                                                       invalidate();
+
+                                                       if (card.isSynchronised()) {
+                                                               // TODO i18n
+                                                               if (card.isChanged())
+                                                                       setMessage(
+                                                                                       "card synchronised: changes from server",
+                                                                                       false);
+                                                               else
+                                                                       setMessage("card synchronised: no changes",
+                                                                                       false);
+                                                       }
+                                               }
+                                       } catch (IOException ioe) {
+                                               ioe.printStackTrace();
+                                               // TODO
+                                               setMessage("ERROR!", true);
+                                       }
+                               }
 
-                               if (cards.get(index) != null)
-                                       return cards.get(index);
+                               return obj;
+                       }
 
-                               String file = files.get(index);
+               });
 
-                               try {
-                                       Card card = Main.getCard(file);
-                                       cards.set(index, card);
+               return actions;
+       }
 
-                                       invalidate();
+       @Override
+       public String wakeup() throws IOException {
+               if (merger != null) {
+                       if (!merger.mergeTargetFile.exists()) {
+                               throw new IOException("Merge cancelled");
+                       }
 
-                                       return card;
-                               } catch (IOException ioe) {
-                                       ioe.printStackTrace();
-                                       return null;
-                               }
+                       // merge back to server if needed and not changed:
+                       try {
+                               Main.getCard(merger.mergeSourceFile, new MergeCallback() {
+                                       @Override
+                                       public Card merge(Card previous, Card local, Card server,
+                                                       Card autoMerged) {
+                                               try {
+                                                       if (server.getContentState(false).equals(
+                                                                       merger.mergeRemoteState)) {
+                                                               return new Card(merger.mergeTargetFile,
+                                                                               Format.VCard21);
+                                                       }
+                                               } catch (IOException e) {
+                                                       e.printStackTrace();
+                                               }
+
+                                               return null;
+                                       }
+                               }).getCard();
+                       } catch (Exception e) {
+                               e.printStackTrace();
+                               throw new IOException("Server changed since merge, cancel", e);
                        }
-               });
 
-               return actions;
+                       merger = null;
+
+                       // TODO i18n
+                       return "merged.";
+               }
+
+               return null;
        }
 }
index 68230d490eeb98e3bb612f323fd91533f73caeef..26cd7cb8ae330797e8394f27eb831762480241ac 100644 (file)
@@ -1,5 +1,6 @@
 package be.nikiroo.jvcard.tui.panes;
 
+import java.io.IOException;
 import java.util.List;
 
 import be.nikiroo.jvcard.tui.KeyAction;
@@ -95,4 +96,18 @@ abstract public class MainContent extends Panel {
        public void refreshData() {
                invalidate();
        }
+
+       /**
+        * Wake up call when the content is popped-back into view. You should call
+        * this method when you exit a previous content and come back to this one.
+        * 
+        * @return a message to display, or NULL
+        * 
+        * @throws IOException
+        *             in case of error (the message of the {@link IOException} will
+        *             be displayed to the user)
+        */
+       public String wakeup() throws IOException {
+               return null;
+       }
 }