Update on remote-server
authorNiki Roo <niki@nikiroo.be>
Mon, 14 Mar 2016 20:40:09 +0000 (21:40 +0100)
committerNiki Roo <niki@nikiroo.be>
Mon, 14 Mar 2016 20:40:09 +0000 (21:40 +0100)
src/be/nikiroo/jvcard/BaseClass.java
src/be/nikiroo/jvcard/Card.java
src/be/nikiroo/jvcard/parsers/AbookParser.java
src/be/nikiroo/jvcard/parsers/Parser.java
src/be/nikiroo/jvcard/parsers/Vcard21Parser.java
src/be/nikiroo/jvcard/remote/Command.java
src/be/nikiroo/jvcard/remote/Server.java
src/be/nikiroo/jvcard/remote/Sync.java

index abaa9ccbd37d04fe7a48fc8a2a57b8c59ebb267a..26863902e59c8ec1447bbc44b9e1006b60843b5e 100644 (file)
@@ -241,20 +241,37 @@ public abstract class BaseClass<E extends BaseClass<?>> implements List<E> {
 
        /**
         * Get the recursive state of the current object, i.e., its children. It
-        * represents the full state information about this object's children. It
-        * does not check the state of the object itself.
+        * represents the full state information about this object's children.
         * 
         * @return a {@link String} representing the current content state of this
-        *         object, i.e., its children
+        *         object, i.e., its children included
         */
        public String getContentState() {
                StringBuilder builder = new StringBuilder();
+               buildContentStateRaw(builder);
+               return StringUtils.getHash(builder.toString());
+       }
 
+       /**
+        * Return the (first) child element with the given ID or NULL if not found.
+        * 
+        * @param id
+        *            the id to look for
+        * 
+        * @return the child element or NULL
+        */
+       public E getById(String id) {
                for (E child : this) {
-                       builder.append(child.getContentState());
+                       if (id == null) {
+                               if (child.getId() == null)
+                                       return child;
+                       } else {
+                               if (id.equals(child.getId()))
+                                       return child;
+                       }
                }
 
-               return StringUtils.getHash(builder.toString());
+               return null;
        }
 
        /**
@@ -275,6 +292,23 @@ public abstract class BaseClass<E extends BaseClass<?>> implements List<E> {
         */
        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.
+        * 
+        * It is not hashed.
+        * 
+        * @param builder
+        *            the {@link StringBuilder} that will represent the current
+        *            content state of this object, i.e., its children included
+        */
+       void buildContentStateRaw(StringBuilder builder) {
+               builder.append(getState());
+               for (E child : this) {
+                       child.buildContentStateRaw(builder);
+               }
+       }
+
        /**
         * Notify that this element has unsaved changes.
         */
index 52eec8c6e4af0d5ac3ae67dbb94f7d63b93f0f37..d550ad4a6dc00df7009c83b580e079eb2ead86a1 100644 (file)
@@ -40,7 +40,7 @@ public class Card extends BaseClass<Contact> {
         *             if format is NULL
         */
        public Card(File file, Format format) throws IOException {
-               this(Parser.parse(file, format));
+               this(Parser.parseContact(file, format));
 
                if (file != null && file.exists()) {
                        lastModified = file.lastModified();
@@ -133,7 +133,7 @@ public class Card extends BaseClass<Contact> {
                if (file == null)
                        return false;
 
-               this.replaceListContent(Parser.parse(file, format));
+               this.replaceListContent(Parser.parseContact(file, format));
                lastModified = file.lastModified();
                setPristine();
 
index 344ae705401652905fe49563fa5fac4110be9f41..cb4f667119be22b0b8cf8713a1b7f439511c30a4 100644 (file)
@@ -9,7 +9,17 @@ import be.nikiroo.jvcard.Contact;
 import be.nikiroo.jvcard.Data;
 
 public class AbookParser {
-       public static List<Contact> parse(List<String> lines) {
+       /**
+        * Load the given data from under the given {@link Format}.
+        * 
+        * @param lines
+        *            the input to load from
+        * @param format
+        *            the {@link Format} to load as
+        * 
+        * @return the list of elements
+        */
+       public static List<Contact> parseContact(List<String> lines) {
                List<Contact> contacts = new LinkedList<Contact>();
 
                for (String line : lines) {
index 1fa7c5749805746d58e41f9fae52e575e2cb529d..ceb35dccca75de055917b4050b5df3f912110fb3 100644 (file)
@@ -28,7 +28,7 @@ public class Parser {
         * @throws IOException
         *             in case of IO error
         */
-       public static List<Contact> parse(File file, Format format)
+       public static List<Contact> parseContact(File file, Format format)
                        throws IOException {
                List<String> lines = null;
 
@@ -46,7 +46,7 @@ public class Parser {
                if (lines == null)
                        return new LinkedList<Contact>();
 
-               return parse(lines, format);
+               return parseContact(lines, format);
        }
 
        /**
@@ -59,12 +59,12 @@ public class Parser {
         * 
         * @return the list of elements
         */
-       public static List<Contact> parse(List<String> lines, Format format) {
+       public static List<Contact> parseContact(List<String> lines, Format format) {
                switch (format) {
                case VCard21:
-                       return Vcard21Parser.parse(lines);
+                       return Vcard21Parser.parseContact(lines);
                case Abook:
-                       return AbookParser.parse(lines);
+                       return AbookParser.parseContact(lines);
 
                default:
                        throw new InvalidParameterException("Unknown format: "
@@ -79,10 +79,6 @@ public class Parser {
         * @param card
         *            the card to convert
         * 
-        * @param startingBKey
-        *            the starting BKey number (all the other will follow) or -1 for
-        *            no BKey
-        * 
         * @param format
         *            the output {@link Format} to use
         * 
index 4f9431733f122cf3f7b819e22e98fdc3c443ca9b..7d0f5e86beb98e17efb7cef1c4520fef3faa798b 100644 (file)
@@ -10,10 +10,20 @@ import be.nikiroo.jvcard.Data;
 import be.nikiroo.jvcard.TypeInfo;
 
 public class Vcard21Parser {
-       public static List<Contact> parse(Iterable<String> textData) {
+       /**
+        * Load the given data from under the given {@link Format}.
+        * 
+        * @param lines
+        *            the input to load from
+        * @param format
+        *            the {@link Format} to load as
+        * 
+        * @return the list of elements
+        */
+       public static List<Contact> parseContact(Iterable<String> textData) {
                Iterator<String> lines = textData.iterator();
                List<Contact> contacts = new LinkedList<Contact>();
-               List<Data> datas = null;
+               List<String> datas = null;
 
                String nextRawLine = null;
                if (lines.hasNext()) {
@@ -43,14 +53,14 @@ public class Vcard21Parser {
 
                        String line = rawLine.toString();
                        if (line.equals("BEGIN:VCARD")) {
-                               datas = new LinkedList<Data>();
+                               datas = new LinkedList<String>();
                        } else if (line.equals("END:VCARD")) {
                                if (datas == null) {
                                        // BAD INPUT FILE. IGNORE.
                                        System.err
                                                        .println("VCARD Parser warning: END:VCARD seen before any VCARD:BEGIN");
                                } else {
-                                       contacts.add(new Contact(datas));
+                                       contacts.add(new Contact(parseData(datas)));
                                }
                        } else {
                                if (datas == null) {
@@ -58,51 +68,69 @@ public class Vcard21Parser {
                                        System.err
                                                        .println("VCARD Parser warning: data seen before any VCARD:BEGIN");
                                } else {
-                                       List<TypeInfo> types = new LinkedList<TypeInfo>();
-                                       String name = "";
-                                       String value = "";
-                                       String group = "";
-
-                                       if (line.contains(":")) {
-                                               int colIndex = line.indexOf(':');
-                                               String rest = line.substring(0, colIndex);
-                                               value = line.substring(colIndex + 1);
-
-                                               if (rest.contains(";")) {
-                                                       String tab[] = rest.split(";");
-                                                       name = tab[0];
-
-                                                       for (int i = 1; i < tab.length; i++) {
-                                                               if (tab[i].contains("=")) {
-                                                                       int equIndex = tab[i].indexOf('=');
-                                                                       String tname = tab[i]
-                                                                                       .substring(0, equIndex);
-                                                                       String tvalue = tab[i]
-                                                                                       .substring(equIndex + 1);
-                                                                       types.add(new TypeInfo(tname, tvalue));
-                                                               } else {
-                                                                       types.add(new TypeInfo(tab[i], ""));
-                                                               }
-                                                       }
+                                       datas.add(line);
+                               }
+                       }
+               }
+
+               return contacts;
+       }
+
+       /**
+        * Load the given data from under the given {@link Format}.
+        * 
+        * @param lines
+        *            the input to load from
+        * @param format
+        *            the {@link Format} to load as
+        * 
+        * @return the list of elements
+        */
+       public static List<Data> parseData(Iterable<String> textData) {
+               List<Data> datas = new LinkedList<Data>();
+
+               for (String line : textData) {
+                       List<TypeInfo> types = new LinkedList<TypeInfo>();
+                       String name = "";
+                       String value = "";
+                       String group = "";
+
+                       if (line.contains(":")) {
+                               int colIndex = line.indexOf(':');
+                               String rest = line.substring(0, colIndex);
+                               value = line.substring(colIndex + 1);
+
+                               if (rest.contains(";")) {
+                                       String tab[] = rest.split(";");
+                                       name = tab[0];
+
+                                       for (int i = 1; i < tab.length; i++) {
+                                               if (tab[i].contains("=")) {
+                                                       int equIndex = tab[i].indexOf('=');
+                                                       String tname = tab[i].substring(0, equIndex);
+                                                       String tvalue = tab[i].substring(equIndex + 1);
+                                                       types.add(new TypeInfo(tname, tvalue));
                                                } else {
-                                                       name = rest;
+                                                       types.add(new TypeInfo(tab[i], ""));
                                                }
-                                       } else {
-                                               name = line;
-                                       }
-
-                                       if (name.contains(".")) {
-                                               int dotIndex = name.indexOf('.');
-                                               group = name.substring(0, dotIndex);
-                                               name = name.substring(dotIndex + 1);
                                        }
-
-                                       datas.add(new Data(types, name, value, group));
+                               } else {
+                                       name = rest;
                                }
+                       } else {
+                               name = line;
+                       }
+
+                       if (name.contains(".")) {
+                               int dotIndex = name.indexOf('.');
+                               group = name.substring(0, dotIndex);
+                               name = name.substring(dotIndex + 1);
                        }
+
+                       datas.add(new Data(types, name, value, group));
                }
 
-               return contacts;
+               return datas;
        }
 
        /**
@@ -112,6 +140,25 @@ public class Vcard21Parser {
         * @param card
         *            the card to convert
         * 
+        * @return the {@link String} representation
+        */
+       public static List<String> toStrings(Card card) {
+               List<String> lines = new LinkedList<String>();
+
+               for (Contact contact : card) {
+                       lines.addAll(toStrings(contact, -1));
+               }
+
+               return lines;
+       }
+
+       /**
+        * Return a {@link String} representation of the given {@link Contact}, line
+        * by line.
+        * 
+        * @param card
+        *            the contact to convert
+        * 
         * @param startingBKey
         *            the starting BKey number (all the other will follow) or -1 for
         *            no BKey
@@ -124,52 +171,7 @@ public class Vcard21Parser {
                lines.add("BEGIN:VCARD");
                lines.add("VERSION:2.1");
                for (Data data : contact) {
-                       StringBuilder dataBuilder = new StringBuilder();
-                       if (data.getGroup() != null && !data.getGroup().trim().equals("")) {
-                               dataBuilder.append(data.getGroup().trim());
-                               dataBuilder.append('.');
-                       }
-                       dataBuilder.append(data.getName());
-                       for (TypeInfo type : data) {
-                               dataBuilder.append(';');
-                               dataBuilder.append(type.getName());
-                               if (type.getValue() != null
-                                               && !type.getValue().trim().equals("")) {
-                                       dataBuilder.append('=');
-                                       dataBuilder.append(type.getValue());
-                               }
-                       }
-                       dataBuilder.append(':');
-
-                       // TODO: bkey!
-                       dataBuilder.append(data.getValue());
-
-                       // RFC says: Content lines SHOULD be folded to a maximum width of 75
-                       // octets -> since it is SHOULD, we will just cut it as 74/75 chars
-                       // depending if the last one fits in one char (note: chars != octet)
-                       boolean continuation = false;
-                       while (dataBuilder.length() > 0) {
-                               int stop = 74;
-                               if (continuation)
-                                       stop--; // the space takes 1
-                               if (dataBuilder.length() > stop) {
-                                       char car = dataBuilder.charAt(stop - 1);
-                                       // RFC forbids cutting a character in 2
-                                       if (Character.isHighSurrogate(car)) {
-                                               stop++;
-                                       }
-                               }
-
-                               stop = Math.min(stop, dataBuilder.length());
-                               if (continuation) {
-                                       lines.add(' ' + dataBuilder.substring(0, stop));
-                               } else {
-                                       lines.add(dataBuilder.substring(0, stop));
-                               }
-                               dataBuilder.delete(0, stop);
-
-                               continuation = true;
-                       }
+                       lines.addAll(toStrings(data));
                }
                lines.add("END:VCARD");
 
@@ -177,19 +179,61 @@ public class Vcard21Parser {
        }
 
        /**
-        * Return a {@link String} representation of the given {@link Card}, line by
+        * Return a {@link String} representation of the given {@link Data}, line by
         * line.
         * 
-        * @param card
-        *            the card to convert
+        * @param data
+        *            the data to convert
         * 
         * @return the {@link String} representation
         */
-       public static List<String> toStrings(Card card) {
+       public static List<String> toStrings(Data data) {
                List<String> lines = new LinkedList<String>();
 
-               for (Contact contact : card) {
-                       lines.addAll(toStrings(contact, -1));
+               StringBuilder dataBuilder = new StringBuilder();
+               if (data.getGroup() != null && !data.getGroup().trim().equals("")) {
+                       dataBuilder.append(data.getGroup().trim());
+                       dataBuilder.append('.');
+               }
+               dataBuilder.append(data.getName());
+               for (TypeInfo type : data) {
+                       dataBuilder.append(';');
+                       dataBuilder.append(type.getName());
+                       if (type.getValue() != null && !type.getValue().trim().equals("")) {
+                               dataBuilder.append('=');
+                               dataBuilder.append(type.getValue());
+                       }
+               }
+               dataBuilder.append(':');
+
+               // TODO: bkey!
+               dataBuilder.append(data.getValue());
+
+               // RFC says: Content lines SHOULD be folded to a maximum width of 75
+               // octets -> since it is SHOULD, we will just cut it as 74/75 chars
+               // depending if the last one fits in one char (note: chars != octet)
+               boolean continuation = false;
+               while (dataBuilder.length() > 0) {
+                       int stop = 74;
+                       if (continuation)
+                               stop--; // the space takes 1
+                       if (dataBuilder.length() > stop) {
+                               char car = dataBuilder.charAt(stop - 1);
+                               // RFC forbids cutting a character in 2
+                               if (Character.isHighSurrogate(car)) {
+                                       stop++;
+                               }
+                       }
+
+                       stop = Math.min(stop, dataBuilder.length());
+                       if (continuation) {
+                               lines.add(' ' + dataBuilder.substring(0, stop));
+                       } else {
+                               lines.add(dataBuilder.substring(0, stop));
+                       }
+                       dataBuilder.delete(0, stop);
+
+                       continuation = true;
                }
 
                return lines;
index 0fb0a7383da119523d32d2029e151a8af42b7f26..3a32f0c49c965388204441704665965dc68251b9 100644 (file)
@@ -15,14 +15,34 @@ public class Command {
                LIST,
                /** HELP about the protocol for interactive access */
                HELP,
+               /** GET a remote card */
+               GET_CARD,
+               /**
+                * PUT mode activation toggle for a card on the remote server (you can
+                * issue *_CONTACT commands when in PUT mode)
+                */
+               PUT_CARD,
+               /** POST a new card to the remote server */
+               POST_CARD,
+               /** DELETE an existing contact from the remote server */
+               DELETE_CARD,
                /** GET a remote contact */
-               GET,
-               /** PUT a new contact to the remote server or update an existing one */
-               PUT,
+               GET_CONTACT,
+               /**
+                * PUT mode activation toggle for a contact on the remote server (you
+                * can issue *_DATA commands when in PUT mode)
+                */
+               PUT_CONTACT,
                /** POST a new contact to the remote server */
-               POST,
+               POST_CONTACT,
                /** DELETE an existing contact from the remote server */
-               DELETE,
+               DELETE_CONTACT,
+               /** GET a remote data */
+               GET_DATA,
+               /** POST a new data to the remote server */
+               POST_DATA,
+               /** DELETE an existing data from the remote server */
+               DELETE_DATA,
        }
 
        private Verb verb;
index e90e42d6ff1e76c5a1caec0d91d2e5c2a80ec808..4b9936573fdecda0beac378d377fa19e2c4c2c1e 100644 (file)
@@ -5,14 +5,17 @@ import java.io.IOException;
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.net.UnknownHostException;
+import java.security.InvalidParameterException;
 import java.util.Date;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.ResourceBundle;
 
 import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.Contact;
+import be.nikiroo.jvcard.Data;
 import be.nikiroo.jvcard.parsers.Format;
-import be.nikiroo.jvcard.parsers.Parser;
+import be.nikiroo.jvcard.parsers.Vcard21Parser;
 import be.nikiroo.jvcard.remote.Command.Verb;
 import be.nikiroo.jvcard.resources.Bundles;
 import be.nikiroo.jvcard.tui.StringUtils;
@@ -40,7 +43,7 @@ public class Server implements Runnable {
        private Object clientsLock = new Object();
        private List<SimpleSocket> clients = new LinkedList<SimpleSocket>();
 
-       private Object cardsLock = new Object();
+       private Object updateLock = new Object();
 
        public static void main(String[] args) throws IOException {
                Server server = new Server(4444);
@@ -124,7 +127,21 @@ public class Server implements Runnable {
                                new Thread(new Runnable() {
                                        @Override
                                        public void run() {
-                                               accept(new SimpleSocket(s, "[request]"));
+                                               SimpleSocket ss = new SimpleSocket(s, "[request]");
+
+                                               addClient(ss);
+                                               try {
+                                                       ss.open(false);
+
+                                                       while (processCmd(ss))
+                                                               ;
+
+                                               } catch (IOException e) {
+                                                       e.printStackTrace();
+                                               } finally {
+                                                       ss.close();
+                                               }
+                                               removeClient(ss);
                                        }
                                }).start();
                        } catch (IOException ioe) {
@@ -157,82 +174,273 @@ public class Server implements Runnable {
        }
 
        /**
-        * Accept a client and process it.
+        * Process a command.
         * 
         * @param s
-        *            the client to process
+        *            the {@link SimpleSocket} from which to get the command to
+        *            process
+        * 
+        * @return TRUE if the client is ready for another command, FALSE when the
+        *         client exited
+        * 
+        * @throws IOException
+        *             in case of IO error
         */
-       private void accept(SimpleSocket s) {
-               addClient(s);
-
-               try {
-                       s.open(false);
-
-                       boolean clientStop = false;
-                       while (!clientStop) {
-                               Command cmd = s.receiveCommand();
-                               Command.Verb verb = cmd.getVerb();
-
-                               if (verb == null)
-                                       break;
-
-                               System.out.println(s + " ->  " + verb);
-
-                               switch (verb) {
-                               case STOP:
-                                       clientStop = true;
-                                       break;
-                               case VERSION:
-                                       s.sendCommand(Verb.VERSION);
-                                       break;
-                               case TIME:
-                                       s.sendLine(StringUtils.fromTime(new Date().getTime()));
-                                       break;
-                               case GET:
-                                       synchronized (cardsLock) {
-                                               s.sendBlock(doGetCard(cmd.getParam()));
-                                       }
-                                       break;
-                               case POST:
-                                       synchronized (cardsLock) {
-                                               s.sendLine(doPostCard(cmd.getParam(), s.receiveBlock()));
-                                               break;
-                                       }
-                               case LIST:
-                                       for (File file : dataDir.listFiles()) {
-                                               if (cmd.getParam() == null
-                                                               || cmd.getParam().length() == 0
-                                                               || file.getName().contains(cmd.getParam())) {
-                                                       s.send(StringUtils.fromTime(file.lastModified())
-                                                                       + " " + file.getName());
-                                               }
-                                       }
-                                       s.sendBlock();
-                                       break;
-                               case HELP:
-                                       // TODO: i18n
-                                       s.send("The following commands are available:");
-                                       s.send("- TIME: get the server time");
-                                       s.send("- HELP: this help screen");
-                                       s.send("- LIST: list the available cards on this server");
-                                       s.send("- VERSION/GET/PUT/POST/DELETE/STOP: TODO");
-                                       s.sendBlock();
-                                       break;
-                               default:
+       private boolean processCmd(SimpleSocket s) throws IOException {
+               Command cmd = s.receiveCommand();
+               Command.Verb verb = cmd.getVerb();
+
+               if (verb == null)
+                       return false;
+
+               boolean clientContinue = true;
+
+               System.out.println(s + " ->  " + verb);
+
+               switch (verb) {
+               case STOP:
+                       clientContinue = false;
+                       break;
+               case VERSION:
+                       s.sendCommand(Verb.VERSION);
+                       break;
+               case TIME:
+                       s.sendLine(StringUtils.fromTime(new Date().getTime()));
+                       break;
+               case GET_CARD:
+                       synchronized (updateLock) {
+                               s.sendBlock(doGetCard(cmd.getParam()));
+                       }
+                       break;
+               case POST_CARD:
+                       synchronized (updateLock) {
+                               s.sendLine(doPostCard(cmd.getParam(), s.receiveBlock()));
+                       }
+                       break;
+               case PUT_CARD:
+                       synchronized (updateLock) {
+                               File vcf = getFile(cmd.getParam());
+                               if (vcf == null) {
                                        System.err
-                                                       .println("Unsupported command received from a client connection, closing it: "
-                                                                       + verb);
-                                       clientStop = true;
-                                       break;
+                                                       .println("Fail to update a card, file not available: "
+                                                                       + cmd.getParam());
+                                       clientContinue = false;
+                               } else {
+                                       Card card = new Card(vcf, Format.VCard21);
+                                       try {
+                                               while (processContactCmd(s, card))
+                                                       ;
+                                               card.save();
+                                       } catch (InvalidParameterException e) {
+                                               System.err
+                                                               .println("Unsupported command received from a client connection, closing it: "
+                                                                               + verb + " (" + e.getMessage() + ")");
+                                               clientContinue = false;
+                                       }
                                }
                        }
-               } catch (IOException e) {
-                       e.printStackTrace();
-               } finally {
-                       s.close();
+                       break;
+               case DELETE_CARD:
+                       // TODO
+                       System.err
+                                       .println("Unsupported command received from a client connection, closing it: "
+                                                       + verb);
+                       clientContinue = false;
+                       break;
+               case LIST:
+                       for (File file : dataDir.listFiles()) {
+                               if (cmd.getParam() == null || cmd.getParam().length() == 0
+                                               || file.getName().contains(cmd.getParam())) {
+                                       s.send(StringUtils.fromTime(file.lastModified()) + " "
+                                                       + file.getName());
+                               }
+                       }
+                       s.sendBlock();
+                       break;
+               case HELP:
+                       // TODO: i18n
+                       s.send("The following commands are available:");
+                       s.send("- TIME: get the server time");
+                       s.send("- HELP: this help screen");
+                       s.send("- LIST: list the available cards on this server");
+                       s.send("- VERSION/GET/PUT/POST/DELETE/STOP: TODO");
+                       s.sendBlock();
+                       break;
+               default:
+                       System.err
+                                       .println("Unsupported command received from a client connection, closing it: "
+                                                       + verb);
+                       clientContinue = false;
+                       break;
+               }
+
+               return clientContinue;
+       }
+
+       /**
+        * Process a *_CONTACT subcommand.
+        * 
+        * @param s
+        *            the {@link SimpleSocket} to process
+        * @param card
+        *            the target {@link Card}
+        * 
+        * @return TRUE if the client is ready for another command, FALSE when the
+        *         client is done
+        * 
+        * @throws IOException
+        *             in case of IO error
+        * 
+        * @throw InvalidParameterException in case of invalid subcommand
+        */
+       private boolean processContactCmd(SimpleSocket s, Card card)
+                       throws IOException {
+               Command cmd = s.receiveCommand();
+               Command.Verb verb = cmd.getVerb();
+
+               if (verb == null)
+                       return false;
+
+               boolean clientContinue = true;
+
+               System.out.println(s + " ->  " + verb);
+
+               switch (verb) {
+               case GET_CONTACT: {
+                       Contact contact = card.getById(cmd.getParam());
+                       if (contact != null)
+                               s.sendBlock(Vcard21Parser.toStrings(contact, -1));
+                       else
+                               s.sendBlock();
+                       break;
+               }
+               case POST_CONTACT: {
+                       String uid = cmd.getParam();
+                       Contact contact = card.getById(uid);
+                       if (contact != null)
+                               contact.delete();
+                       List<Contact> list = Vcard21Parser.parseContact(s.receiveBlock());
+                       if (list.size() > 0) {
+                               contact = list.get(0);
+                               contact.getPreferredData("UID").setValue(uid);
+                               card.add(contact);
+                       }
+                       break;
+               }
+               case PUT_CONTACT: {
+                       String uid = cmd.getParam();
+                       Contact contact = card.getById(uid);
+                       if (contact == null) {
+                               throw new InvalidParameterException(
+                                               "Cannot find contact to modify for UID: " + uid);
+                       }
+                       while (processDataCmd(s, contact))
+                               ;
+                       break;
+               }
+               case DELETE_CONTACT: {
+                       String uid = cmd.getParam();
+                       Contact contact = card.getById(uid);
+                       if (contact == null) {
+                               throw new InvalidParameterException(
+                                               "Cannot find contact to delete for UID: " + uid);
+                       }
+
+                       contact.delete();
+                       break;
+               }
+               case PUT_CARD: {
+                       clientContinue = false;
+                       break;
+               }
+               default: {
+                       throw new InvalidParameterException("command invalid here");
+               }
                }
 
-               removeClient(s);
+               return clientContinue;
+       }
+
+       /**
+        * Process a *_DATA subcommand.
+        * 
+        * @param s
+        *            the {@link SimpleSocket} to process
+        * @param card
+        *            the target {@link Contact}
+        * 
+        * @return TRUE if the client is ready for another command, FALSE when the
+        *         client is done
+        * 
+        * @throws IOException
+        *             in case of IO error
+        * 
+        * @throw InvalidParameterException in case of invalid subcommand
+        */
+       private boolean processDataCmd(SimpleSocket s, Contact contact)
+                       throws IOException {
+               Command cmd = s.receiveCommand();
+               Command.Verb verb = cmd.getVerb();
+
+               if (verb == null)
+                       return false;
+
+               boolean clientContinue = true;
+
+               System.out.println(s + " ->  " + verb);
+
+               switch (verb) {
+               case GET_DATA: {
+                       Data data = contact.getById(cmd.getParam());
+                       if (data != null)
+                               s.sendBlock(Vcard21Parser.toStrings(data));
+                       else
+                               s.sendBlock();
+                       break;
+               }
+               case POST_DATA: {
+                       String cstate = cmd.getParam();
+                       Data data = null;
+                       for (Data d : contact) {
+                               if (cstate.equals(d.getContentState()))
+                                       data = d;
+                       }
+
+                       if (data != null)
+                               data.delete();
+                       List<Data> list = Vcard21Parser.parseData(s.receiveBlock());
+                       if (list.size() > 0) {
+                               contact.add(list.get(0));
+                       }
+                       break;
+               }
+               case DELETE_DATA: {
+                       String cstate = cmd.getParam();
+                       Data data = null;
+                       for (Data d : contact) {
+                               if (cstate.equals(d.getContentState()))
+                                       data = d;
+                       }
+
+                       if (data == null) {
+                               throw new InvalidParameterException(
+                                               "Cannot find data to delete for content state: "
+                                                               + cstate);
+                       }
+
+                       contact.delete();
+                       break;
+               }
+               case PUT_CONTACT: {
+                       clientContinue = false;
+                       break;
+               }
+               default: {
+                       throw new InvalidParameterException("command invalid here");
+               }
+               }
+
+               return clientContinue;
        }
 
        /**
@@ -249,17 +457,14 @@ public class Server implements Runnable {
        private List<String> doGetCard(String name) throws IOException {
                List<String> lines = new LinkedList<String>();
 
-               if (name != null && name.length() > 0) {
-                       File vcf = new File(dataDir.getAbsolutePath() + File.separator
-                                       + name);
+               File vcf = getFile(name);
 
-                       if (vcf.exists()) {
-                               Card card = new Card(vcf, Format.VCard21);
+               if (vcf != null && vcf.exists()) {
+                       Card card = new Card(vcf, Format.VCard21);
 
-                               // timestamp:
-                               lines.add(StringUtils.fromTime(card.getLastModified()));
-                               lines.addAll(Parser.toStrings(card, Format.VCard21));
-                       }
+                       // timestamp:
+                       lines.add(StringUtils.fromTime(card.getLastModified()));
+                       lines.addAll(Vcard21Parser.toStrings(card));
                }
 
                return lines;
@@ -280,11 +485,11 @@ public class Server implements Runnable {
         */
        private String doPostCard(String name, List<String> data)
                        throws IOException {
-               if (name != null && name.length() > 0) {
-                       File vcf = new File(dataDir.getAbsolutePath() + File.separator
-                                       + name);
 
-                       Card card = new Card(Parser.parse(data, Format.VCard21));
+               File vcf = getFile(name);
+
+               if (vcf != null) {
+                       Card card = new Card(Vcard21Parser.parseContact(data));
                        card.saveAs(vcf, Format.VCard21);
 
                        return StringUtils.fromTime(vcf.lastModified());
@@ -292,4 +497,21 @@ public class Server implements Runnable {
 
                return "";
        }
+
+       /**
+        * Return the {@link File} corresponding to the given resource name.
+        * 
+        * @param name
+        *            the resource name
+        * 
+        * @return the corresponding {@link File} or NULL if the name was NULL or
+        *         empty
+        */
+       private File getFile(String name) {
+               if (name != null && name.length() > 0) {
+                       return new File(dataDir.getAbsolutePath() + File.separator + name);
+               }
+
+               return null;
+       }
 }
index 9fff9b8985056039e42c14c51dfb7013efb1412f..d382a2fc626fae9870648a381793d4a4893d6a1f 100644 (file)
@@ -12,14 +12,16 @@ import java.io.OutputStreamWriter;
 import java.net.Socket;
 import java.net.UnknownHostException;
 import java.security.InvalidParameterException;
-import java.time.LocalDate;
+import java.util.LinkedList;
 import java.util.List;
 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.parsers.Format;
-import be.nikiroo.jvcard.parsers.Parser;
+import be.nikiroo.jvcard.parsers.Vcard21Parser;
 import be.nikiroo.jvcard.remote.Command.Verb;
 import be.nikiroo.jvcard.resources.Bundles;
 import be.nikiroo.jvcard.tui.StringUtils;
@@ -189,9 +191,11 @@ public class Sync {
                // Check changes
                boolean serverChanges = (tsServer - tsOriginal) > GRACE_TIME;
                boolean localChanges = false;
+               Card local = null;
+               Card original = null;
                if (tsOriginal != -1) {
-                       Card local = new Card(getCache(cacheDir), Format.VCard21);
-                       Card original = new Card(getCache(cacheDirOrig), Format.VCard21);
+                       local = new Card(getCache(cacheDir), Format.VCard21);
+                       original = new Card(getCache(cacheDirOrig), Format.VCard21);
                        localChanges = !local.isEquals(original, true);
                }
 
@@ -199,24 +203,28 @@ public class Sync {
 
                // Sync to server if:
                if (localChanges) {
-                       // TODO: sync instead (with PUT)
-                       action = Verb.POST;
+                       action = Verb.PUT_CARD;
                }
 
-               // Sync from server if
+               // Sync from/to server if
                if (serverChanges && localChanges) {
-                       // TODO
-                       throw new IOException("Sync operation not supported yet :(");
+                       action = Verb.PUT_CARD;
+               }
+
+               // Sync from server if:
+               if (serverChanges) {
+                       // TODO: only sends changed cstate if serverChanges
+                       action = Verb.GET_CARD;
                }
 
                // PUT the whole file if:
                if (tsServer == -1) {
-                       action = Verb.POST;
+                       action = Verb.POST_CARD;
                }
 
                // GET the whole file if:
-               if (tsOriginal == -1 || serverChanges) {
-                       action = Verb.GET;
+               if (tsOriginal == -1) {
+                       action = Verb.GET_CARD;
                }
 
                System.err.println("remote: " + (tsServer / 1000) % 1000 + " ("
@@ -229,23 +237,57 @@ public class Sync {
 
                if (action != null) {
                        switch (action) {
-                       case GET:
-                               s.sendCommand(Verb.GET, name);
+                       case GET_CARD:
+                               s.sendCommand(Verb.GET_CARD, name);
                                List<String> data = s.receiveBlock();
                                setLastModified(data.remove(0));
-                               Card server = new Card(Parser.parse(data, Format.VCard21));
+                               Card server = new Card(Vcard21Parser.parseContact(data));
                                card.replaceListContent(server);
 
                                if (card.isDirty())
                                        card.save();
                                card.saveAs(getCache(cacheDirOrig), Format.VCard21);
                                break;
-                       case POST:
-                               s.sendCommand(Verb.POST, name);
-                               s.sendLine(card.toString(Format.VCard21));
+                       case POST_CARD:
+                               s.sendCommand(Verb.POST_CARD, name);
+                               s.sendBlock(Vcard21Parser.toStrings(card));
                                card.saveAs(getCache(cacheDirOrig), Format.VCard21);
                                setLastModified(s.receiveLine());
                                break;
+                       case PUT_CARD:
+                               List<Contact> added = new LinkedList<Contact>();
+                               List<Contact> removed = new LinkedList<Contact>();
+                               List<Contact> from = new LinkedList<Contact>();
+                               List<Contact> to = new LinkedList<Contact>();
+                               original.compare(local, added, removed, from, to);
+                               s.sendCommand(Verb.PUT_CARD, name);
+                               for (Contact c : removed) {
+                                       s.sendCommand(Verb.DELETE_CONTACT, c.getId());
+                               }
+                               for (Contact c : added) {
+                                       s.sendCommand(Verb.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<Data> subadded = new LinkedList<Data>();
+                                               List<Data> subremoved = new LinkedList<Data>();
+                                               f.compare(t, subadded, subremoved, subremoved, subadded);
+                                               s.sendCommand(Verb.PUT_CONTACT, name);
+                                               for (Data d : subremoved) {
+                                                       s.sendCommand(Verb.DELETE_DATA, d.getContentState());
+                                               }
+                                               for (Data d : subadded) {
+                                                       s.sendCommand(Verb.POST_DATA, d.getContentState());
+                                                       s.sendBlock(Vcard21Parser.toStrings(d));
+                                               }
+                                       }
+                               }
+                               s.sendCommand(Verb.PUT_CARD);
+                               break;
                        default:
                                // TODO
                                throw new IOException(action