--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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 + ")";
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+# 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
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;
*
*/
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";
+ "\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;
files.addAll(open("."));
}
+ // TODO error case when no file
+
Window win = new MainWindow(new FileList(files));
try {
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;
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;
}
static private Card getCard(String input) throws IOException {
+ boolean remote = false;
Format format = Format.Abook;
String ext = input;
if (ext.contains(".")) {
}
}
+ 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;