From 02b341aa6dcd14dc311f6ea218e199f20e242f36 Mon Sep 17 00:00:00 2001
From: Niki Roo
Date: Sat, 19 Mar 2016 11:01:06 +0100
Subject: [PATCH] Remoting support: update: Currently implemented: - sync local
changes to server - sync server changes back locally - get new card from
server - post new card to server
Not fully implemented yet:
- merge local/remote changes
---
src/be/nikiroo/jvcard/launcher/Main.java | 123 +-------
src/be/nikiroo/jvcard/launcher/Optional.java | 140 +++++++++
src/be/nikiroo/jvcard/remote/Sync.java | 311 ++++++++++++-------
3 files changed, 349 insertions(+), 225 deletions(-)
create mode 100644 src/be/nikiroo/jvcard/launcher/Optional.java
diff --git a/src/be/nikiroo/jvcard/launcher/Main.java b/src/be/nikiroo/jvcard/launcher/Main.java
index 925ade4..997cdec 100644
--- a/src/be/nikiroo/jvcard/launcher/Main.java
+++ b/src/be/nikiroo/jvcard/launcher/Main.java
@@ -3,8 +3,6 @@ package be.nikiroo.jvcard.launcher;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.LinkedList;
@@ -205,7 +203,7 @@ public class Main {
if (port != null) {
try {
- runServer(port);
+ Optional.runServer(port);
} catch (Exception e) {
if (e instanceof IOException) {
System.err
@@ -225,7 +223,7 @@ public class Main {
}
} else {
try {
- startTui(textMode, files);
+ Optional.startTui(textMode, files);
} catch (Exception e) {
if (e instanceof IOException) {
System.err
@@ -270,7 +268,7 @@ public class Main {
Card card = null;
try {
if (remote) {
- card = syncCard(input);
+ card = Optional.syncCard(input);
} else {
card = new Card(new File(input), format);
}
@@ -283,121 +281,6 @@ public class Main {
return card;
}
- /**
- * Create a new jVCard server on the given port, then run it.
- *
- * @param port
- * the port to run on
- *
- * @throws SecurityException
- * in case of internal error
- * @throws NoSuchMethodException
- * in case of internal error
- * @throws ClassNotFoundException
- * in case of internal error
- * @throws IllegalAccessException
- * in case of internal error
- * @throws InstantiationException
- * in case of internal error
- * @throws InvocationTargetException
- * in case of internal error
- * @throws IllegalArgumentException
- * in case of internal error
- * @throws IOException
- * in case of IO error
- */
- @SuppressWarnings("unchecked")
- static private void runServer(int port) throws NoSuchMethodException,
- SecurityException, ClassNotFoundException, InstantiationException,
- IllegalAccessException, IllegalArgumentException,
- InvocationTargetException {
- @SuppressWarnings("rawtypes")
- Class serverClass = Class.forName("be.nikiroo.jvcard.remote.Server");
- Method run = serverClass.getDeclaredMethod("run", new Class[] {});
- run.invoke(serverClass.getConstructor(int.class).newInstance(port));
- }
-
- /**
- * Start the TUI program.
- *
- * @param textMode
- * TRUE to force text mode, FALSE to force the Swing terminal
- * emulator, null to automatically determine the best choice
- * @param files
- * the files to show at startup
- *
- * @throws SecurityException
- * in case of internal error
- * @throws NoSuchMethodException
- * in case of internal error
- * @throws ClassNotFoundException
- * in case of internal error
- * @throws IllegalAccessException
- * in case of internal error
- * @throws InstantiationException
- * in case of internal error
- * @throws InvocationTargetException
- * in case of internal error
- * @throws IllegalArgumentException
- * in case of internal error
- * @throws IOException
- * in case of IO error
- */
- @SuppressWarnings("unchecked")
- static private void startTui(Boolean textMode, List files)
- throws NoSuchMethodException, SecurityException,
- ClassNotFoundException, InstantiationException,
- IllegalAccessException, IllegalArgumentException,
- InvocationTargetException {
- @SuppressWarnings("rawtypes")
- Class launcherClass = Class
- .forName("be.nikiroo.jvcard.tui.TuiLauncher");
- Method start = launcherClass.getDeclaredMethod("start", new Class[] {
- Boolean.class, List.class });
- start.invoke(launcherClass.newInstance(), textMode, files);
- }
-
- /**
- * Return the {@link Card} corresponding to the given URL, synchronised if
- * necessary.
- *
- * @param input
- * the jvcard:// with resource name URL (e.g.:
- * jvcard://localhost:4444/coworkers)
- *
- * @throws SecurityException
- * in case of internal error
- * @throws NoSuchMethodException
- * in case of internal error
- * @throws ClassNotFoundException
- * in case of internal error
- * @throws IllegalAccessException
- * in case of internal error
- * @throws InstantiationException
- * in case of internal error
- * @throws InvocationTargetException
- * in case of internal error
- * @throws IllegalArgumentException
- * in case of internal error
- * @throws IOException
- * in case of IO error
- */
- @SuppressWarnings("unchecked")
- static private Card syncCard(String input) 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 });
-
- Object o = syncClass.getConstructor(String.class).newInstance(input);
- Card card = (Card) sync.invoke(o, false);
-
- return card;
- }
-
/**
* Open the given path and add all its files if it is a directory or just
* this one if not to the returned list.
diff --git a/src/be/nikiroo/jvcard/launcher/Optional.java b/src/be/nikiroo/jvcard/launcher/Optional.java
new file mode 100644
index 0000000..b737c01
--- /dev/null
+++ b/src/be/nikiroo/jvcard/launcher/Optional.java
@@ -0,0 +1,140 @@
+package be.nikiroo.jvcard.launcher;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+
+import be.nikiroo.jvcard.Card;
+
+/**
+ * This class let you call "optional" methods, that is, methods and classes that
+ * may or may not be present.
+ *
+ *
+ * It currently offers services for:
+ *
+ * - remoting support
+ * - TUI support
+ *
+ *
+ *
+ * @author niki
+ *
+ */
+class Optional {
+ /**
+ * Create a new jVCard server on the given port, then run it.
+ *
+ * @param port
+ * the port to run on
+ *
+ * @throws SecurityException
+ * in case of internal error
+ * @throws NoSuchMethodException
+ * in case of internal error
+ * @throws ClassNotFoundException
+ * in case of internal error
+ * @throws IllegalAccessException
+ * in case of internal error
+ * @throws InstantiationException
+ * in case of internal error
+ * @throws InvocationTargetException
+ * in case of internal error
+ * @throws IllegalArgumentException
+ * in case of internal error
+ * @throws IOException
+ * in case of IO error
+ */
+ @SuppressWarnings("unchecked")
+ static public void runServer(int port) throws NoSuchMethodException,
+ SecurityException, ClassNotFoundException, InstantiationException,
+ IllegalAccessException, IllegalArgumentException,
+ InvocationTargetException {
+ @SuppressWarnings("rawtypes")
+ Class serverClass = Class.forName("be.nikiroo.jvcard.remote.Server");
+ Method run = serverClass.getDeclaredMethod("run", new Class[] {});
+ run.invoke(serverClass.getConstructor(int.class).newInstance(port));
+ }
+
+ /**
+ * Start the TUI program.
+ *
+ * @param textMode
+ * TRUE to force text mode, FALSE to force the Swing terminal
+ * emulator, null to automatically determine the best choice
+ * @param files
+ * the files to show at startup
+ *
+ * @throws SecurityException
+ * in case of internal error
+ * @throws NoSuchMethodException
+ * in case of internal error
+ * @throws ClassNotFoundException
+ * in case of internal error
+ * @throws IllegalAccessException
+ * in case of internal error
+ * @throws InstantiationException
+ * in case of internal error
+ * @throws InvocationTargetException
+ * in case of internal error
+ * @throws IllegalArgumentException
+ * in case of internal error
+ * @throws IOException
+ * in case of IO error
+ */
+ @SuppressWarnings("unchecked")
+ static public void startTui(Boolean textMode, List files)
+ throws NoSuchMethodException, SecurityException,
+ ClassNotFoundException, InstantiationException,
+ IllegalAccessException, IllegalArgumentException,
+ InvocationTargetException {
+ @SuppressWarnings("rawtypes")
+ Class launcherClass = Class
+ .forName("be.nikiroo.jvcard.tui.TuiLauncher");
+ Method start = launcherClass.getDeclaredMethod("start", new Class[] {
+ Boolean.class, List.class });
+ start.invoke(launcherClass.newInstance(), textMode, files);
+ }
+
+ /**
+ * Return the {@link Card} corresponding to the given URL, synchronised if
+ * necessary.
+ *
+ * @param input
+ * the jvcard:// with resource name URL (e.g.:
+ * jvcard://localhost:4444/coworkers)
+ *
+ * @throws SecurityException
+ * in case of internal error
+ * @throws NoSuchMethodException
+ * in case of internal error
+ * @throws ClassNotFoundException
+ * in case of internal error
+ * @throws IllegalAccessException
+ * in case of internal error
+ * @throws InstantiationException
+ * in case of internal error
+ * @throws InvocationTargetException
+ * in case of internal error
+ * @throws IllegalArgumentException
+ * in case of internal error
+ * @throws IOException
+ * in case of IO error
+ */
+ @SuppressWarnings("unchecked")
+ static public Card syncCard(String input) 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 });
+
+ Object o = syncClass.getConstructor(String.class).newInstance(input);
+ Card card = (Card) sync.invoke(o, false);
+
+ return card;
+ }
+}
diff --git a/src/be/nikiroo/jvcard/remote/Sync.java b/src/be/nikiroo/jvcard/remote/Sync.java
index 29344d1..771798c 100644
--- a/src/be/nikiroo/jvcard/remote/Sync.java
+++ b/src/be/nikiroo/jvcard/remote/Sync.java
@@ -36,7 +36,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;
@@ -114,7 +114,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
@@ -141,8 +141,30 @@ public class Sync {
return false;
}
- // return: synced or not
- // TODO jDoc
+ /**
+ * Synchronise the current resource if needed, then return the locally
+ * cached version of said resource.
+ *
+ *
+ * A synchronisation is deemed necessary if one of the following is true:
+ *
+ * - force is TRUE
+ * - CLIENT_AUTO_SYNC is TRUE in the configuration file
+ * - the {@link Card} exists locally but not on the remote server
+ * - the {@link Card} exists on the remote server but not locally
+ *
+ *
+ *
+ * @param force
+ * force the synchronisation to occur
+ *
+ * @return the synchronised (or not) {@link Card}
+ *
+ * @throws UnknownHostException
+ * in case of server name resolution failure
+ * @throws IOException
+ * in case of IO error
+ */
public Card sync(boolean force) throws UnknownHostException, IOException {
long tsOriginal = getLastModified();
@@ -176,7 +198,7 @@ public class Sync {
}
// Error cases:
- // - file not preset neither in cache nor on server
+ // - file not present neither in cache nor on server
// - remote < previous
if ((tsServer == -1 && tsOriginal == -1)
|| (tsServer != -1 && tsOriginal != -1 && ((tsOriginal - tsServer) > GRACE_TIME))) {
@@ -207,11 +229,10 @@ public class Sync {
// Sync from/to server if
if (serverChanges && localChanges) {
- // TODO
action = Command.HELP;
}
- // PUT the whole file if:
+ // POST the whole file if:
if (tsServer == -1) {
action = Command.POST_CARD;
}
@@ -239,7 +260,7 @@ public class Sync {
}
switch (action) {
- case GET_CARD:
+ case GET_CARD: {
s.sendCommand(Command.GET_CARD);
List data = s.receiveBlock();
setLastModified(data.remove(0));
@@ -250,120 +271,66 @@ public class Sync {
local.save();
local.saveAs(getCache(cacheDirOrig), Format.VCard21);
break;
- case POST_CARD:
+ }
+ 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: {
- List added = new LinkedList();
- List removed = new LinkedList();
- List from = new LinkedList();
- List to = new LinkedList();
- 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 subadded = new LinkedList();
- List subremoved = new LinkedList();
- f.compare(t, subadded, subremoved, subremoved,
- subadded);
- s.sendCommand(Command.PUT_CONTACT, name);
- for (Data d : subremoved) {
- s.sendCommand(Command.DELETE_DATA,
- d.getContentState());
- }
- for (Data d : subadded) {
- s.sendCommand(Command.POST_DATA,
- d.getContentState());
- s.sendBlock(Vcard21Parser.toStrings(d));
- }
- }
- }
+ String serverLastModifTime = updateToServer(s, original,
+ local);
local.saveAs(getCache(cacheDirOrig), Format.VCard21);
- s.sendCommand(Command.PUT_CARD);
- setLastModified(s.receiveLine());
+ setLastModified(serverLastModifTime);
break;
}
case HASH_CONTACT: {
- s.sendCommand(Command.PUT_CARD);
-
- s.sendCommand(Command.LIST_CONTACT);
- Map remote = new HashMap();
- 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);
- }
-
- List deleted = new LinkedList();
- List changed = new LinkedList();
- List added = new LinkedList();
-
- for (Contact c : local) {
- String hash = remote.get(c.getId());
- if (hash == null) {
- deleted.add(c);
- } else if (!hash.equals(c.getContentState())) {
- changed.add(c);
- }
- }
-
- for (String uid : remote.keySet()) {
- if (local.getById(uid) == null)
- added.add(uid);
- }
-
- // process:
-
- for (Contact c : deleted) {
- c.delete();
- }
-
- for (String uid : added) {
- s.sendCommand(Command.GET_CONTACT, uid);
- for (Contact cc : Vcard21Parser.parseContact(s
- .receiveBlock())) {
- local.add(cc);
- }
- }
-
- for (Contact c : changed) {
- c.delete();
- s.sendCommand(Command.GET_CONTACT, c.getId());
- for (Contact cc : Vcard21Parser.parseContact(s
- .receiveBlock())) {
- local.add(cc);
- }
- }
+ String serverLastModifTime = updateFromServer(s, local);
local.save();
local.saveAs(getCache(cacheDirOrig), Format.VCard21);
- s.sendCommand(Command.PUT_CARD);
- setLastModified(s.receiveLine());
+
+ setLastModified(serverLastModifTime);
+ 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
+
+ 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);
+
+ // TODO: auto merge into mergeF (from original, local,
+ // server)
+ local.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);
+
+ merge.saveAs(getCache(cacheDir), Format.VCard21);
+ merge.saveAs(getCache(cacheDirOrig), Format.VCard21);
+
+ setLastModified(serverLastModifTime);
+
+ local = merge;
+
break;
}
- default:
- // TODO
- throw new IOException(action
- + " operation not supported yet :(");
}
s.sendCommand(Command.SELECT);
@@ -380,6 +347,140 @@ public class Sync {
return local;
}
+ /**
+ * Will update the currently selected {@link Card} on the remote server to
+ * be in the same state as local, assuming the server is currently
+ * in original state.
+ *
+ * @param s
+ * the {@link SimpleSocket} to work on, which MUST 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 added = new LinkedList();
+ List removed = new LinkedList();
+ List from = new LinkedList();
+ List to = new LinkedList();
+ 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 subadded = new LinkedList();
+ List subremoved = new LinkedList();
+ f.compare(t, subadded, subremoved, subremoved, subadded);
+ s.sendCommand(Command.PUT_CONTACT, name);
+ for (Data d : subremoved) {
+ s.sendCommand(Command.DELETE_DATA, d.getContentState());
+ }
+ for (Data d : subadded) {
+ s.sendCommand(Command.POST_DATA, d.getContentState());
+ s.sendBlock(Vcard21Parser.toStrings(d));
+ }
+ }
+ }
+
+ s.sendCommand(Command.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 MUST 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 remote = new HashMap();
+ 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);
+ }
+
+ List deleted = new LinkedList();
+ List changed = new LinkedList();
+ List added = new LinkedList();
+
+ for (Contact c : local) {
+ String hash = remote.get(c.getId());
+ if (hash == null) {
+ deleted.add(c);
+ } else if (!hash.equals(c.getContentState())) {
+ changed.add(c);
+ }
+ }
+
+ for (String uid : remote.keySet()) {
+ if (local.getById(uid) == null)
+ added.add(uid);
+ }
+
+ // process:
+
+ for (Contact c : deleted) {
+ c.delete();
+ }
+
+ for (String uid : added) {
+ s.sendCommand(Command.GET_CONTACT, uid);
+ for (Contact cc : Vcard21Parser.parseContact(s.receiveBlock())) {
+ local.add(cc);
+ }
+ }
+
+ for (Contact c : changed) {
+ c.delete();
+ s.sendCommand(Command.GET_CONTACT, c.getId());
+ for (Contact cc : Vcard21Parser.parseContact(s.receiveBlock())) {
+ local.add(cc);
+ }
+ }
+
+ s.sendCommand(Command.PUT_CARD);
+
+ return s.receiveLine();
+ }
+
/**
* Return the requested cache for the current resource.
*
--
2.27.0