29344d1e33db3ad6a01341ba56d212347280c930
[jvcard.git] / src / be / nikiroo / jvcard / remote / Sync.java
1 package be.nikiroo.jvcard.remote;
2
3 import java.io.BufferedReader;
4 import java.io.BufferedWriter;
5 import java.io.File;
6 import java.io.FileInputStream;
7 import java.io.FileNotFoundException;
8 import java.io.FileOutputStream;
9 import java.io.IOException;
10 import java.io.InputStreamReader;
11 import java.io.OutputStreamWriter;
12 import java.net.Socket;
13 import java.net.UnknownHostException;
14 import java.security.InvalidParameterException;
15 import java.util.HashMap;
16 import java.util.LinkedList;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.MissingResourceException;
20 import java.util.ResourceBundle;
21
22 import be.nikiroo.jvcard.Card;
23 import be.nikiroo.jvcard.Contact;
24 import be.nikiroo.jvcard.Data;
25 import be.nikiroo.jvcard.parsers.Format;
26 import be.nikiroo.jvcard.parsers.Vcard21Parser;
27 import be.nikiroo.jvcard.resources.Bundles;
28 import be.nikiroo.jvcard.resources.StringUtils;
29
30 /**
31 * This class will synchronise {@link Card}s between a local instance an a
32 * remote jVCard server.
33 *
34 * @author niki
35 *
36 */
37 public class Sync {
38 /** The time in ms after which we declare that 2 timestamps are different */
39 static private final int GRACE_TIME = 2000;
40
41 /** Directory where to store local cache of remote {@link Card}s. */
42 static private File cacheDir;
43
44 /**
45 * Directory where to store cache of remote {@link Card}s without
46 * modifications since the last synchronisation.
47 */
48 static private File cacheDirOrig;
49 /** Directory where to store timestamps for files in cacheDirOrig */
50 static private File cacheDirOrigTS;
51
52 static private boolean autoSync;
53 private String host;
54 private int port;
55
56 /** Resource name on the remote server. */
57 private String name;
58
59 /**
60 * Create a new {@link Sync} object, ready to operate for the given resource
61 * on the given server.
62 *
63 * <p>
64 * Note that the format used is the standard "host:port_number/file", with
65 * an optional <tt>jvcard://</tt> prefix.
66 * </p>
67 *
68 * <p>
69 * E.g.: <tt>jvcard://localhost:4444/family.vcf</tt>
70 * </p>
71 *
72 * @param url
73 * the server and port to contact, optionally prefixed with
74 * <tt>jvcard://</tt>
75 *
76 * @throws InvalidParameterException
77 * if the remote configuration file <tt>remote.properties</tt>
78 * cannot be accessed or if the cache directory cannot be used
79 */
80 public Sync(String url) {
81 if (cacheDir == null) {
82 config();
83 }
84
85 try {
86 url = url.replace("jvcard://", "");
87 int indexSl = url.indexOf('/');
88 this.name = url.substring(indexSl + 1);
89 url = url.substring(0, indexSl);
90 this.host = url.split("\\:")[0];
91 this.port = Integer.parseInt(url.split("\\:")[1]);
92 } catch (Exception e) {
93 throw new InvalidParameterException(
94 "the given parameter was not a valid HOST:PORT value: "
95 + url);
96 }
97 }
98
99 /**
100 * Create a new {@link Sync} object, ready to operate on the given server.
101 *
102 *
103 * @param host
104 * the server to contact
105 * @param port
106 * the port to use
107 * @param name
108 * the resource name to synchronise to
109 */
110 public Sync(String host, int port, String name) {
111 this.host = host;
112 this.port = port;
113 this.name = name;
114 }
115
116 /**
117 * Check if the synchronisation is available for this resource.
118 *
119 * @return TRUE if it is possible to contact the remote server and that this
120 * server has the resource available
121 */
122 public boolean isAvailable() {
123 try {
124 SimpleSocket s = new SimpleSocket(new Socket(host, port),
125 "check avail client");
126 s.open(true);
127 s.sendCommand(Command.LIST_CARD);
128 List<String> timestampedFiles = s.receiveBlock();
129 s.close();
130
131 for (String timestampedFile : timestampedFiles) {
132 String file = timestampedFile.substring(StringUtils.fromTime(0)
133 .length() + 1);
134 if (file.equals(name)) {
135 return true;
136 }
137 }
138 } catch (Exception e) {
139 }
140
141 return false;
142 }
143
144 // return: synced or not
145 // TODO jDoc
146 public Card sync(boolean force) throws UnknownHostException, IOException {
147
148 long tsOriginal = getLastModified();
149
150 Card local = new Card(getCache(cacheDir), Format.VCard21);
151 local.setRemote(true);
152
153 // do NOT update unless we are in autoSync or forced mode or we don't
154 // have the file on cache
155 if (!autoSync && !force && tsOriginal != -1) {
156 return local;
157 }
158
159 SimpleSocket s = new SimpleSocket(new Socket(host, port), "sync client");
160
161 // get the server time stamp
162 long tsServer = -1;
163 try {
164 s.open(true);
165 s.sendCommand(Command.LIST_CARD);
166 List<String> timestampedFiles = s.receiveBlock();
167
168 for (String timestampedFile : timestampedFiles) {
169 String file = timestampedFile.substring(StringUtils.fromTime(0)
170 .length() + 1);
171 if (file.equals(name)) {
172 tsServer = StringUtils.toTime(timestampedFile.substring(0,
173 StringUtils.fromTime(0).length()));
174 break;
175 }
176 }
177
178 // Error cases:
179 // - file not preset neither in cache nor on server
180 // - remote < previous
181 if ((tsServer == -1 && tsOriginal == -1)
182 || (tsServer != -1 && tsOriginal != -1 && ((tsOriginal - tsServer) > GRACE_TIME))) {
183 throw new IOException(
184 "The timestamps between server and client are invalid");
185 }
186
187 // Check changes
188 boolean serverChanges = (tsServer - tsOriginal) > GRACE_TIME;
189 boolean localChanges = false;
190 Card original = null;
191 if (tsOriginal != -1) {
192 original = new Card(getCache(cacheDirOrig), Format.VCard21);
193 localChanges = !local.isEquals(original, true);
194 }
195
196 Command action = null;
197
198 // Sync to server if:
199 if (localChanges) {
200 action = Command.PUT_CARD;
201 }
202
203 // Sync from server if:
204 if (serverChanges) {
205 action = Command.HASH_CONTACT;
206 }
207
208 // Sync from/to server if
209 if (serverChanges && localChanges) {
210 // TODO
211 action = Command.HELP;
212 }
213
214 // PUT the whole file if:
215 if (tsServer == -1) {
216 action = Command.POST_CARD;
217 }
218
219 // GET the whole file if:
220 if (tsOriginal == -1) {
221 action = Command.GET_CARD;
222 }
223
224 System.err.println("remote: " + (tsServer / 1000) % 1000 + " ("
225 + tsServer + ")");
226 System.err.println("previous: " + (tsOriginal / 1000) % 1000 + " ("
227 + tsOriginal + ")");
228 System.err.println("local changes: " + localChanges);
229 System.err.println("server changes: " + serverChanges);
230 System.err.println("choosen action: " + action);
231
232 if (action != null) {
233 s.sendCommand(Command.SELECT, name);
234 if (tsServer != StringUtils.toTime(s.receiveLine())) {
235 System.err.println("DEBUG: it changed. retry.");
236 s.sendCommand(Command.SELECT);
237 s.close();
238 return sync(force);
239 }
240
241 switch (action) {
242 case GET_CARD:
243 s.sendCommand(Command.GET_CARD);
244 List<String> data = s.receiveBlock();
245 setLastModified(data.remove(0));
246 Card server = new Card(Vcard21Parser.parseContact(data));
247 local.replaceListContent(server);
248
249 if (local.isDirty())
250 local.save();
251 local.saveAs(getCache(cacheDirOrig), Format.VCard21);
252 break;
253 case POST_CARD:
254 s.sendCommand(Command.POST_CARD);
255 s.sendBlock(Vcard21Parser.toStrings(local));
256 local.saveAs(getCache(cacheDirOrig), Format.VCard21);
257 setLastModified(s.receiveLine());
258 break;
259 case PUT_CARD: {
260 List<Contact> added = new LinkedList<Contact>();
261 List<Contact> removed = new LinkedList<Contact>();
262 List<Contact> from = new LinkedList<Contact>();
263 List<Contact> to = new LinkedList<Contact>();
264 original.compare(local, added, removed, from, to);
265
266 s.sendCommand(Command.PUT_CARD);
267
268 for (Contact c : removed) {
269 s.sendCommand(Command.DELETE_CONTACT, c.getId());
270 }
271 for (Contact c : added) {
272 s.sendCommand(Command.POST_CONTACT, c.getId());
273 s.sendBlock(Vcard21Parser.toStrings(c, -1));
274 }
275 if (from.size() > 0) {
276 for (int index = 0; index < from.size(); index++) {
277 Contact f = from.get(index);
278 Contact t = to.get(index);
279
280 List<Data> subadded = new LinkedList<Data>();
281 List<Data> subremoved = new LinkedList<Data>();
282 f.compare(t, subadded, subremoved, subremoved,
283 subadded);
284 s.sendCommand(Command.PUT_CONTACT, name);
285 for (Data d : subremoved) {
286 s.sendCommand(Command.DELETE_DATA,
287 d.getContentState());
288 }
289 for (Data d : subadded) {
290 s.sendCommand(Command.POST_DATA,
291 d.getContentState());
292 s.sendBlock(Vcard21Parser.toStrings(d));
293 }
294 }
295 }
296
297 local.saveAs(getCache(cacheDirOrig), Format.VCard21);
298 s.sendCommand(Command.PUT_CARD);
299 setLastModified(s.receiveLine());
300
301 break;
302 }
303 case HASH_CONTACT: {
304 s.sendCommand(Command.PUT_CARD);
305
306 s.sendCommand(Command.LIST_CONTACT);
307 Map<String, String> remote = new HashMap<String, String>();
308 for (String line : s.receiveBlock()) {
309 int indexSp = line.indexOf(" ");
310 String hash = line.substring(0, indexSp);
311 String uid = line.substring(indexSp + 1);
312
313 remote.put(uid, hash);
314 }
315
316 List<Contact> deleted = new LinkedList<Contact>();
317 List<Contact> changed = new LinkedList<Contact>();
318 List<String> added = new LinkedList<String>();
319
320 for (Contact c : local) {
321 String hash = remote.get(c.getId());
322 if (hash == null) {
323 deleted.add(c);
324 } else if (!hash.equals(c.getContentState())) {
325 changed.add(c);
326 }
327 }
328
329 for (String uid : remote.keySet()) {
330 if (local.getById(uid) == null)
331 added.add(uid);
332 }
333
334 // process:
335
336 for (Contact c : deleted) {
337 c.delete();
338 }
339
340 for (String uid : added) {
341 s.sendCommand(Command.GET_CONTACT, uid);
342 for (Contact cc : Vcard21Parser.parseContact(s
343 .receiveBlock())) {
344 local.add(cc);
345 }
346 }
347
348 for (Contact c : changed) {
349 c.delete();
350 s.sendCommand(Command.GET_CONTACT, c.getId());
351 for (Contact cc : Vcard21Parser.parseContact(s
352 .receiveBlock())) {
353 local.add(cc);
354 }
355 }
356
357 local.save();
358 local.saveAs(getCache(cacheDirOrig), Format.VCard21);
359 s.sendCommand(Command.PUT_CARD);
360 setLastModified(s.receiveLine());
361 break;
362 }
363 default:
364 // TODO
365 throw new IOException(action
366 + " operation not supported yet :(");
367 }
368
369 s.sendCommand(Command.SELECT);
370 }
371 } catch (IOException e) {
372 throw e;
373 } catch (Exception e) {
374 e.printStackTrace();
375 return local;
376 } finally {
377 s.close();
378 }
379
380 return local;
381 }
382
383 /**
384 * Return the requested cache for the current resource.
385 *
386 * @param dir
387 * the cache to use
388 *
389 * @return the cached {@link File}
390 */
391 private File getCache(File dir) {
392 return new File(dir.getPath() + File.separator + name);
393 }
394
395 /**
396 * Return the cached {@link File} corresponding to the current resource.
397 *
398 * @return the cached {@link File}
399 */
400 public File getCache() {
401 return new File(cacheDir.getPath() + File.separator + name);
402 }
403
404 /**
405 * Get the last modified date of the current resource's original cached
406 * file, that is, the time the server reported as the "last modified time"
407 * when this resource was transfered.
408 *
409 * @return the last modified time from the server back when this resource
410 * was transfered
411 */
412 public long getLastModified() {
413 try {
414 BufferedReader in = new BufferedReader(new InputStreamReader(
415 new FileInputStream(cacheDirOrigTS.getPath()
416 + File.separator + name)));
417 String line = in.readLine();
418 in.close();
419
420 return StringUtils.toTime(line);
421 } catch (FileNotFoundException e) {
422 return -1;
423 } catch (Exception e) {
424 return -1;
425 }
426 }
427
428 /**
429 * Set the last modified date of the current resource's original cached
430 * file, that is, the time the server reported as the "last modified time"
431 * when this resource was transfered.
432 *
433 * @param time
434 * the last modified time from the server back when this resource
435 * was transfered
436 */
437 public void setLastModified(String time) {
438 try {
439 BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
440 new FileOutputStream(cacheDirOrigTS.getPath()
441 + File.separator + name)));
442 out.append(time);
443 out.newLine();
444 out.close();
445 } catch (FileNotFoundException e) {
446 e.printStackTrace();
447 } catch (IOException e) {
448 e.printStackTrace();
449 }
450 }
451
452 /**
453 * Configure the synchronisation mechanism (cache, auto update...).
454 *
455 * @throws InvalidParameterException
456 * if the remote configuration file <tt>remote.properties</tt>
457 * cannot be accessed or if the cache directory cannot be used
458 */
459 static private void config() {
460 String dir = null;
461 ResourceBundle bundle = Bundles.getBundle("remote");
462
463 try {
464 dir = bundle.getString("CLIENT_CACHE_DIR").trim();
465
466 cacheDir = new File(dir + File.separator + "current");
467 cacheDir.mkdir();
468 cacheDirOrig = new File(dir + File.separator + "original");
469 cacheDirOrig.mkdir();
470 cacheDirOrigTS = new File(dir + File.separator + "timestamps");
471 cacheDirOrigTS.mkdir();
472
473 if (!cacheDir.exists() || !cacheDirOrig.exists()) {
474 throw new IOException("Cannot open or create cache store at: "
475 + dir);
476 }
477
478 String autoStr = bundle.getString("CLIENT_AUTO_SYNC");
479 if (autoStr != null && autoStr.trim().equalsIgnoreCase("true")) {
480 autoSync = true;
481 }
482
483 } catch (MissingResourceException e) {
484 throw new InvalidParameterException(
485 "Cannot access remote.properties configuration file");
486 } catch (Exception e) {
487 throw new InvalidParameterException(
488 "Cannot open or create cache store at: " + dir);
489 }
490 }
491 }