1 package be
.nikiroo
.jvcard
.remote
;
3 import java
.io
.BufferedReader
;
4 import java
.io
.BufferedWriter
;
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
;
20 import be
.nikiroo
.jvcard
.Card
;
21 import be
.nikiroo
.jvcard
.Contact
;
22 import be
.nikiroo
.jvcard
.Data
;
23 import be
.nikiroo
.jvcard
.launcher
.CardResult
;
24 import be
.nikiroo
.jvcard
.launcher
.CardResult
.MergeCallback
;
25 import be
.nikiroo
.jvcard
.parsers
.Format
;
26 import be
.nikiroo
.jvcard
.parsers
.Vcard21Parser
;
27 import be
.nikiroo
.jvcard
.remote
.SimpleSocket
.BlockAppendable
;
28 import be
.nikiroo
.jvcard
.resources
.RemoteBundle
;
29 import be
.nikiroo
.jvcard
.resources
.RemotingOption
;
30 import be
.nikiroo
.utils
.StringUtils
;
33 * This class will synchronise {@link Card}s between a local instance an a
34 * remote jVCard server.
40 /** The time in ms after which we declare that 2 timestamps are different */
41 static private final int GRACE_TIME
= 2001;
43 /** Directory where to store local cache of remote {@link Card}s. */
44 static private File cacheDir
;
47 * Directory where to store cache of remote {@link Card}s without
48 * modifications since the last synchronisation.
50 static private File cacheDirOrig
;
51 /** Directory where to store timestamps for files in cacheDirOrig */
52 static private File cacheDirOrigTS
;
54 static private boolean autoSync
;
58 /** Resource name on the remote server. */
62 * Create a new {@link Sync} object, ready to operate for the given resource
63 * on the given server.
66 * Note that the format used is the standard "host:port_number/file", with
67 * an optional <tt>jvcard://</tt> prefix.
71 * E.g.: <tt>jvcard://localhost:4444/family.vcf</tt>
75 * the server and port to contact, optionally prefixed with
78 * @throws InvalidParameterException
79 * if the remote configuration file <tt>remote.properties</tt>
80 * cannot be accessed or if the cache directory cannot be used
82 public Sync(String url
) {
83 if (cacheDir
== null) {
88 url
= url
.replace("jvcard://", "");
89 int indexSl
= url
.indexOf('/');
90 this.name
= url
.substring(indexSl
+ 1);
91 url
= url
.substring(0, indexSl
);
92 this.host
= url
.split("\\:")[0];
93 this.port
= Integer
.parseInt(url
.split("\\:")[1]);
94 } catch (Exception e
) {
95 throw new InvalidParameterException(
96 "the given parameter was not a valid HOST:PORT value: "
102 * Create a new {@link Sync} object, ready to operate on the given server.
106 * the server to contact
110 * the resource name to synchronise to
112 public Sync(String host
, int port
, String name
) {
119 * Check if the remote server already know about this resource.
121 * @return TRUE if it is possible to contact the remote server and that this
122 * server has the resource available
124 public boolean isAvailable() {
126 SimpleSocket s
= new SimpleSocket(new Socket(host
, port
),
127 "check avail client");
129 s
.sendCommand(Command
.LIST_CARD
);
130 List
<String
> timestampedFiles
= s
.receiveBlock();
133 for (String timestampedFile
: timestampedFiles
) {
134 String file
= timestampedFile
.substring(StringUtils
.fromTime(0)
136 if (file
.equals(name
)) {
140 } catch (Exception e
) {
147 * Synchronise the current resource if needed, then return the locally
148 * cached version of said resource.
151 * A synchronisation is deemed necessary if one of the following is true:
153 * <li><tt>force</tt> is TRUE</li>
154 * <li><tt>CLIENT_AUTO_SYNC</tt> is TRUE in the configuration file</li>
155 * <li>the {@link Card} exists locally but not on the remote server</li>
156 * <li>the {@link Card} exists on the remote server but not locally</li>
161 * force the synchronisation to occur
163 * the {@link MergeCallback} to call in case of conflict
165 * @return the synchronised (or not) {@link Card}
167 * @throws UnknownHostException
168 * in case of server name resolution failure
169 * @throws IOException
170 * in case of IO error
172 public CardResult
sync(boolean force
, MergeCallback callback
)
173 throws UnknownHostException
, IOException
{
174 long tsOriginal
= getLastModified();
176 Card local
= new Card(getCache(cacheDir
), Format
.VCard21
);
178 // do NOT update unless we are in autoSync or forced mode or we don't
179 // have the file on cache
180 if (!autoSync
&& !force
&& tsOriginal
!= -1) {
181 return new CardResult(local
, true, false, false);
184 SimpleSocket s
= new SimpleSocket(new Socket(host
, port
), "sync client");
186 // get the server time stamp
188 boolean serverChanges
= false;
191 s
.sendCommand(Command
.LIST_CARD
);
192 List
<String
> timestampedFiles
= s
.receiveBlock();
194 for (String timestampedFile
: timestampedFiles
) {
195 String file
= timestampedFile
.substring(StringUtils
.fromTime(0)
197 if (file
.equals(name
)) {
198 tsServer
= StringUtils
.toTime(timestampedFile
.substring(0,
199 StringUtils
.fromTime(0).length()));
205 // - file not present neither in cache nor on server
206 // - remote < previous
207 if ((tsServer
== -1 && tsOriginal
== -1)
208 || (tsServer
!= -1 && tsOriginal
!= -1 && ((tsOriginal
- tsServer
) > GRACE_TIME
))) {
209 throw new IOException(
210 "The timestamps between server and client are invalid");
214 serverChanges
= (tsServer
- tsOriginal
) > GRACE_TIME
;
215 boolean localChanges
= false;
216 Card original
= null;
217 if (tsOriginal
!= -1) {
218 original
= new Card(getCache(cacheDirOrig
), Format
.VCard21
);
219 localChanges
= !local
.isEquals(original
, true);
222 Command action
= null;
224 // Sync to server if:
226 action
= Command
.PUT_CARD
;
229 // Sync from server if:
231 action
= Command
.HASH_CONTACT
;
234 // Sync from/to server if
235 if (serverChanges
&& localChanges
) {
236 action
= Command
.HELP
;
239 // POST the whole file if:
240 if (tsServer
== -1) {
241 action
= Command
.POST_CARD
;
244 // GET the whole file if:
245 if (tsOriginal
== -1) {
246 action
= Command
.GET_CARD
;
249 System
.err
.println("remote: " + (tsServer
/ 1000) % 1000 + " ("
251 System
.err
.println("previous: " + (tsOriginal
/ 1000) % 1000 + " ("
253 System
.err
.println("local changes: " + localChanges
);
254 System
.err
.println("server changes: " + serverChanges
);
255 System
.err
.println("choosen action: " + action
);
257 if (action
!= null) {
258 s
.sendCommand(Command
.SELECT
, name
);
259 if (tsServer
!= StringUtils
.toTime(s
.receiveLine())) {
260 System
.err
.println("DEBUG: it changed. retry.");
261 s
.sendCommand(Command
.SELECT
);
263 return sync(force
, callback
);
268 s
.sendCommand(Command
.GET_CARD
);
269 List
<String
> data
= s
.receiveBlock();
270 setLastModified(data
.remove(0));
271 local
.replaceListContent(Vcard21Parser
.parseContact(data
));
275 local
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
279 s
.sendCommand(Command
.POST_CARD
);
280 BlockAppendable app
= s
.createBlockAppendable();
281 Vcard21Parser
.write(app
, local
);
283 local
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
284 setLastModified(s
.receiveLine());
288 String serverLastModifTime
= updateToServer(s
, original
,
291 local
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
293 setLastModified(serverLastModifTime
);
297 String serverLastModifTime
= updateFromServer(s
, local
);
300 local
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
302 setLastModified(serverLastModifTime
);
306 // note: we are holding the server here, so it could throw
307 // us away if we take too long
309 // TODO: check if those files are deleted
310 File mergeF
= File
.createTempFile("contact-merge", ".vcf");
312 .createTempFile("contact-server", ".vcf");
313 original
.saveAs(serverF
, Format
.VCard21
);
315 Card server
= new Card(serverF
, Format
.VCard21
);
316 updateFromServer(s
, server
);
319 server
.saveAs(mergeF
, Format
.VCard21
);
320 Card merge
= new Card(mergeF
, Format
.VCard21
);
321 List
<Contact
> added
= new LinkedList
<Contact
>();
322 List
<Contact
> removed
= new LinkedList
<Contact
>();
323 original
.compare(local
, added
, removed
, removed
, added
);
324 for (Contact c
: removed
)
325 merge
.getById(c
.getId()).delete();
326 for (Contact c
: added
)
327 merge
.add(Vcard21Parser
.clone(c
));
332 if (callback
== null) {
333 throw new IOException(
334 "Conflicting changes detected and merge operation not allowed");
337 merge
= callback
.merge(original
, local
, server
, merge
);
339 throw new IOException(
340 "Conflicting changes detected and merge operation cancelled");
343 // TODO: something like:
344 // String serverLastModifTime = updateToServer(s, original,
346 // ...but without starting with original since it is not
348 s
.sendCommand(Command
.POST_CARD
);
349 BlockAppendable app
= s
.createBlockAppendable();
350 Vcard21Parser
.write(app
, merge
);
352 String serverLastModifTime
= s
.receiveLine();
355 merge
.saveAs(getCache(cacheDir
), Format
.VCard21
);
356 merge
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
358 setLastModified(serverLastModifTime
);
369 s
.sendCommand(Command
.SELECT
);
371 } catch (IOException e
) {
372 return new CardResult(e
);
373 } catch (Exception e
) {
374 return new CardResult(new IOException(e
));
379 return new CardResult(local
, true, true, serverChanges
);
383 * Will update the currently selected {@link Card} on the remote server to
384 * be in the same state as <tt>local</tt>, assuming the server is currently
385 * in <tt>original</tt> state.
388 * the {@link SimpleSocket} to work on, which <b>MUST</b> be in
391 * the original {@link Card} as it was before the client made
394 * the {@link Card} to which state we want the server in
396 * @return the last modified time from the remote server (which is basically
399 * @throws IOException
400 * in case of IO error
402 private String
updateToServer(SimpleSocket s
, Card original
, Card local
)
404 List
<Contact
> added
= new LinkedList
<Contact
>();
405 List
<Contact
> removed
= new LinkedList
<Contact
>();
406 List
<Contact
> from
= new LinkedList
<Contact
>();
407 List
<Contact
> to
= new LinkedList
<Contact
>();
408 original
.compare(local
, added
, removed
, from
, to
);
410 s
.sendCommand(Command
.PUT_CARD
);
412 for (Contact c
: removed
) {
413 s
.sendCommand(Command
.DELETE_CONTACT
, c
.getId());
415 for (Contact c
: added
) {
416 s
.sendCommand(Command
.POST_CONTACT
, c
.getId());
417 BlockAppendable app
= s
.createBlockAppendable();
418 Vcard21Parser
.write(app
, c
, -1);
421 if (from
.size() > 0) {
422 for (int index
= 0; index
< from
.size(); index
++) {
423 Contact f
= from
.get(index
);
424 Contact t
= to
.get(index
);
426 List
<Data
> subadded
= new LinkedList
<Data
>();
427 List
<Data
> subremoved
= new LinkedList
<Data
>();
428 f
.compare(t
, subadded
, subremoved
, subremoved
, subadded
);
429 s
.sendCommand(Command
.PUT_CONTACT
, f
.getId());
430 for (Data d
: subremoved
) {
431 s
.sendCommand(Command
.DELETE_DATA
, d
.getContentState(true));
433 for (Data d
: subadded
) {
434 s
.sendCommand(Command
.POST_DATA
, d
.getContentState(true));
435 BlockAppendable app
= s
.createBlockAppendable();
436 Vcard21Parser
.write(app
, d
);
439 s
.sendCommand(Command
.PUT_CONTACT
);
443 s
.sendCommand(Command
.PUT_CARD
);
445 return s
.receiveLine();
449 * Will update the given {@link Card} object (not {@link File}) to the
450 * currently selected {@link Card} on the remote server.
453 * the {@link SimpleSocket} to work on, which <b>MUST</b> be in
456 * the {@link Card} to update
458 * @return the last modified time from the remote server
460 * @throws IOException
461 * in case of IO error
463 private String
updateFromServer(SimpleSocket s
, Card local
)
465 s
.sendCommand(Command
.PUT_CARD
);
467 s
.sendCommand(Command
.LIST_CONTACT
);
468 Map
<String
, String
> remote
= new HashMap
<String
, String
>();
469 for (String line
: s
.receiveBlock()) {
470 int indexSp
= line
.indexOf(" ");
471 String hash
= line
.substring(0, indexSp
);
472 String uid
= line
.substring(indexSp
+ 1);
474 remote
.put(uid
, hash
);
477 List
<Contact
> deleted
= new LinkedList
<Contact
>();
478 List
<Contact
> changed
= new LinkedList
<Contact
>();
479 List
<String
> added
= new LinkedList
<String
>();
481 for (Contact c
: local
) {
482 String hash
= remote
.get(c
.getId());
485 } else if (!hash
.equals(c
.getContentState(true))) {
490 for (String uid
: remote
.keySet()) {
491 if (local
.getById(uid
) == null)
497 for (Contact c
: deleted
) {
501 for (String uid
: added
) {
502 s
.sendCommand(Command
.GET_CONTACT
, uid
);
503 for (Contact cc
: Vcard21Parser
.parseContact(s
.receiveBlock())) {
508 for (Contact c
: changed
) {
510 s
.sendCommand(Command
.GET_CONTACT
, c
.getId());
511 for (Contact cc
: Vcard21Parser
.parseContact(s
.receiveBlock())) {
516 s
.sendCommand(Command
.PUT_CARD
);
518 return s
.receiveLine();
522 * Return the requested cache for the current resource.
527 * @return the cached {@link File}
529 private File
getCache(File dir
) {
530 return new File(dir
.getPath() + File
.separator
+ name
);
534 * Return the cached {@link File} corresponding to the current resource.
536 * @return the cached {@link File}
538 public File
getCache() {
539 return new File(cacheDir
.getPath() + File
.separator
+ name
);
543 * Get the last modified date of the current resource's original cached
544 * file, that is, the time the server reported as the "last modified time"
545 * when this resource was transfered.
547 * @return the last modified time from the server back when this resource
550 public long getLastModified() {
552 BufferedReader in
= new BufferedReader(new InputStreamReader(
553 new FileInputStream(cacheDirOrigTS
.getPath()
554 + File
.separator
+ name
)));
555 String line
= in
.readLine();
558 return StringUtils
.toTime(line
);
559 } catch (FileNotFoundException e
) {
561 } catch (Exception e
) {
567 * Set the last modified date of the current resource's original cached
568 * file, that is, the time the server reported as the "last modified time"
569 * when this resource was transfered.
572 * the last modified time from the server back when this resource
575 public void setLastModified(String time
) {
577 BufferedWriter out
= new BufferedWriter(new OutputStreamWriter(
578 new FileOutputStream(cacheDirOrigTS
.getPath()
579 + File
.separator
+ name
)));
583 } catch (FileNotFoundException e
) {
585 } catch (IOException e
) {
591 * Configure the synchronisation mechanism (cache, auto update...).
593 * @throws InvalidParameterException
594 * if the remote configuration file <tt>remote.properties</tt>
595 * cannot be accessed or if the cache directory cannot be used
597 static private void config() {
599 RemoteBundle bundle
= new RemoteBundle();
602 dir
= bundle
.getString(RemotingOption
.CLIENT_CACHE_DIR
);
604 cacheDir
= new File(dir
+ File
.separator
+ "current");
606 cacheDirOrig
= new File(dir
+ File
.separator
+ "original");
607 cacheDirOrig
.mkdir();
608 cacheDirOrigTS
= new File(dir
+ File
.separator
+ "timestamps");
609 cacheDirOrigTS
.mkdir();
611 if (!cacheDir
.exists() || !cacheDirOrig
.exists()) {
612 throw new IOException("Cannot open or create cache store at: "
617 .getBoolean(RemotingOption
.CLIENT_AUTO_SYNC
, false);
618 } catch (Exception e
) {
619 throw new InvalidParameterException(
620 "Cannot open or create cache store at: " + dir
);