package be.nikiroo.jvcard;
+import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
* 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
*
* 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
}
/**
- * 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());
}
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).
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("]");
}
}
* 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();
}
* 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();
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}.
* @throws InvalidParameterException
* if format is NULL
*/
- public Card(List<Contact> contacts) throws IOException {
+ public Card(List<Contact> contacts) {
super(contacts);
lastModified = -1;
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);
--- /dev/null
+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;
+ }
+}
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;
* @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;
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;
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
* @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
* 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;
}
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.
*
if (contact == null) {
s.sendBlock();
} else {
- s.sendLine(contact.getContentState());
+ s.sendLine(contact.getContentState(true));
}
break;
}
|| (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();
String cstate = cmd.getParam();
Data data = null;
for (Data d : contact) {
- if (cstate.equals(d.getContentState()))
+ if (cstate.equals(d.getContentState(true)))
data = d;
}
String cstate = cmd.getParam();
Data data = null;
for (Data d : contact) {
- if (cstate.equals(d.getContentState()))
+ if (cstate.equals(d.getContentState(true)))
data = d;
}
case HASH_DATA: {
for (Data data : contact) {
if (data.getId().equals(cmd.getParam())) {
- s.send(data.getContentState());
+ s.send(data.getContentState(true));
}
}
s.sendBlock();
|| 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();
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;
*
* @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 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);
}
// Check changes
- boolean serverChanges = (tsServer - tsOriginal) > GRACE_TIME;
+ serverChanges = (tsServer - tsOriginal) > GRACE_TIME;
boolean localChanges = false;
Card original = null;
if (tsOriginal != -1) {
System.err.println("DEBUG: it changed. retry.");
s.sendCommand(Command.SELECT);
s.close();
- return sync(force);
+ return sync(force, callback);
}
switch (action) {
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();
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");
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);
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);
}
/**
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);
}
}
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);
}
}
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;
}
/**
- * 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;
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.
*
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)
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)
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)
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;
}
package be.nikiroo.jvcard.tui;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
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;
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);
}
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:
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;
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);
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
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('/');
// 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;
}
}
package be.nikiroo.jvcard.tui.panes;
+import java.io.IOException;
import java.util.List;
import be.nikiroo.jvcard.tui.KeyAction;
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;
+ }
}