From a046fa49cdcaea2ce4eb9104387c66f915205f2d Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Sun, 13 Mar 2016 14:11:11 +0100 Subject: [PATCH] jvcard remote support (initial commit, not ready for use yet) --- src/be/nikiroo/jvcard/remote/Command.java | 143 +++++++ src/be/nikiroo/jvcard/remote/Server.java | 293 ++++++++++++++ .../nikiroo/jvcard/remote/SimpleSocket.java | 310 +++++++++++++++ src/be/nikiroo/jvcard/remote/Sync.java | 365 ++++++++++++++++++ .../jvcard/resources/remote.properties | 19 + src/be/nikiroo/jvcard/tui/Main.java | 75 +++- src/be/nikiroo/jvcard/tui/panes/FileList.java | 16 +- 7 files changed, 1210 insertions(+), 11 deletions(-) create mode 100644 src/be/nikiroo/jvcard/remote/Command.java create mode 100644 src/be/nikiroo/jvcard/remote/Server.java create mode 100644 src/be/nikiroo/jvcard/remote/SimpleSocket.java create mode 100644 src/be/nikiroo/jvcard/remote/Sync.java create mode 100644 src/be/nikiroo/jvcard/resources/remote.properties diff --git a/src/be/nikiroo/jvcard/remote/Command.java b/src/be/nikiroo/jvcard/remote/Command.java new file mode 100644 index 0000000..0fb0a73 --- /dev/null +++ b/src/be/nikiroo/jvcard/remote/Command.java @@ -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 index 0000000..2ead82b --- /dev/null +++ b/src/be/nikiroo/jvcard/remote/Server.java @@ -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. + * + *

+ * It is NOT 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 + * NOT enable such a server to be accessible from internet. This is not + * safe. Use a ssh/openssl tunnel or similar. + *

+ * + * @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 clients = new LinkedList(); + + 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 doGetCard(String name) throws IOException { + List lines = new LinkedList(); + + 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 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 index 0000000..e35189b --- /dev/null +++ b/src/be/nikiroo/jvcard/remote/SimpleSocket.java @@ -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 MUST 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 NOT 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) 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 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 NOT 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 receiveBlock() throws IOException { + List result = new LinkedList(); + + 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 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 index 0000000..dbf0324 --- /dev/null +++ b/src/be/nikiroo/jvcard/remote/Sync.java @@ -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. + * + *

+ * Note that the format used is the standard "host:port_number/file", with + * an optional jvcard:// prefix. + *

+ * + *

+ * E.g.: jvcard://localhost:4444/family.vcf + *

+ * + * @param url + * the server and port to contact, optionally prefixed with + * jvcard:// + * + * @throws InvalidParameterException + * if the remote configuration file remote.properties + * 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 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 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 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 remote.properties + * 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 index 0000000..d6c9e72 --- /dev/null +++ b/src/be/nikiroo/jvcard/resources/remote.properties @@ -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 diff --git a/src/be/nikiroo/jvcard/tui/Main.java b/src/be/nikiroo/jvcard/tui/Main.java index 6f12a2b..bf6ab4b 100644 --- a/src/be/nikiroo/jvcard/tui/Main.java +++ b/src/be/nikiroo/jvcard/tui/Main.java @@ -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 open(String path) { List files = new LinkedList(); - 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.: + * jvcard://localhost:4444/) + * + * @return the list of {@link Card}s + */ + static private List list(String path) { + List files = new LinkedList(); + + 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; diff --git a/src/be/nikiroo/jvcard/tui/panes/FileList.java b/src/be/nikiroo/jvcard/tui/panes/FileList.java index 75f9352..bfc7f8b 100644 --- a/src/be/nikiroo/jvcard/tui/panes/FileList.java +++ b/src/be/nikiroo/jvcard/tui/panes/FileList.java @@ -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; -- 2.27.0