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