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
;
19 import java
.util
.MissingResourceException
;
20 import java
.util
.ResourceBundle
;
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
;
31 * This class will synchronise {@link Card}s between a local instance an a
32 * remote jVCard server.
38 /** The time in ms after which we declare that 2 timestamps are different */
39 static private final int GRACE_TIME
= 2001;
41 /** Directory where to store local cache of remote {@link Card}s. */
42 static private File cacheDir
;
45 * Directory where to store cache of remote {@link Card}s without
46 * modifications since the last synchronisation.
48 static private File cacheDirOrig
;
49 /** Directory where to store timestamps for files in cacheDirOrig */
50 static private File cacheDirOrigTS
;
52 static private boolean autoSync
;
56 /** Resource name on the remote server. */
60 * Create a new {@link Sync} object, ready to operate for the given resource
61 * on the given server.
64 * Note that the format used is the standard "host:port_number/file", with
65 * an optional <tt>jvcard://</tt> prefix.
69 * E.g.: <tt>jvcard://localhost:4444/family.vcf</tt>
73 * the server and port to contact, optionally prefixed with
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
80 public Sync(String url
) {
81 if (cacheDir
== null) {
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: "
100 * Create a new {@link Sync} object, ready to operate on the given server.
104 * the server to contact
108 * the resource name to synchronise to
110 public Sync(String host
, int port
, String name
) {
117 * Check if the remote server already know about this resource.
119 * @return TRUE if it is possible to contact the remote server and that this
120 * server has the resource available
122 public boolean isAvailable() {
124 SimpleSocket s
= new SimpleSocket(new Socket(host
, port
),
125 "check avail client");
127 s
.sendCommand(Command
.LIST_CARD
);
128 List
<String
> timestampedFiles
= s
.receiveBlock();
131 for (String timestampedFile
: timestampedFiles
) {
132 String file
= timestampedFile
.substring(StringUtils
.fromTime(0)
134 if (file
.equals(name
)) {
138 } catch (Exception e
) {
145 * Synchronise the current resource if needed, then return the locally
146 * cached version of said resource.
149 * A synchronisation is deemed necessary if one of the following is true:
151 * <li><tt>force</tt> is TRUE</li>
152 * <li><tt>CLIENT_AUTO_SYNC</tt> is TRUE in the configuration file</li>
153 * <li>the {@link Card} exists locally but not on the remote server</li>
154 * <li>the {@link Card} exists on the remote server but not locally</li>
159 * force the synchronisation to occur
161 * @return the synchronised (or not) {@link Card}
163 * @throws UnknownHostException
164 * in case of server name resolution failure
165 * @throws IOException
166 * in case of IO error
168 public Card
sync(boolean force
) throws UnknownHostException
, IOException
{
170 long tsOriginal
= getLastModified();
172 Card local
= new Card(getCache(cacheDir
), Format
.VCard21
);
173 local
.setRemote(true);
175 // do NOT update unless we are in autoSync or forced mode or we don't
176 // have the file on cache
177 if (!autoSync
&& !force
&& tsOriginal
!= -1) {
181 SimpleSocket s
= new SimpleSocket(new Socket(host
, port
), "sync client");
183 // get the server time stamp
187 s
.sendCommand(Command
.LIST_CARD
);
188 List
<String
> timestampedFiles
= s
.receiveBlock();
190 for (String timestampedFile
: timestampedFiles
) {
191 String file
= timestampedFile
.substring(StringUtils
.fromTime(0)
193 if (file
.equals(name
)) {
194 tsServer
= StringUtils
.toTime(timestampedFile
.substring(0,
195 StringUtils
.fromTime(0).length()));
201 // - file not present neither in cache nor on server
202 // - remote < previous
203 if ((tsServer
== -1 && tsOriginal
== -1)
204 || (tsServer
!= -1 && tsOriginal
!= -1 && ((tsOriginal
- tsServer
) > GRACE_TIME
))) {
205 throw new IOException(
206 "The timestamps between server and client are invalid");
210 boolean serverChanges
= (tsServer
- tsOriginal
) > GRACE_TIME
;
211 boolean localChanges
= false;
212 Card original
= null;
213 if (tsOriginal
!= -1) {
214 original
= new Card(getCache(cacheDirOrig
), Format
.VCard21
);
215 localChanges
= !local
.isEquals(original
, true);
218 Command action
= null;
220 // Sync to server if:
222 action
= Command
.PUT_CARD
;
225 // Sync from server if:
227 action
= Command
.HASH_CONTACT
;
230 // Sync from/to server if
231 if (serverChanges
&& localChanges
) {
232 action
= Command
.HELP
;
235 // POST the whole file if:
236 if (tsServer
== -1) {
237 action
= Command
.POST_CARD
;
240 // GET the whole file if:
241 if (tsOriginal
== -1) {
242 action
= Command
.GET_CARD
;
245 System
.err
.println("remote: " + (tsServer
/ 1000) % 1000 + " ("
247 System
.err
.println("previous: " + (tsOriginal
/ 1000) % 1000 + " ("
249 System
.err
.println("local changes: " + localChanges
);
250 System
.err
.println("server changes: " + serverChanges
);
251 System
.err
.println("choosen action: " + action
);
253 if (action
!= null) {
254 s
.sendCommand(Command
.SELECT
, name
);
255 if (tsServer
!= StringUtils
.toTime(s
.receiveLine())) {
256 System
.err
.println("DEBUG: it changed. retry.");
257 s
.sendCommand(Command
.SELECT
);
264 s
.sendCommand(Command
.GET_CARD
);
265 List
<String
> data
= s
.receiveBlock();
266 setLastModified(data
.remove(0));
267 Card server
= new Card(Vcard21Parser
.parseContact(data
));
268 local
.replaceListContent(server
);
272 local
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
276 s
.sendCommand(Command
.POST_CARD
);
277 s
.sendBlock(Vcard21Parser
.toStrings(local
));
278 local
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
279 setLastModified(s
.receiveLine());
283 String serverLastModifTime
= updateToServer(s
, original
,
286 local
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
288 setLastModified(serverLastModifTime
);
292 String serverLastModifTime
= updateFromServer(s
, local
);
295 local
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
297 setLastModified(serverLastModifTime
);
302 throw new IOException("two-way sync not supported yet");
304 // note: we are holding the server here, so it could throw
305 // us away if we take too long
307 File mergeF
= File
.createTempFile("contact-merge", ".vcf");
309 .createTempFile("contact-server", ".vcf");
310 original
.saveAs(serverF
, Format
.VCard21
);
312 Card server
= new Card(serverF
, Format
.VCard21
);
313 updateFromServer(s
, server
);
315 // TODO: auto merge into mergeF (from original, local,
317 local
.saveAs(mergeF
, Format
.VCard21
);
318 Card merge
= new Card(mergeF
, Format
.VCard21
);
320 // TODO: ask client if ok or to change it herself
322 String serverLastModifTime
= updateToServer(s
, original
,
325 merge
.saveAs(getCache(cacheDir
), Format
.VCard21
);
326 merge
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
328 setLastModified(serverLastModifTime
);
336 s
.sendCommand(Command
.SELECT
);
338 } catch (IOException e
) {
340 } catch (Exception e
) {
351 * Will update the currently selected {@link Card} on the remote server to
352 * be in the same state as <tt>local</tt>, assuming the server is currently
353 * in <tt>original</tt> state.
356 * the {@link SimpleSocket} to work on, which <b>MUST</b> be in
359 * the original {@link Card} as it was before the client made
362 * the {@link Card} to which state we want the server in
364 * @return the last modified time from the remote server (which is basically
367 * @throws IOException
368 * in case of IO error
370 private String
updateToServer(SimpleSocket s
, Card original
, Card local
)
372 List
<Contact
> added
= new LinkedList
<Contact
>();
373 List
<Contact
> removed
= new LinkedList
<Contact
>();
374 List
<Contact
> from
= new LinkedList
<Contact
>();
375 List
<Contact
> to
= new LinkedList
<Contact
>();
376 original
.compare(local
, added
, removed
, from
, to
);
378 s
.sendCommand(Command
.PUT_CARD
);
380 for (Contact c
: removed
) {
381 s
.sendCommand(Command
.DELETE_CONTACT
, c
.getId());
383 for (Contact c
: added
) {
384 s
.sendCommand(Command
.POST_CONTACT
, c
.getId());
385 s
.sendBlock(Vcard21Parser
.toStrings(c
, -1));
387 if (from
.size() > 0) {
388 for (int index
= 0; index
< from
.size(); index
++) {
389 Contact f
= from
.get(index
);
390 Contact t
= to
.get(index
);
392 List
<Data
> subadded
= new LinkedList
<Data
>();
393 List
<Data
> subremoved
= new LinkedList
<Data
>();
394 f
.compare(t
, subadded
, subremoved
, subremoved
, subadded
);
395 s
.sendCommand(Command
.PUT_CONTACT
, name
);
396 for (Data d
: subremoved
) {
397 s
.sendCommand(Command
.DELETE_DATA
, d
.getContentState());
399 for (Data d
: subadded
) {
400 s
.sendCommand(Command
.POST_DATA
, d
.getContentState());
401 s
.sendBlock(Vcard21Parser
.toStrings(d
));
406 s
.sendCommand(Command
.PUT_CARD
);
408 return s
.receiveLine();
412 * Will update the given {@link Card} object (not {@link File}) to the
413 * currently selected {@link Card} on the remote server.
416 * the {@link SimpleSocket} to work on, which <b>MUST</b> be in
419 * the {@link Card} to update
421 * @return the last modified time from the remote server
423 * @throws IOException
424 * in case of IO error
426 private String
updateFromServer(SimpleSocket s
, Card local
)
428 s
.sendCommand(Command
.PUT_CARD
);
430 s
.sendCommand(Command
.LIST_CONTACT
);
431 Map
<String
, String
> remote
= new HashMap
<String
, String
>();
432 for (String line
: s
.receiveBlock()) {
433 int indexSp
= line
.indexOf(" ");
434 String hash
= line
.substring(0, indexSp
);
435 String uid
= line
.substring(indexSp
+ 1);
437 remote
.put(uid
, hash
);
440 List
<Contact
> deleted
= new LinkedList
<Contact
>();
441 List
<Contact
> changed
= new LinkedList
<Contact
>();
442 List
<String
> added
= new LinkedList
<String
>();
444 for (Contact c
: local
) {
445 String hash
= remote
.get(c
.getId());
448 } else if (!hash
.equals(c
.getContentState())) {
453 for (String uid
: remote
.keySet()) {
454 if (local
.getById(uid
) == null)
460 for (Contact c
: deleted
) {
464 for (String uid
: added
) {
465 s
.sendCommand(Command
.GET_CONTACT
, uid
);
466 for (Contact cc
: Vcard21Parser
.parseContact(s
.receiveBlock())) {
471 for (Contact c
: changed
) {
473 s
.sendCommand(Command
.GET_CONTACT
, c
.getId());
474 for (Contact cc
: Vcard21Parser
.parseContact(s
.receiveBlock())) {
479 s
.sendCommand(Command
.PUT_CARD
);
481 return s
.receiveLine();
485 * Return the requested cache for the current resource.
490 * @return the cached {@link File}
492 private File
getCache(File dir
) {
493 return new File(dir
.getPath() + File
.separator
+ name
);
497 * Return the cached {@link File} corresponding to the current resource.
499 * @return the cached {@link File}
501 public File
getCache() {
502 return new File(cacheDir
.getPath() + File
.separator
+ name
);
506 * Get the last modified date of the current resource's original cached
507 * file, that is, the time the server reported as the "last modified time"
508 * when this resource was transfered.
510 * @return the last modified time from the server back when this resource
513 public long getLastModified() {
515 BufferedReader in
= new BufferedReader(new InputStreamReader(
516 new FileInputStream(cacheDirOrigTS
.getPath()
517 + File
.separator
+ name
)));
518 String line
= in
.readLine();
521 return StringUtils
.toTime(line
);
522 } catch (FileNotFoundException e
) {
524 } catch (Exception e
) {
530 * Set the last modified date of the current resource's original cached
531 * file, that is, the time the server reported as the "last modified time"
532 * when this resource was transfered.
535 * the last modified time from the server back when this resource
538 public void setLastModified(String time
) {
540 BufferedWriter out
= new BufferedWriter(new OutputStreamWriter(
541 new FileOutputStream(cacheDirOrigTS
.getPath()
542 + File
.separator
+ name
)));
546 } catch (FileNotFoundException e
) {
548 } catch (IOException e
) {
554 * Configure the synchronisation mechanism (cache, auto update...).
556 * @throws InvalidParameterException
557 * if the remote configuration file <tt>remote.properties</tt>
558 * cannot be accessed or if the cache directory cannot be used
560 static private void config() {
562 ResourceBundle bundle
= Bundles
.getBundle("remote");
565 dir
= bundle
.getString("CLIENT_CACHE_DIR").trim();
567 cacheDir
= new File(dir
+ File
.separator
+ "current");
569 cacheDirOrig
= new File(dir
+ File
.separator
+ "original");
570 cacheDirOrig
.mkdir();
571 cacheDirOrigTS
= new File(dir
+ File
.separator
+ "timestamps");
572 cacheDirOrigTS
.mkdir();
574 if (!cacheDir
.exists() || !cacheDirOrig
.exists()) {
575 throw new IOException("Cannot open or create cache store at: "
579 String autoStr
= bundle
.getString("CLIENT_AUTO_SYNC");
580 if (autoStr
!= null && autoStr
.trim().equalsIgnoreCase("true")) {
584 } catch (MissingResourceException e
) {
585 throw new InvalidParameterException(
586 "Cannot access remote.properties configuration file");
587 } catch (Exception e
) {
588 throw new InvalidParameterException(
589 "Cannot open or create cache store at: " + dir
);