Resources system rewrite + new "--save-config DIR" option
[jvcard.git] / src / be / nikiroo / jvcard / remote / Server.java
index 2ead82b19a90d6bf3a84b25c9c44d9394fd0d171..962f2d00133069eee26205f97e23205373b86e44 100644 (file)
@@ -5,17 +5,21 @@ 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.HashMap;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.ResourceBundle;
+import java.util.Map;
 
 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.remote.Command.Verb;
-import be.nikiroo.jvcard.resources.Bundles;
-import be.nikiroo.jvcard.tui.StringUtils;
+import be.nikiroo.jvcard.parsers.Vcard21Parser;
+import be.nikiroo.jvcard.resources.StringUtils;
+import be.nikiroo.jvcard.resources.bundles.RemoteBundle;
+import be.nikiroo.jvcard.resources.enums.RemotingOption;
 
 /**
  * This class implements a small server that can listen for requests to
@@ -40,15 +44,11 @@ public class Server implements Runnable {
        private Object clientsLock = new Object();
        private List<SimpleSocket> clients = new LinkedList<SimpleSocket>();
 
-       private Object cardsLock = new Object();
-
-       public static void main(String[] args) throws IOException {
-               Server server = new Server(4444);
-               server.run();
-       }
+       private Object updateLock = new Object();
+       private Map<File, Integer> updates = new HashMap<File, Integer>();
 
        /**
-        * Create a new jVCard sercer on the given port.
+        * Create a new jVCard server on the given port.
         * 
         * @param port
         *            the port to run on
@@ -58,9 +58,9 @@ public class Server implements Runnable {
         */
        public Server(int port) throws IOException {
                this.port = port;
-               ResourceBundle bundle = Bundles.getBundle("remote");
+               RemoteBundle bundle = new RemoteBundle();
                try {
-                       String dir = bundle.getString("SERVER_DATA_PATH");
+                       String dir = bundle.getString(RemotingOption.SERVER_DATA_PATH);
                        dataDir = new File(dir);
                        dataDir.mkdir();
 
@@ -87,7 +87,7 @@ public class Server implements Runnable {
                        SimpleSocket c = new SimpleSocket(new Socket((String) null, port),
                                        "special STOP client");
                        c.open(true);
-                       c.sendCommand(Verb.STOP);
+                       c.sendCommand(Command.STOP);
                        c.close();
                } catch (UnknownHostException e) {
                        e.printStackTrace();
@@ -124,7 +124,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,83 +171,424 @@ public class Server implements Runnable {
        }
 
        /**
-        * Accept a client and process it.
+        * Process a first-level 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);
+       private boolean processCmd(SimpleSocket s) throws IOException {
+               CommandInstance cmd = s.receiveCommand();
+               Command command = cmd.getCommand();
 
-               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()));
+               if (command == null)
+                       return false;
+
+               boolean clientContinue = true;
+
+               System.out.println(s + " ->  " + command
+                               + (cmd.getParam() == null ? "" : " " + cmd.getParam()));
+
+               switch (command) {
+               case STOP: {
+                       clientContinue = false;
+                       break;
+               }
+               case VERSION: {
+                       s.sendLine("" + SimpleSocket.CURRENT_VERSION);
+                       break;
+               }
+               case TIME: {
+                       s.sendLine(StringUtils.fromTime(new Date().getTime()));
+                       break;
+               }
+               case SELECT: {
+                       String name = cmd.getParam();
+                       File file = new File(dataDir.getAbsolutePath() + File.separator
+                                       + name);
+                       if (name == null || name.length() == 0 || !file.exists()) {
+                               System.err
+                                               .println("SELECT: resource not found, closing connection: "
+                                                               + name);
+                               clientContinue = false;
+                       } else {
+                               synchronized (updateLock) {
+                                       for (File f : updates.keySet()) {
+                                               if (f.getCanonicalPath()
+                                                               .equals(file.getCanonicalPath())) {
+                                                       file = f;
+                                                       break;
+                                               }
                                        }
-                                       break;
-                               case POST:
-                                       synchronized (cardsLock) {
-                                               doPostCard(cmd.getParam(), s.receiveBlock());
-                                               s.sendBlock();
-                                               break;
+
+                                       if (!updates.containsKey(file))
+                                               updates.put(file, 0);
+                                       updates.put(file, updates.get(file) + 1);
+                               }
+
+                               synchronized (file) {
+                                       try {
+                                               s.sendLine(StringUtils.fromTime(file.lastModified()));
+
+                                               while (processLockedCmd(s, name))
+                                                       ;
+                                       } catch (InvalidParameterException e) {
+                                               System.err
+                                                               .println("Unsupported command received from a client connection, closing it: "
+                                                                               + command + " (" + e.getMessage() + ")");
+                                               clientContinue = false;
                                        }
-                               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());
-                                               }
+                               }
+
+                               synchronized (updateLock) {
+                                       int num = updates.get(file) - 1;
+                                       if (num == 0) {
+                                               updates.remove(file);
+                                       } else {
+                                               updates.put(file, num);
                                        }
-                                       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:
+                               }
+                       }
+                       break;
+               }
+               case LIST_CARD: {
+                       for (File file : dataDir.listFiles()) {
+                               if (cmd.getParam() == null
+                                               || cmd.getParam().length() == 0
+                                               || file.getName().toLowerCase()
+                                                               .contains(cmd.getParam().toLowerCase())) {
+                                       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_CARD: 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: "
+                                                       + command);
+                       clientContinue = false;
+                       break;
+               }
+               }
+
+               return clientContinue;
+       }
+
+       /**
+        * Process a subcommand while protected for resource <tt>name</tt>.
+        * 
+        * @param s
+        *            the {@link SimpleSocket} to process
+        * 
+        * @param name
+        *            the resource that is protected (and to target)
+        * 
+        * @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 processLockedCmd(SimpleSocket s, String name)
+                       throws IOException {
+               CommandInstance cmd = s.receiveCommand();
+               Command command = cmd.getCommand();
+
+               if (command == null)
+                       return false;
+
+               boolean clientContinue = true;
+
+               System.out.println(s + " ->  " + command);
+
+               switch (command) {
+               case GET_CARD: {
+                       s.sendBlock(doGetCard(name));
+                       break;
+               }
+               case POST_CARD: {
+                       s.sendLine(doPostCard(name, s.receiveBlock()));
+                       break;
+               }
+               case PUT_CARD: {
+                       File vcf = getFile(name);
+                       if (vcf == null) {
+                               System.err
+                                               .println("Fail to update a card, file not available: "
+                                                               + name);
+                               clientContinue = false;
+                       } else {
+                               Card card = new Card(vcf, Format.VCard21);
+                               try {
+                                       while (processContactCmd(s, card))
+                                               ;
+                                       card.save();
+                                       s.sendLine(StringUtils.fromTime(card.getLastModified()));
+                               } catch (InvalidParameterException e) {
                                        System.err
                                                        .println("Unsupported command received from a client connection, closing it: "
-                                                                       + verb);
-                                       clientStop = true;
-                                       break;
+                                                                       + command + " (" + 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: "
+                                                       + command);
+                       clientContinue = false;
+                       break;
+               }
+               case SELECT: {
+                       clientContinue = false;
+                       break;
+               }
+               default: {
+                       throw new InvalidParameterException("command invalid here: "
+                                       + command);
+               }
                }
 
-               removeClient(s);
+               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 {
+               CommandInstance cmd = s.receiveCommand();
+               Command command = cmd.getCommand();
+
+               if (command == null)
+                       return false;
+
+               boolean clientContinue = true;
+
+               System.out.println(s + " ->  " + command);
+
+               switch (command) {
+               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: {
+                       List<Contact> list = Vcard21Parser.parseContact(s.receiveBlock());
+                       if (list.size() > 0) {
+                               Contact newContact = list.get(0);
+                               String uid = newContact.getPreferredDataValue("UID");
+                               Contact oldContact = card.getById(uid);
+                               if (oldContact != null)
+                                       oldContact.delete();
+                               card.add(newContact);
+                       }
+
+                       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 HASH_CONTACT: {
+                       String uid = cmd.getParam();
+                       Contact contact = card.getById(uid);
+
+                       if (contact == null) {
+                               s.sendBlock();
+                       } else {
+                               s.sendLine(contact.getContentState(true));
+                       }
+                       break;
+               }
+               case LIST_CONTACT: {
+                       for (Contact contact : card) {
+                               if (cmd.getParam() == null
+                                               || cmd.getParam().length() == 0
+                                               || (contact.getPreferredDataValue("FN") + contact
+                                                               .getPreferredDataValue("N")).toLowerCase()
+                                                               .contains(cmd.getParam().toLowerCase())) {
+                                       s.send(contact.getContentState(true) + " "
+                                                       + contact.getId());
+                               }
+                       }
+                       s.sendBlock();
+                       break;
+               }
+               case PUT_CARD: {
+                       clientContinue = false;
+                       break;
+               }
+               default: {
+                       throw new InvalidParameterException("command invalid here: "
+                                       + command);
+               }
+               }
+
+               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 {
+               CommandInstance cmd = s.receiveCommand();
+               Command command = cmd.getCommand();
+
+               if (command == null)
+                       return false;
+
+               boolean clientContinue = true;
+
+               System.out.println(s + " ->  " + command);
+
+               switch (command) {
+               case GET_DATA: {
+                       for (Data data : contact) {
+                               if (data.getName().equals(cmd.getParam())) {
+                                       for (String line : Vcard21Parser.toStrings(data)) {
+                                               s.send(line);
+                                       }
+                               }
+                       }
+                       s.sendBlock();
+                       break;
+               }
+               case POST_DATA: {
+                       String cstate = cmd.getParam();
+                       Data data = null;
+                       for (Data d : contact) {
+                               if (cstate.equals(d.getContentState(true)))
+                                       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(true)))
+                                       data = d;
+                       }
+
+                       if (data == null) {
+                               throw new InvalidParameterException(
+                                               "Cannot find data to delete for content state: "
+                                                               + cstate);
+                       }
+
+                       contact.delete();
+                       break;
+               }
+               case HASH_DATA: {
+                       for (Data data : contact) {
+                               if (data.getId().equals(cmd.getParam())) {
+                                       s.send(data.getContentState(true));
+                               }
+                       }
+                       s.sendBlock();
+                       break;
+               }
+               case LIST_DATA: {
+                       for (Data data : contact) {
+                               if (cmd.getParam() == null
+                                               || cmd.getParam().length() == 0
+                                               || data.getName().toLowerCase()
+                                                               .contains(cmd.getParam().toLowerCase())) {
+                                       s.send(data.getContentState(true) + " " + data.getName());
+                               }
+                       }
+                       s.sendBlock();
+                       break;
+               }
+               case PUT_CONTACT: {
+                       clientContinue = false;
+                       break;
+               }
+               default: {
+                       throw new InvalidParameterException("command invalid here: "
+                                       + command);
+               }
+               }
+
+               return clientContinue;
        }
 
        /**
@@ -250,21 +605,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()));
-
-                               // TODO: !!! fix this !!!
-                               for (String line : card.toString(Format.VCard21).split("\r\n")) {
-                                       lines.add(line);
-                               }
-                       }
+                       // timestamp + data
+                       lines.add(StringUtils.fromTime(card.getLastModified()));
+                       lines.addAll(Vcard21Parser.toStrings(card));
                }
 
                return lines;
@@ -278,16 +626,40 @@ public class Server implements Runnable {
         * @param data
         *            the data to save
         * 
+        * @return the date of last modification
+        * 
         * @throws IOException
         *             in case of error
         */
-       private void doPostCard(String name, List<String> data) throws IOException {
-               if (name != null && name.length() > 0) {
-                       File vcf = new File(dataDir.getAbsolutePath() + File.separator
-                                       + name);
+       private String doPostCard(String name, List<String> data)
+                       throws IOException {
+
+               File vcf = getFile(name);
 
-                       Card card = new Card(Parser.parse(data, Format.VCard21));
+               if (vcf != null) {
+                       Card card = new Card(Vcard21Parser.parseContact(data));
                        card.saveAs(vcf, Format.VCard21);
+
+                       return StringUtils.fromTime(vcf.lastModified());
                }
+
+               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;
        }
 }