X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fbe%2Fnikiroo%2Fjvcard%2Fremote%2FSync.java;fp=src%2Fbe%2Fnikiroo%2Fjvcard%2Fremote%2FSync.java;h=dbf03242fb3e82083a6ef601ee50b3edb66ad975;hb=a046fa49cdcaea2ce4eb9104387c66f915205f2d;hp=0000000000000000000000000000000000000000;hpb=1c03abafc3987d93fa682e7b8758e51bed8a4faf;p=jvcard.git 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); + } + } +}