jvcard remote support (initial commit, not ready for use yet)
authorNiki Roo <niki@nikiroo.be>
Sun, 13 Mar 2016 13:11:11 +0000 (14:11 +0100)
committerNiki Roo <niki@nikiroo.be>
Sun, 13 Mar 2016 13:11:11 +0000 (14:11 +0100)
src/be/nikiroo/jvcard/remote/Command.java [new file with mode: 0644]
src/be/nikiroo/jvcard/remote/Server.java [new file with mode: 0644]
src/be/nikiroo/jvcard/remote/SimpleSocket.java [new file with mode: 0644]
src/be/nikiroo/jvcard/remote/Sync.java [new file with mode: 0644]
src/be/nikiroo/jvcard/resources/remote.properties [new file with mode: 0644]
src/be/nikiroo/jvcard/tui/Main.java
src/be/nikiroo/jvcard/tui/panes/FileList.java

diff --git a/src/be/nikiroo/jvcard/remote/Command.java b/src/be/nikiroo/jvcard/remote/Command.java
new file mode 100644 (file)
index 0000000..0fb0a73
--- /dev/null
@@ -0,0 +1,143 @@
+package be.nikiroo.jvcard.remote;
+
+public class Command {
+       public enum Verb {
+               /** VERSION of the protocol */
+               VERSION,
+               /** TIME of the remote server in milliseconds since the Unix epoch */
+               TIME,
+               /** STOP the communication (client stops) */
+               STOP,
+               /**
+                * LIST all the contacts on the remote server that contain the search
+                * term, or all contacts if no search term given
+                */
+               LIST,
+               /** HELP about the protocol for interactive access */
+               HELP,
+               /** GET a remote contact */
+               GET,
+               /** PUT a new contact to the remote server or update an existing one */
+               PUT,
+               /** POST a new contact to the remote server */
+               POST,
+               /** DELETE an existing contact from the remote server */
+               DELETE,
+       }
+
+       private Verb verb;
+       private int version;
+       private String param;
+
+       /**
+        * Create a new, empty {@link Command} with the given {@link Verb} and
+        * version.
+        * 
+        * @param verb
+        *            the {@link Verb}
+        * @param version
+        *            the version
+        */
+       public Command(Verb verb, int version) {
+               this(verb, null, version);
+       }
+
+       /**
+        * Create a new, empty {@link Command} with the given {@link Verb} and
+        * version.
+        * 
+        * @param verb
+        *            the {@link Verb}
+        * @param version
+        *            the version
+        */
+       public Command(Verb verb, String param, int version) {
+               this.verb = verb;
+               this.version = version;
+               this.param = param;
+       }
+
+       /**
+        * Read a command line (starting with a {@link Verb}) and process its
+        * content here in a more readable format.
+        * 
+        * @param input
+        *            the command line
+        * @param version
+        *            the version (which can be overrided by a {@link Verb#VERSION}
+        *            command)
+        */
+       public Command(String input, int version) {
+               this.version = version;
+
+               if (input != null) {
+                       String v = input;
+                       int indexSp = input.indexOf(" ");
+                       if (indexSp >= 0) {
+                               v = input.substring(0, indexSp);
+                       }
+
+                       for (Verb verb : Verb.values()) {
+                               if (v.equals(verb.name())) {
+                                       this.verb = verb;
+                               }
+                       }
+
+                       if (verb != null) {
+                               String param = null;
+                               if (indexSp >= 0)
+                                       param = input.substring(indexSp + 1);
+
+                               this.param = param;
+
+                               if (verb == Verb.VERSION) {
+                                       try {
+                                               version = Integer.parseInt(param);
+                                       } catch (NumberFormatException e) {
+                                               e.printStackTrace();
+                                       }
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Return the version
+        * 
+        * @return the version
+        */
+       public int getVersion() {
+               return version;
+       }
+
+       /**
+        * Return the {@link Verb}
+        * 
+        * @return the {@link Verb}
+        */
+       public Verb getVerb() {
+               return verb;
+       }
+
+       /**
+        * Return the parameter of this {@link Command} if any.
+        * 
+        * @return the parameter or NULL
+        */
+       public String getParam() {
+               return param;
+       }
+
+       @Override
+       public String toString() {
+               if (verb == null)
+                       return "[null command]";
+
+               switch (verb) {
+               case VERSION:
+                       return verb.name() + " " + version;
+               default:
+                       return verb.name() + (param == null ? "" : " " + param);
+               }
+       }
+}
diff --git a/src/be/nikiroo/jvcard/remote/Server.java b/src/be/nikiroo/jvcard/remote/Server.java
new file mode 100644 (file)
index 0000000..2ead82b
--- /dev/null
@@ -0,0 +1,293 @@
+package be.nikiroo.jvcard.remote;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.UnknownHostException;
+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.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;
+
+/**
+ * This class implements a small server that can listen for requests to
+ * synchronise, get and put {@link Card}s.
+ * 
+ * <p>
+ * It is <b>NOT</b> secured in any way (it even is nice enough to give you a
+ * help message when you connect in raw mode via nc on how to use it), so do
+ * <b>NOT</b> enable such a server to be accessible from internet. This is not
+ * safe. Use a ssh/openssl tunnel or similar.
+ * </p>
+ * 
+ * @author niki
+ *
+ */
+public class Server implements Runnable {
+       private ServerSocket ss;
+       private int port;
+       private boolean stop;
+       private File dataDir;
+
+       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();
+       }
+
+       /**
+        * Create a new jVCard sercer on the given port.
+        * 
+        * @param port
+        *            the port to run on
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       public Server(int port) throws IOException {
+               this.port = port;
+               ResourceBundle bundle = Bundles.getBundle("remote");
+               try {
+                       String dir = bundle.getString("SERVER_DATA_PATH");
+                       dataDir = new File(dir);
+                       dataDir.mkdir();
+
+                       if (!dataDir.exists()) {
+                               throw new IOException("Cannot open or create data store at: "
+                                               + dataDir);
+                       }
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       throw new IOException("Cannot open or create data store at: "
+                                       + dataDir, e);
+               }
+
+               ss = new ServerSocket(port);
+       }
+
+       /**
+        * Stop the server. It may take some time before returning, but will only
+        * return when the server is actually stopped.
+        */
+       public void stop() {
+               stop = true;
+               try {
+                       SimpleSocket c = new SimpleSocket(new Socket((String) null, port),
+                                       "special STOP client");
+                       c.open(true);
+                       c.sendCommand(Verb.STOP);
+                       c.close();
+               } catch (UnknownHostException e) {
+                       e.printStackTrace();
+               } catch (IOException e) {
+                       e.printStackTrace();
+               }
+
+               if (clients.size() > 0) {
+                       try {
+                               Thread.sleep(100);
+                       } catch (InterruptedException e) {
+                       }
+
+                       if (clients.size() > 0) {
+                               synchronized (clientsLock) {
+                                       for (SimpleSocket s : clients) {
+                                               System.err
+                                                               .println("Forcefully closing client connection");
+                                               s.close();
+                                       }
+
+                                       clients.clear();
+                               }
+                       }
+               }
+       }
+
+       @Override
+       public void run() {
+               while (!stop) {
+                       try {
+                               final Socket s = ss.accept();
+                               // TODO: thread pool?
+                               new Thread(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               accept(new SimpleSocket(s, "[request]"));
+                                       }
+                               }).start();
+                       } catch (IOException ioe) {
+                               ioe.printStackTrace();
+                       }
+               }
+       }
+
+       /**
+        * Add a client to the current count.
+        * 
+        * @return the client index number
+        */
+       private void addClient(SimpleSocket s) {
+               synchronized (clientsLock) {
+                       clients.add(s);
+               }
+       }
+
+       /**
+        * Remove a client from the current count.
+        * 
+        * @param index
+        *            the client index number
+        */
+       private void removeClient(SimpleSocket s) {
+               synchronized (clientsLock) {
+                       clients.remove(s);
+               }
+       }
+
+       /**
+        * Accept a client and process it.
+        * 
+        * @param s
+        *            the client to process
+        */
+       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) {
+                                               doPostCard(cmd.getParam(), s.receiveBlock());
+                                               s.sendBlock();
+                                               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);
+                                       clientStop = true;
+                                       break;
+                               }
+                       }
+               } catch (IOException e) {
+                       e.printStackTrace();
+               } finally {
+                       s.close();
+               }
+
+               removeClient(s);
+       }
+
+       /**
+        * Return the serialised {@link Card} (with timestamp).
+        * 
+        * @param name
+        *            the resource name to load
+        * 
+        * @return the serialised data
+        * 
+        * @throws IOException
+        *             in case of error
+        */
+       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);
+
+                       if (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);
+                               }
+                       }
+               }
+
+               return lines;
+       }
+
+       /**
+        * Save the data to the new given resource.
+        * 
+        * @param name
+        *            the resource name to save
+        * @param data
+        *            the data to save
+        * 
+        * @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);
+
+                       Card card = new Card(Parser.parse(data, Format.VCard21));
+                       card.saveAs(vcf, Format.VCard21);
+               }
+       }
+}
diff --git a/src/be/nikiroo/jvcard/remote/SimpleSocket.java b/src/be/nikiroo/jvcard/remote/SimpleSocket.java
new file mode 100644 (file)
index 0000000..e35189b
--- /dev/null
@@ -0,0 +1,310 @@
+package be.nikiroo.jvcard.remote;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+import be.nikiroo.jvcard.remote.Command.Verb;
+
+/**
+ * A client or server connection, that will allow you to connect to, send and
+ * receive data to/from a jVCard remote server.
+ * 
+ * 
+ * 
+ * @author niki
+ */
+public class SimpleSocket {
+       /**
+        * The current version of the network protocol.
+        */
+       static private final int CURRENT_VERSION = 1;
+
+       /**
+        * The end of block marker.
+        * 
+        * An end of block marker needs to be on a line on itself to be valid, and
+        * will denote the end of a block of data.
+        */
+       static private String EOB = ".";
+
+       private Socket s;
+       private PrintWriter out;
+       private BufferedReader in;
+       private int version; // version of the OTHER end, not this one (this one is
+                                                       // CURRENT_VERSION obviously)
+
+       private String label; // can be used for debugging
+
+       /**
+        * Create a new {@link SimpleSocket} with the given {@link Socket}.
+        * 
+        * @param s
+        *            the {@link Socket}
+        */
+       public SimpleSocket(Socket s, String label) {
+               this.s = s;
+               this.label = label;
+       }
+
+       /**
+        * Return the label of this {@link SimpleSocket}. This is mainly used for
+        * debugging purposes or user display if any. It is optional.
+        * 
+        * @return the label
+        */
+       public String getLabel() {
+               if (label == null)
+                       return "[no name]";
+               return label;
+       }
+
+       /**
+        * Open the {@link SimpleSocket} for reading/writing and negotiates the
+        * version.
+        * 
+        * Note that you <b>MUST</b> call {@link SimpleSocket#close()} when you are
+        * done to release the acquired resources.
+        * 
+        * @param client
+        *            TRUE for clients, FALSE for servers (server speaks first)
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       public void open(boolean client) throws IOException {
+               out = new PrintWriter(s.getOutputStream(), false);
+               in = new BufferedReader(new InputStreamReader(s.getInputStream()));
+
+               if (client) {
+                       version = new Command(receiveLine(), -1).getVersion();
+                       sendLine(new Command(Command.Verb.VERSION, CURRENT_VERSION)
+                                       .toString());
+               } else {
+                       send(new Command(Command.Verb.VERSION, CURRENT_VERSION).toString());
+                       // TODO: i18n
+                       send("[Some help info here]");
+                       send("you need to reply with your VERSION + end of block");
+                       send("please send HELP in a full block or help");
+                       sendBlock();
+                       version = new Command(receiveLine(), -1).getVersion();
+               }
+       }
+
+       /**
+        * Close the connection and release acquired resources.
+        * 
+        * @return TRUE if everything was closed properly, FALSE if the connection
+        *         was broken (in all cases, resources are released)
+        */
+       public boolean close() {
+               boolean broken = false;
+
+               try {
+                       sendBlock();
+                       broken = out.checkError();
+               } catch (IOException e) {
+                       broken = true;
+               }
+
+               try {
+                       s.close();
+               } catch (IOException e) {
+                       e.printStackTrace();
+                       broken = true;
+                       try {
+                               in.close();
+                       } catch (IOException ee) {
+                       }
+                       out.close();
+               }
+
+               s = null;
+               in = null;
+               out = null;
+
+               return !broken;
+       }
+
+       /**
+        * Sends commands to the remote server. Do <b>NOT</b> sends the end-of-block
+        * marker.
+        * 
+        * @param data
+        *            the data to send
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       protected void send(String data) throws IOException {
+               if (data != null) {
+                       out.write(data);
+               }
+
+               out.write("\n");
+
+               if (out.checkError())
+                       throw new IOException();
+       }
+
+       /**
+        * Sends an end-of-block marker to the remote server.
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       public void sendBlock() throws IOException {
+               sendBlock((List<String>) null);
+       }
+
+       /**
+        * Sends commands to the remote server, then sends an end-of-block marker.
+        * 
+        * @param data
+        *            the data to send
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       public void sendLine(String data) throws IOException {
+               sendBlock(Arrays.asList(new String[] { data }));
+       }
+
+       /**
+        * Sends commands to the remote server, then sends an end-of-block marker.
+        * 
+        * @param data
+        *            the data to send
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       public void sendBlock(List<String> data) throws IOException {
+               if (data != null) {
+                       for (String dataLine : data) {
+                               send(dataLine);
+                       }
+               }
+
+               send(EOB);
+       }
+
+       /**
+        * Sends commands to the remote server, then sends an end-of-block marker.
+        * 
+        * @param verb
+        *            the {@link Verb} to send
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       public void sendCommand(Command.Verb verb) throws IOException {
+               sendCommand(verb, null);
+       }
+
+       /**
+        * Sends commands to the remote server, then sends an end-of-block marker.
+        * 
+        * @param verb
+        *            the data to send
+        * 
+        * @param param
+        *            the parameter for this command if any
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       public void sendCommand(Command.Verb verb, String param) throws IOException {
+               sendLine(new Command(verb, param, CURRENT_VERSION).toString());
+       }
+
+       /**
+        * Read a line from the remote server.
+        * 
+        * Do <b>NOT</b> read until the end-of-block marker, and can return said
+        * block without conversion.
+        * 
+        * @return the read line
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       protected String receive() throws IOException {
+               String line = in.readLine();
+               return line;
+       }
+
+       /**
+        * Read lines from the remote server until the end-of-block ("\0\n") marker
+        * is detected.
+        * 
+        * @return the read lines without the end marker, or NULL if nothing more to
+        *         read
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       public List<String> receiveBlock() throws IOException {
+               List<String> result = new LinkedList<String>();
+
+               String line = receive();
+               for (; line != null && !line.equals(EOB); line = receive()) {
+                       result.add(line);
+               }
+
+               if (line == null)
+                       return null;
+
+               return result;
+       }
+
+       /**
+        * Read a line from the remote server then read until the next end-of-block
+        * marker.
+        * 
+        * @return the parsed line, or NULL if nothing more to read
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       public String receiveLine() throws IOException {
+               List<String> lines = receiveBlock();
+
+               if (lines.size() > 0)
+                       return lines.get(0);
+
+               return null;
+       }
+
+       /**
+        * Read a line from the remote server and convert it to a {@link Command},
+        * then read until the next end-of-block marker.
+        * 
+        * @return the parsed {@link Command}
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       public Command receiveCommand() throws IOException {
+               String line = receive();
+               Command cmd = new Command(line, version);
+               receiveBlock();
+               return cmd;
+       }
+
+       @Override
+       public String toString() {
+               String source = "[not connected]";
+               InetAddress iadr = s.getInetAddress();
+               if (iadr != null)
+                       source = iadr.getHostName();
+
+               return getLabel() + " (" + source + ")";
+       }
+}
diff --git a/src/be/nikiroo/jvcard/remote/Sync.java b/src/be/nikiroo/jvcard/remote/Sync.java
new file mode 100644 (file)
index 0000000..dbf0324
--- /dev/null
@@ -0,0 +1,365 @@
+package be.nikiroo.jvcard.remote;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.InvalidParameterException;
+import java.util.List;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+import be.nikiroo.jvcard.Card;
+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;
+
+/**
+ * This class will synchronise {@link Card}s between a local instance an a
+ * remote jVCard server.
+ * 
+ * @author niki
+ *
+ */
+public class Sync {
+       /** The time in ms after which we declare that 2 timestamps are different */
+       static private final int GRACE_TIME = 2000;
+
+       /** Directory where to store local cache of remote {@link Card}s. */
+       static private File cacheDir;
+
+       /**
+        * Directory where to store cache of remote {@link Card}s without
+        * modifications since the last synchronisation.
+        */
+       static private File cacheDirOrig;
+       /** Directory where to store timestamps for files in cacheDirOrig */
+       static private File cacheDirOrigTS;
+
+       static private boolean autoSync;
+       private String host;
+       private int port;
+
+       /** Resource name on the remote server. */
+       private String name;
+
+       /**
+        * Create a new {@link Sync} object, ready to operate for the given resource
+        * on the given server.
+        * 
+        * <p>
+        * Note that the format used is the standard "host:port_number/file", with
+        * an optional <tt>jvcard://</tt> prefix.
+        * </p>
+        * 
+        * <p>
+        * E.g.: <tt>jvcard://localhost:4444/family.vcf</tt>
+        * </p>
+        * 
+        * @param url
+        *            the server and port to contact, optionally prefixed with
+        *            <tt>jvcard://</tt>
+        * 
+        * @throws InvalidParameterException
+        *             if the remote configuration file <tt>remote.properties</tt>
+        *             cannot be accessed or if the cache directory cannot be used
+        */
+       public Sync(String url) {
+               if (cacheDir == null) {
+                       config();
+               }
+
+               try {
+                       url = url.replace("jvcard://", "");
+                       int indexSl = url.indexOf('/');
+                       this.name = url.substring(indexSl + 1);
+                       url = url.substring(0, indexSl);
+                       this.host = url.split("\\:")[0];
+                       this.port = Integer.parseInt(url.split("\\:")[1]);
+               } catch (Exception e) {
+                       throw new InvalidParameterException(
+                                       "the given parameter was not a valid HOST:PORT value: "
+                                                       + url);
+               }
+       }
+
+       /**
+        * Create a new {@link Sync} object, ready to operate on the given server.
+        * 
+        * 
+        * @param host
+        *            the server to contact
+        * @param port
+        *            the port to use
+        * @param name
+        *            the resource name to synchronise to
+        */
+       public Sync(String host, int port, String name) {
+               this.host = host;
+               this.port = port;
+               this.name = name;
+       }
+
+       /**
+        * Check if the synchronisation is available for this resource.
+        * 
+        * @return TRUE if it is possible to contact the remote server and that this
+        *         server has the resource available
+        */
+       public boolean isAvailable() {
+               try {
+                       SimpleSocket s = new SimpleSocket(new Socket(host, port),
+                                       "check avail client");
+                       s.open(true);
+                       s.sendCommand(Verb.LIST);
+                       List<String> timestampedFiles = s.receiveBlock();
+                       s.close();
+
+                       for (String timestampedFile : timestampedFiles) {
+                               String file = timestampedFile.substring(StringUtils.fromTime(0)
+                                               .length() + 1);
+                               if (file.equals(name)) {
+                                       return true;
+                               }
+                       }
+               } catch (Exception e) {
+               }
+
+               return false;
+       }
+
+       // return: synced or not
+       public boolean sync(Card card, boolean force) throws UnknownHostException,
+                       IOException {
+
+               long tsOriginal = getLastModified();
+
+               // 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 false;
+               }
+
+               SimpleSocket s = new SimpleSocket(new Socket(host, port), "sync client");
+
+               // get the server time stamp
+               long tsServer = -1;
+               try {
+                       s.open(true);
+                       s.sendCommand(Verb.LIST);
+                       List<String> timestampedFiles = s.receiveBlock();
+
+                       for (String timestampedFile : timestampedFiles) {
+                               String file = timestampedFile.substring(StringUtils.fromTime(0)
+                                               .length() + 1);
+                               if (file.equals(name)) {
+                                       tsServer = StringUtils.toTime(timestampedFile.substring(0,
+                                                       StringUtils.fromTime(0).length()));
+                                       break;
+                               }
+                       }
+               } catch (IOException e) {
+                       s.close();
+                       throw e;
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       s.close();
+                       return false;
+               }
+
+               // Error cases:
+               // - file not preset neither in cache nor on server
+               // - remote < previous
+               if ((tsServer == -1 && tsOriginal == -1)
+                               || (tsServer != -1 && tsOriginal != -1 && ((tsOriginal - tsServer) > GRACE_TIME))) {
+                       throw new IOException(
+                                       "The timestamps between server and client are invalid");
+               }
+
+               // Check changes
+               boolean serverChanges = (tsServer - tsOriginal) > GRACE_TIME;
+               boolean localChanges = false;
+               if (tsOriginal != -1) {
+                       Card local = new Card(getCache(cacheDir), Format.VCard21);
+                       Card original = new Card(getCache(cacheDirOrig), Format.VCard21);
+                       localChanges = !local.isEquals(original);
+               }
+
+               Verb action = null;
+
+               // Sync to server if:
+               if (localChanges) {
+                       // TODO: sync instead (with PUT)
+                       action = Verb.POST;
+               }
+
+               // Sync from server if
+               if (serverChanges && localChanges) {
+                       // TODO
+                       throw new IOException("Sync operation not supported yet :(");
+               }
+
+               // PUT the whole file if:
+               if (tsServer == -1) {
+                       action = Verb.POST;
+               }
+
+               // GET the whole file if:
+               if (tsOriginal == -1 || serverChanges) {
+                       action = Verb.GET;
+               }
+
+               System.err.println("remote: " + (tsServer / 1000) % 1000 + " ("
+                               + tsServer + ")");
+               System.err.println("previous: " + (tsOriginal / 1000) % 1000 + " ("
+                               + tsOriginal + ")");
+               System.err.println("local changes: " + localChanges);
+               System.err.println("server changes: " + serverChanges);
+               System.err.println("choosen action: " + action);
+
+               if (action != null) {
+                       switch (action) {
+                       case GET:
+                               s.sendCommand(Verb.GET, name);
+                               List<String> data = s.receiveBlock();
+                               setLastModified(data.remove(0));
+                               Card server = new Card(Parser.parse(data, Format.VCard21));
+                               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));
+                               break;
+                       default:
+                               // TODO
+                               throw new IOException(action
+                                               + " operation not supported yet :(");
+                       }
+               }
+
+               s.close();
+
+               return true;
+       }
+
+       /**
+        * Return the requested cache for the current resource.
+        * 
+        * @param dir
+        *            the cache to use
+        *
+        * @return the cached {@link File}
+        */
+       private File getCache(File dir) {
+               return new File(dir.getPath() + File.separator + name);
+       }
+
+       /**
+        * Return the cached {@link File} corresponding to the current resource.
+        * 
+        * @return the cached {@link File}
+        */
+       public File getCache() {
+               return new File(cacheDir.getPath() + File.separator + name);
+       }
+
+       /**
+        * Get the last modified date of the current resource's original cached
+        * file, that is, the time the server reported as the "last modified time"
+        * when this resource was transfered.
+        * 
+        * @return the last modified time from the server back when this resource
+        *         was transfered
+        */
+       public long getLastModified() {
+               try {
+                       BufferedReader in = new BufferedReader(new InputStreamReader(
+                                       new FileInputStream(cacheDirOrigTS.getPath()
+                                                       + File.separator + name)));
+                       String line = in.readLine();
+                       in.close();
+
+                       return StringUtils.toTime(line);
+               } catch (FileNotFoundException e) {
+                       return -1;
+               } catch (IOException e) {
+                       return -1;
+               }
+       }
+
+       /**
+        * Set the last modified date of the current resource's original cached
+        * file, that is, the time the server reported as the "last modified time"
+        * when this resource was transfered.
+        * 
+        * @param time
+        *            the last modified time from the server back when this resource
+        *            was transfered
+        */
+       public void setLastModified(String time) {
+               try {
+                       BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
+                                       new FileOutputStream(cacheDirOrigTS.getPath()
+                                                       + File.separator + name)));
+                       out.append(time);
+                       out.newLine();
+                       out.close();
+               } catch (FileNotFoundException e) {
+                       e.printStackTrace();
+               } catch (IOException e) {
+                       e.printStackTrace();
+               }
+       }
+
+       /**
+        * Configure the synchronisation mechanism (cache, auto update...).
+        * 
+        * @throws InvalidParameterException
+        *             if the remote configuration file <tt>remote.properties</tt>
+        *             cannot be accessed or if the cache directory cannot be used
+        */
+       static private void config() {
+               String dir = null;
+               ResourceBundle bundle = Bundles.getBundle("remote");
+
+               try {
+                       dir = bundle.getString("CLIENT_CACHE_DIR").trim();
+
+                       cacheDir = new File(dir + File.separator + "current");
+                       cacheDir.mkdir();
+                       cacheDirOrig = new File(dir + File.separator + "original");
+                       cacheDirOrig.mkdir();
+                       cacheDirOrigTS = new File(dir + File.separator + "timestamps");
+                       cacheDirOrigTS.mkdir();
+
+                       if (!cacheDir.exists() || !cacheDirOrig.exists()) {
+                               throw new IOException("Cannot open or create cache store at: "
+                                               + dir);
+                       }
+
+                       String autoStr = bundle.getString("CLIENT_AUTO_SYNC");
+                       if (autoStr != null && autoStr.trim().equalsIgnoreCase("true")) {
+                               autoSync = true;
+                       }
+
+               } catch (MissingResourceException e) {
+                       throw new InvalidParameterException(
+                                       "Cannot access remote.properties configuration file");
+               } catch (Exception e) {
+                       throw new InvalidParameterException(
+                                       "Cannot open or create cache store at: " + dir);
+               }
+       }
+}
diff --git a/src/be/nikiroo/jvcard/resources/remote.properties b/src/be/nikiroo/jvcard/resources/remote.properties
new file mode 100644 (file)
index 0000000..d6c9e72
--- /dev/null
@@ -0,0 +1,19 @@
+# remote configuration (client and server)
+# 
+
+###############
+### Server: ###
+###############
+
+# when starting as a jVCard remote server, where to look for data:
+SERVER_DATA_PATH = /tmp/server/
+
+###############
+### Client: ###
+###############
+
+# when loading "jvcard://" links, where to save cache files
+CLIENT_CACHE_DIR = /tmp/client/ 
+
+# "true" to automatically synchronise remote cards
+CLIENT_AUTO_SYNC = true 
index 6f12a2b7dba4c161daa1f6a029ea89c070ee9522..bf6ab4ba2c589a026d0285cde03fe8e397dc9f2f 100644 (file)
@@ -3,12 +3,16 @@ package be.nikiroo.jvcard.tui;
 import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Field;
+import java.net.Socket;
 import java.nio.charset.Charset;
 import java.util.LinkedList;
 import java.util.List;
 
+import be.nikiroo.jvcard.Card;
 import be.nikiroo.jvcard.i18n.Trans;
 import be.nikiroo.jvcard.i18n.Trans.StringId;
+import be.nikiroo.jvcard.remote.Command.Verb;
+import be.nikiroo.jvcard.remote.SimpleSocket;
 import be.nikiroo.jvcard.resources.Bundles;
 import be.nikiroo.jvcard.tui.panes.FileList;
 
@@ -24,6 +28,11 @@ import com.googlecode.lanterna.input.KeyStroke;
  *
  */
 public class Main {
+       // TODO: move Main to be.nikiroo.jvcard, use introspection to load the other
+       // main classes, allow the 3 programs to run from this new Main
+       // also requires StringUtils/... in a new package
+       private int TODO;
+
        public static final String APPLICATION_TITLE = "jVcard";
        public static final String APPLICATION_VERSION = "1.0-beta2-dev";
 
@@ -93,7 +102,8 @@ public class Main {
                                                                + "\t--noutf: force non-utf8 mode if you need it\n"
                                                                + "\t--config DIRECTORY: force the given directory as a CONFIG_DIR\n"
                                                                + "everyhing else is either a file to open or a directory to open\n"
-                                                               + "(we will only open 1st level files in given directories)\n");
+                                                               + "(we will only open 1st level files in given directories)\n"
+                                                               + "('jvcard://hostname:8888/file' links -- or without 'file' -- are also ok)\n");
                                return;
                        } else if (!noMoreParams && arg.equals("--tui")) {
                                textMode = true;
@@ -131,6 +141,8 @@ public class Main {
                        files.addAll(open("."));
                }
 
+               // TODO error case when no file
+
                Window win = new MainWindow(new FileList(files));
 
                try {
@@ -153,18 +165,61 @@ public class Main {
        static private List<String> open(String path) {
                List<String> files = new LinkedList<String>();
 
-               File file = new File(path);
-               if (file.exists()) {
-                       if (file.isDirectory()) {
-                               for (File subfile : file.listFiles()) {
-                                       if (!subfile.isDirectory())
-                                               files.add(subfile.getAbsolutePath());
-                               }
+               if (path != null && path.startsWith("jvcard://")) {
+                       if (path.endsWith("/")) {
+                               files.addAll(list(path));
                        } else {
-                               files.add(file.getAbsolutePath());
+                               files.add(path);
                        }
                } else {
-                       System.err.println("File or directory not found: \"" + path + "\"");
+                       File file = new File(path);
+                       if (file.exists()) {
+                               if (file.isDirectory()) {
+                                       for (File subfile : file.listFiles()) {
+                                               if (!subfile.isDirectory())
+                                                       files.add(subfile.getAbsolutePath());
+                                       }
+                               } else {
+                                       files.add(file.getAbsolutePath());
+                               }
+                       } else {
+                               System.err.println("File or directory not found: \"" + path
+                                               + "\"");
+                       }
+               }
+
+               return files;
+       }
+
+       /**
+        * List all the available {@link Card}s on the given network location (which
+        * is expected to be a jVCard remote server, obviously).
+        * 
+        * @param path
+        *            the jVCard remote server path (e.g.:
+        *            <tt>jvcard://localhost:4444/</tt>)
+        * 
+        * @return the list of {@link Card}s
+        */
+       static private List<String> list(String path) {
+               List<String> files = new LinkedList<String>();
+
+               try {
+                       String host = path.split("\\:")[1].substring(2);
+                       int port = Integer.parseInt(path.split("\\:")[2].replaceAll("/$",
+                                       ""));
+                       SimpleSocket s = new SimpleSocket(new Socket(host, port),
+                                       "sync client");
+                       s.open(true);
+
+                       s.sendCommand(Verb.LIST);
+                       for (String p : s.receiveBlock()) {
+                               files.add(path
+                                               + p.substring(StringUtils.fromTime(0).length() + 1));
+                       }
+                       s.close();
+               } catch (Exception e) {
+                       e.printStackTrace();
                }
 
                return files;
index 75f9352cf92f389060276e67c1983b7f811068c3..bfc7f8b393f7666c2aa9e68ab7a1a0ca32593df6 100644 (file)
@@ -9,6 +9,7 @@ import java.util.List;
 import be.nikiroo.jvcard.Card;
 import be.nikiroo.jvcard.i18n.Trans;
 import be.nikiroo.jvcard.parsers.Format;
+import be.nikiroo.jvcard.remote.Sync;
 import be.nikiroo.jvcard.tui.KeyAction;
 import be.nikiroo.jvcard.tui.KeyAction.DataType;
 import be.nikiroo.jvcard.tui.KeyAction.Mode;
@@ -124,6 +125,7 @@ public class FileList extends MainContentList {
        }
 
        static private Card getCard(String input) throws IOException {
+               boolean remote = false;
                Format format = Format.Abook;
                String ext = input;
                if (ext.contains(".")) {
@@ -133,9 +135,21 @@ public class FileList extends MainContentList {
                        }
                }
 
+               if (input.contains("://")) {
+                       format = Format.VCard21;
+                       remote = true;
+               }
+
                Card card = null;
                try {
-                       card = new Card(new File(input), format);
+                       if (remote) {
+                               Sync sync = new Sync(input);
+                               card = new Card(sync.getCache(), format);
+                               card.setRemote(true);
+                               sync.sync(card, false);
+                       } else {
+                               card = new Card(new File(input), format);
+                       }
                } catch (IOException ioe) {
                        ioe.printStackTrace();
                        throw ioe;