X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fbe%2Fnikiroo%2Fjvcard%2Fremote%2FSync.java;h=609041eaaceef39e449df33a7d85781b99f6c2ab;hb=e119a1c1a924998b9315e46c96b1c750aab1deb9;hp=29344d1e33db3ad6a01341ba56d212347280c930;hpb=845fb1d7c3adc0d6cdf9465c0e983ba447cfab6d;p=jvcard.git diff --git a/src/be/nikiroo/jvcard/remote/Sync.java b/src/be/nikiroo/jvcard/remote/Sync.java index 29344d1..609041e 100644 --- a/src/be/nikiroo/jvcard/remote/Sync.java +++ b/src/be/nikiroo/jvcard/remote/Sync.java @@ -16,16 +16,17 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.MissingResourceException; -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; import be.nikiroo.jvcard.resources.StringUtils; +import be.nikiroo.jvcard.resources.bundles.RemoteBundle; +import be.nikiroo.jvcard.resources.enums.RemotingOption; /** * This class will synchronise {@link Card}s between a local instance an a @@ -36,7 +37,7 @@ import be.nikiroo.jvcard.resources.StringUtils; */ public class Sync { /** The time in ms after which we declare that 2 timestamps are different */ - static private final int GRACE_TIME = 2000; + static private final int GRACE_TIME = 2001; /** Directory where to store local cache of remote {@link Card}s. */ static private File cacheDir; @@ -114,7 +115,7 @@ public class Sync { } /** - * Check if the synchronisation is available for this resource. + * Check if the remote server already know about this resource. * * @return TRUE if it is possible to contact the remote server and that this * server has the resource available @@ -141,25 +142,49 @@ public class Sync { return false; } - // return: synced or not - // TODO jDoc - public Card sync(boolean force) throws UnknownHostException, IOException { - + /** + * 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: + *

+ *

+ * + * @param force + * force the synchronisation to occur + * @param callback + * the {@link MergeCallback} to call in case of conflict + * + * @return the synchronised (or not) {@link Card} + * + * @throws UnknownHostException + * in case of server name resolution failure + * @throws IOException + * in case of IO error + */ + public CardResult sync(boolean force, MergeCallback callback) + throws UnknownHostException, IOException { long tsOriginal = getLastModified(); Card local = new Card(getCache(cacheDir), Format.VCard21); - 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); @@ -176,7 +201,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))) { @@ -185,7 +210,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) { @@ -207,11 +232,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; } @@ -235,149 +259,254 @@ 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) { - case GET_CARD: + case GET_CARD: { s.sendCommand(Command.GET_CARD); List 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(); 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); + String serverLastModifTime = updateFromServer(s, local); - remote.put(uid, hash); - } + local.save(); + local.saveAs(getCache(cacheDirOrig), Format.VCard21); - 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); - } + setLastModified(serverLastModifTime); + break; + } + case HELP: { + // note: we are holding the server here, so it could throw + // us away if we take too long + + // TODO: check if those files are deleted + File mergeF = File.createTempFile("contact-merge", ".vcf"); + File serverF = File + .createTempFile("contact-server", ".vcf"); + original.saveAs(serverF, Format.VCard21); + + Card server = new Card(serverF, Format.VCard21); + updateFromServer(s, server); + + // Do an auto sync + server.saveAs(mergeF, Format.VCard21); + Card merge = new Card(mergeF, Format.VCard21); + List added = new LinkedList(); + List removed = new LinkedList(); + 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"); } - for (String uid : remote.keySet()) { - if (local.getById(uid) == null) - added.add(uid); + merge = callback.merge(original, local, server, merge); + if (merge == null) { + throw new IOException( + "Conflicting changes detected and merge operation cancelled"); } - // process: + // 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(); + // - for (Contact c : deleted) { - c.delete(); - } + merge.saveAs(getCache(cacheDir), Format.VCard21); + merge.saveAs(getCache(cacheDirOrig), Format.VCard21); - for (String uid : added) { - s.sendCommand(Command.GET_CONTACT, uid); - for (Contact cc : Vcard21Parser.parseContact(s - .receiveBlock())) { - local.add(cc); - } - } + setLastModified(serverLastModifTime); - for (Contact c : changed) { - c.delete(); - s.sendCommand(Command.GET_CONTACT, c.getId()); - for (Contact cc : Vcard21Parser.parseContact(s - .receiveBlock())) { - local.add(cc); - } - } + local = merge; - local.save(); - local.saveAs(getCache(cacheDirOrig), Format.VCard21); - s.sendCommand(Command.PUT_CARD); - setLastModified(s.receiveLine()); break; } default: - // TODO - throw new IOException(action - + " operation not supported yet :("); + // 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); + } + + /** + * 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, f.getId()); + for (Data d : subremoved) { + s.sendCommand(Command.DELETE_DATA, d.getContentState(true)); + } + for (Data d : subadded) { + s.sendCommand(Command.POST_DATA, d.getContentState(true)); + s.sendBlock(Vcard21Parser.toStrings(d)); + } + s.sendCommand(Command.PUT_CONTACT); + } + } + + 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(true))) { + 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(); } /** @@ -458,10 +587,10 @@ public class Sync { */ static private void config() { String dir = null; - ResourceBundle bundle = Bundles.getBundle("remote"); + RemoteBundle bundle = new RemoteBundle(); try { - dir = bundle.getString("CLIENT_CACHE_DIR").trim(); + dir = bundle.getString(RemotingOption.CLIENT_CACHE_DIR); cacheDir = new File(dir + File.separator + "current"); cacheDir.mkdir(); @@ -475,14 +604,8 @@ public class Sync { + dir); } - String autoStr = bundle.getString("CLIENT_AUTO_SYNC"); - if (autoStr != null && autoStr.trim().equalsIgnoreCase("true")) { - autoSync = true; - } - - } catch (MissingResourceException e) { - throw new InvalidParameterException( - "Cannot access remote.properties configuration file"); + autoSync = bundle + .getBoolean(RemotingOption.CLIENT_AUTO_SYNC, false); } catch (Exception e) { throw new InvalidParameterException( "Cannot open or create cache store at: " + dir);