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.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import be.nikiroo.jvcard.Card; import be.nikiroo.jvcard.Contact; import be.nikiroo.jvcard.Data; import be.nikiroo.jvcard.parsers.Format; import be.nikiroo.jvcard.parsers.Vcard21Parser; import be.nikiroo.jvcard.resources.Bundles; import be.nikiroo.jvcard.resources.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(Command.LIST_CARD); 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 // TODO jDoc public Card sync(boolean force) throws UnknownHostException, IOException { long tsOriginal = getLastModified(); Card local = new Card(getCache(cacheDir), Format.VCard21); local.setRemote(true); // 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 local; } SimpleSocket s = new SimpleSocket(new Socket(host, port), "sync client"); // get the server time stamp long tsServer = -1; try { s.open(true); s.sendCommand(Command.LIST_CARD); 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; } } // 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; Card original = null; if (tsOriginal != -1) { original = new Card(getCache(cacheDirOrig), Format.VCard21); localChanges = !local.isEquals(original, true); } Command action = null; // Sync to server if: if (localChanges) { action = Command.PUT_CARD; } // Sync from server if: if (serverChanges) { action = Command.HASH_CONTACT; } // Sync from/to server if if (serverChanges && localChanges) { // TODO action = Command.HELP; } // PUT the whole file if: if (tsServer == -1) { action = Command.POST_CARD; } // GET the whole file if: if (tsOriginal == -1) { action = Command.GET_CARD; } 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) { s.sendCommand(Command.SELECT, name); if (tsServer != StringUtils.toTime(s.receiveLine())) { System.err.println("DEBUG: it changed. retry."); s.sendCommand(Command.SELECT); s.close(); return sync(force); } switch (action) { case GET_CARD: s.sendCommand(Command.GET_CARD); List data = s.receiveBlock(); setLastModified(data.remove(0)); Card server = new Card(Vcard21Parser.parseContact(data)); local.replaceListContent(server); if (local.isDirty()) local.save(); local.saveAs(getCache(cacheDirOrig), Format.VCard21); break; case POST_CARD: s.sendCommand(Command.POST_CARD); s.sendBlock(Vcard21Parser.toStrings(local)); local.saveAs(getCache(cacheDirOrig), Format.VCard21); setLastModified(s.receiveLine()); break; case PUT_CARD: { List added = new LinkedList(); List removed = new LinkedList(); List from = new LinkedList(); List to = new LinkedList(); original.compare(local, added, removed, from, to); s.sendCommand(Command.PUT_CARD); for (Contact c : removed) { s.sendCommand(Command.DELETE_CONTACT, c.getId()); } for (Contact c : added) { s.sendCommand(Command.POST_CONTACT, c.getId()); s.sendBlock(Vcard21Parser.toStrings(c, -1)); } if (from.size() > 0) { for (int index = 0; index < from.size(); index++) { Contact f = from.get(index); Contact t = to.get(index); List subadded = new LinkedList(); List subremoved = new LinkedList(); f.compare(t, subadded, subremoved, subremoved, subadded); s.sendCommand(Command.PUT_CONTACT, name); for (Data d : subremoved) { s.sendCommand(Command.DELETE_DATA, d.getContentState()); } for (Data d : subadded) { s.sendCommand(Command.POST_DATA, d.getContentState()); s.sendBlock(Vcard21Parser.toStrings(d)); } } } local.saveAs(getCache(cacheDirOrig), Format.VCard21); s.sendCommand(Command.PUT_CARD); setLastModified(s.receiveLine()); break; } case HASH_CONTACT: { s.sendCommand(Command.PUT_CARD); s.sendCommand(Command.LIST_CONTACT); Map remote = new HashMap(); for (String line : s.receiveBlock()) { int indexSp = line.indexOf(" "); String hash = line.substring(0, indexSp); String uid = line.substring(indexSp + 1); remote.put(uid, hash); } List deleted = new LinkedList(); List changed = new LinkedList(); List added = new LinkedList(); for (Contact c : local) { String hash = remote.get(c.getId()); if (hash == null) { deleted.add(c); } else if (!hash.equals(c.getContentState())) { changed.add(c); } } for (String uid : remote.keySet()) { if (local.getById(uid) == null) added.add(uid); } // process: for (Contact c : deleted) { c.delete(); } for (String uid : added) { s.sendCommand(Command.GET_CONTACT, uid); for (Contact cc : Vcard21Parser.parseContact(s .receiveBlock())) { local.add(cc); } } for (Contact c : changed) { c.delete(); s.sendCommand(Command.GET_CONTACT, c.getId()); for (Contact cc : Vcard21Parser.parseContact(s .receiveBlock())) { local.add(cc); } } local.save(); local.saveAs(getCache(cacheDirOrig), Format.VCard21); s.sendCommand(Command.PUT_CARD); setLastModified(s.receiveLine()); break; } default: // TODO throw new IOException(action + " operation not supported yet :("); } s.sendCommand(Command.SELECT); } } catch (IOException e) { throw e; } catch (Exception e) { e.printStackTrace(); return local; } finally { s.close(); } return local; } /** * 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 (Exception 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); } } }