From 5ad0e17e7fea1c602cb2638a006424af9c7e33e8 Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Thu, 24 Mar 2016 05:22:02 +0100 Subject: [PATCH] Remote support ~complete (need more tests at least) --- src/be/nikiroo/jvcard/BaseClass.java | 107 +++++++++-- src/be/nikiroo/jvcard/Card.java | 22 +-- .../nikiroo/jvcard/launcher/CardResult.java | 118 ++++++++++++ src/be/nikiroo/jvcard/launcher/Main.java | 15 +- src/be/nikiroo/jvcard/launcher/Optional.java | 19 +- .../nikiroo/jvcard/parsers/Vcard21Parser.java | 25 +++ src/be/nikiroo/jvcard/remote/Server.java | 13 +- src/be/nikiroo/jvcard/remote/Sync.java | 83 ++++++--- src/be/nikiroo/jvcard/tui/KeyAction.java | 101 ++++++++-- src/be/nikiroo/jvcard/tui/MainWindow.java | 21 +++ src/be/nikiroo/jvcard/tui/panes/FileList.java | 172 +++++++++++++++--- .../nikiroo/jvcard/tui/panes/MainContent.java | 15 ++ 12 files changed, 593 insertions(+), 118 deletions(-) create mode 100644 src/be/nikiroo/jvcard/launcher/CardResult.java diff --git a/src/be/nikiroo/jvcard/BaseClass.java b/src/be/nikiroo/jvcard/BaseClass.java index df0fcf0..1dd7644 100644 --- a/src/be/nikiroo/jvcard/BaseClass.java +++ b/src/be/nikiroo/jvcard/BaseClass.java @@ -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. + *

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

* + *

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

* * @author niki * @@ -119,8 +125,8 @@ public abstract class BaseClass> implements List { * If not equals, the differences will be represented by the given * {@link List}s if they are not NULL. *
    - *
  • addedwill represent the elements in list but not in - * this
  • + *
  • added will represent the elements in list but not + * in this
  • *
  • removed will represent the elements in this but not * in list
  • *
  • from will represent the elements in list that are @@ -250,22 +256,25 @@ public abstract class BaseClass> implements List { } /** - * 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. * *

    * Not that this state is lossy. 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. *

    * + * @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> implements List { 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> implements List { 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> implements List { * 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> implements List { * 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(); diff --git a/src/be/nikiroo/jvcard/Card.java b/src/be/nikiroo/jvcard/Card.java index d2567a4..5475358 100644 --- a/src/be/nikiroo/jvcard/Card.java +++ b/src/be/nikiroo/jvcard/Card.java @@ -22,7 +22,6 @@ public class Card extends BaseClass { 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 { * @throws InvalidParameterException * if format is NULL */ - public Card(List contacts) throws IOException { + public Card(List contacts) { super(contacts); lastModified = -1; @@ -197,25 +196,6 @@ public class Card extends BaseClass { 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 index 0000000..68a5ae7 --- /dev/null +++ b/src/be/nikiroo/jvcard/launcher/CardResult.java @@ -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; + } +} diff --git a/src/be/nikiroo/jvcard/launcher/Main.java b/src/be/nikiroo/jvcard/launcher/Main.java index 997cdec..06c55c6 100644 --- a/src/be/nikiroo/jvcard/launcher/Main.java +++ b/src/be/nikiroo/jvcard/launcher/Main.java @@ -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.: * jvcard://localhost:4444/coworkers.vcf) + * @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; diff --git a/src/be/nikiroo/jvcard/launcher/Optional.java b/src/be/nikiroo/jvcard/launcher/Optional.java index b737c01..9eb0d6b 100644 --- a/src/be/nikiroo/jvcard/launcher/Optional.java +++ b/src/be/nikiroo/jvcard/launcher/Optional.java @@ -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.: * jvcard://localhost:4444/coworkers) + * @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; } diff --git a/src/be/nikiroo/jvcard/parsers/Vcard21Parser.java b/src/be/nikiroo/jvcard/parsers/Vcard21Parser.java index 7d0f5e8..61f9016 100644 --- a/src/be/nikiroo/jvcard/parsers/Vcard21Parser.java +++ b/src/be/nikiroo/jvcard/parsers/Vcard21Parser.java @@ -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. * diff --git a/src/be/nikiroo/jvcard/remote/Server.java b/src/be/nikiroo/jvcard/remote/Server.java index 08091f6..60f323a 100644 --- a/src/be/nikiroo/jvcard/remote/Server.java +++ b/src/be/nikiroo/jvcard/remote/Server.java @@ -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(); diff --git a/src/be/nikiroo/jvcard/remote/Sync.java b/src/be/nikiroo/jvcard/remote/Sync.java index 771798c..9963665 100644 --- a/src/be/nikiroo/jvcard/remote/Sync.java +++ b/src/be/nikiroo/jvcard/remote/Sync.java @@ -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 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 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"); + } + + 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 subadded = new LinkedList(); List subremoved = new LinkedList(); 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); } } diff --git a/src/be/nikiroo/jvcard/tui/KeyAction.java b/src/be/nikiroo/jvcard/tui/KeyAction.java index f39b103..2c10e62 100644 --- a/src/be/nikiroo/jvcard/tui/KeyAction.java +++ b/src/be/nikiroo/jvcard/tui/KeyAction.java @@ -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. + * + *

    + * + * You are expected to override this method to return your object, the 3 + * afore-mentioned methods will use this one as the source. + * + *

    + * + * DO NOT process data here, this method will be called often; this + * should only be a getter method. + * + * @return the associated object + */ public Object getObject() { return null; } diff --git a/src/be/nikiroo/jvcard/tui/MainWindow.java b/src/be/nikiroo/jvcard/tui/MainWindow.java index 2362899..d7cb80d 100644 --- a/src/be/nikiroo/jvcard/tui/MainWindow.java +++ b/src/be/nikiroo/jvcard/tui/MainWindow.java @@ -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: diff --git a/src/be/nikiroo/jvcard/tui/panes/FileList.java b/src/be/nikiroo/jvcard/tui/panes/FileList.java index e7632e0..7065aaf 100644 --- a/src/be/nikiroo/jvcard/tui/panes/FileList.java +++ b/src/be/nikiroo/jvcard/tui/panes/FileList.java @@ -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 files; - private List cards; + private List cards; + + private FileList merger; + private String mergeRemoteState; + private String mergeSourceFile; + private File mergeTargetFile; public FileList(List files) { setFiles(files); @@ -33,7 +42,7 @@ public class FileList extends MainContentList { public void setFiles(List files) { clearItems(); this.files = files; - cards = new ArrayList(); + cards = new ArrayList(); for (String file : files) { addItem(file); // TODO @@ -62,8 +71,12 @@ public class FileList extends MainContentList { List parts = new LinkedList(); 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 mfiles = new LinkedList(); + 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; } } diff --git a/src/be/nikiroo/jvcard/tui/panes/MainContent.java b/src/be/nikiroo/jvcard/tui/panes/MainContent.java index 68230d4..26cd7cb 100644 --- a/src/be/nikiroo/jvcard/tui/panes/MainContent.java +++ b/src/be/nikiroo/jvcard/tui/panes/MainContent.java @@ -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; + } } -- 2.27.0