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: + *

+ *

+ * + * @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