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
.LinkedList
;
16 import java
.util
.List
;
17 import java
.util
.MissingResourceException
;
18 import java
.util
.ResourceBundle
;
20 import be
.nikiroo
.jvcard
.Card
;
21 import be
.nikiroo
.jvcard
.Contact
;
22 import be
.nikiroo
.jvcard
.Data
;
23 import be
.nikiroo
.jvcard
.parsers
.Format
;
24 import be
.nikiroo
.jvcard
.parsers
.Vcard21Parser
;
25 import be
.nikiroo
.jvcard
.remote
.Command
.Verb
;
26 import be
.nikiroo
.jvcard
.resources
.Bundles
;
27 import be
.nikiroo
.jvcard
.tui
.StringUtils
;
30 * This class will synchronise {@link Card}s between a local instance an a
31 * remote jVCard server.
37 /** The time in ms after which we declare that 2 timestamps are different */
38 static private final int GRACE_TIME
= 2000;
40 /** Directory where to store local cache of remote {@link Card}s. */
41 static private File cacheDir
;
44 * Directory where to store cache of remote {@link Card}s without
45 * modifications since the last synchronisation.
47 static private File cacheDirOrig
;
48 /** Directory where to store timestamps for files in cacheDirOrig */
49 static private File cacheDirOrigTS
;
51 static private boolean autoSync
;
55 /** Resource name on the remote server. */
59 * Create a new {@link Sync} object, ready to operate for the given resource
60 * on the given server.
63 * Note that the format used is the standard "host:port_number/file", with
64 * an optional <tt>jvcard://</tt> prefix.
68 * E.g.: <tt>jvcard://localhost:4444/family.vcf</tt>
72 * the server and port to contact, optionally prefixed with
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
79 public Sync(String url
) {
80 if (cacheDir
== null) {
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: "
99 * Create a new {@link Sync} object, ready to operate on the given server.
103 * the server to contact
107 * the resource name to synchronise to
109 public Sync(String host
, int port
, String name
) {
116 * Check if the synchronisation is available for this resource.
118 * @return TRUE if it is possible to contact the remote server and that this
119 * server has the resource available
121 public boolean isAvailable() {
123 SimpleSocket s
= new SimpleSocket(new Socket(host
, port
),
124 "check avail client");
126 s
.sendCommand(Verb
.LIST
);
127 List
<String
> timestampedFiles
= s
.receiveBlock();
130 for (String timestampedFile
: timestampedFiles
) {
131 String file
= timestampedFile
.substring(StringUtils
.fromTime(0)
133 if (file
.equals(name
)) {
137 } catch (Exception e
) {
143 // return: synced or not
144 public boolean sync(Card card
, boolean force
) throws UnknownHostException
,
147 long tsOriginal
= getLastModified();
149 // do NOT update unless we are in autoSync or forced mode or we don't
150 // have the file on cache
151 if (!autoSync
&& !force
&& tsOriginal
!= -1) {
155 SimpleSocket s
= new SimpleSocket(new Socket(host
, port
), "sync client");
157 // get the server time stamp
161 s
.sendCommand(Verb
.LIST
);
162 List
<String
> timestampedFiles
= s
.receiveBlock();
164 for (String timestampedFile
: timestampedFiles
) {
165 String file
= timestampedFile
.substring(StringUtils
.fromTime(0)
167 if (file
.equals(name
)) {
168 tsServer
= StringUtils
.toTime(timestampedFile
.substring(0,
169 StringUtils
.fromTime(0).length()));
173 } catch (IOException e
) {
176 } catch (Exception e
) {
183 // - file not preset neither in cache nor on server
184 // - remote < previous
185 if ((tsServer
== -1 && tsOriginal
== -1)
186 || (tsServer
!= -1 && tsOriginal
!= -1 && ((tsOriginal
- tsServer
) > GRACE_TIME
))) {
187 throw new IOException(
188 "The timestamps between server and client are invalid");
192 boolean serverChanges
= (tsServer
- tsOriginal
) > GRACE_TIME
;
193 boolean localChanges
= false;
195 Card original
= null;
196 if (tsOriginal
!= -1) {
197 local
= new Card(getCache(cacheDir
), Format
.VCard21
);
198 original
= new Card(getCache(cacheDirOrig
), Format
.VCard21
);
199 localChanges
= !local
.isEquals(original
, true);
204 // Sync to server if:
206 action
= Verb
.PUT_CARD
;
209 // Sync from/to server if
210 if (serverChanges
&& localChanges
) {
211 action
= Verb
.PUT_CARD
;
214 // Sync from server if:
216 // TODO: only sends changed cstate if serverChanges
217 action
= Verb
.GET_CARD
;
220 // PUT the whole file if:
221 if (tsServer
== -1) {
222 action
= Verb
.POST_CARD
;
225 // GET the whole file if:
226 if (tsOriginal
== -1) {
227 action
= Verb
.GET_CARD
;
230 System
.err
.println("remote: " + (tsServer
/ 1000) % 1000 + " ("
232 System
.err
.println("previous: " + (tsOriginal
/ 1000) % 1000 + " ("
234 System
.err
.println("local changes: " + localChanges
);
235 System
.err
.println("server changes: " + serverChanges
);
236 System
.err
.println("choosen action: " + action
);
238 if (action
!= null) {
241 s
.sendCommand(Verb
.GET_CARD
, name
);
242 List
<String
> data
= s
.receiveBlock();
243 setLastModified(data
.remove(0));
244 Card server
= new Card(Vcard21Parser
.parseContact(data
));
245 card
.replaceListContent(server
);
249 card
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
252 s
.sendCommand(Verb
.POST_CARD
, name
);
253 s
.sendBlock(Vcard21Parser
.toStrings(card
));
254 card
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
255 setLastModified(s
.receiveLine());
258 List
<Contact
> added
= new LinkedList
<Contact
>();
259 List
<Contact
> removed
= new LinkedList
<Contact
>();
260 List
<Contact
> from
= new LinkedList
<Contact
>();
261 List
<Contact
> to
= new LinkedList
<Contact
>();
262 original
.compare(local
, added
, removed
, from
, to
);
263 s
.sendCommand(Verb
.PUT_CARD
, name
);
264 for (Contact c
: removed
) {
265 s
.sendCommand(Verb
.DELETE_CONTACT
, c
.getId());
267 for (Contact c
: added
) {
268 s
.sendCommand(Verb
.POST_CONTACT
, c
.getId());
269 s
.sendBlock(Vcard21Parser
.toStrings(c
, -1));
271 if (from
.size() > 0) {
272 for (int index
= 0; index
< from
.size(); index
++) {
273 Contact f
= from
.get(index
);
274 Contact t
= to
.get(index
);
276 List
<Data
> subadded
= new LinkedList
<Data
>();
277 List
<Data
> subremoved
= new LinkedList
<Data
>();
278 f
.compare(t
, subadded
, subremoved
, subremoved
, subadded
);
279 s
.sendCommand(Verb
.PUT_CONTACT
, name
);
280 for (Data d
: subremoved
) {
281 s
.sendCommand(Verb
.DELETE_DATA
, d
.getContentState());
283 for (Data d
: subadded
) {
284 s
.sendCommand(Verb
.POST_DATA
, d
.getContentState());
285 s
.sendBlock(Vcard21Parser
.toStrings(d
));
289 s
.sendCommand(Verb
.PUT_CARD
);
293 throw new IOException(action
294 + " operation not supported yet :(");
304 * Return the requested cache for the current resource.
309 * @return the cached {@link File}
311 private File
getCache(File dir
) {
312 return new File(dir
.getPath() + File
.separator
+ name
);
316 * Return the cached {@link File} corresponding to the current resource.
318 * @return the cached {@link File}
320 public File
getCache() {
321 return new File(cacheDir
.getPath() + File
.separator
+ name
);
325 * Get the last modified date of the current resource's original cached
326 * file, that is, the time the server reported as the "last modified time"
327 * when this resource was transfered.
329 * @return the last modified time from the server back when this resource
332 public long getLastModified() {
334 BufferedReader in
= new BufferedReader(new InputStreamReader(
335 new FileInputStream(cacheDirOrigTS
.getPath()
336 + File
.separator
+ name
)));
337 String line
= in
.readLine();
340 return StringUtils
.toTime(line
);
341 } catch (FileNotFoundException e
) {
343 } catch (Exception e
) {
349 * Set the last modified date of the current resource's original cached
350 * file, that is, the time the server reported as the "last modified time"
351 * when this resource was transfered.
354 * the last modified time from the server back when this resource
357 public void setLastModified(String time
) {
359 BufferedWriter out
= new BufferedWriter(new OutputStreamWriter(
360 new FileOutputStream(cacheDirOrigTS
.getPath()
361 + File
.separator
+ name
)));
365 } catch (FileNotFoundException e
) {
367 } catch (IOException e
) {
373 * Configure the synchronisation mechanism (cache, auto update...).
375 * @throws InvalidParameterException
376 * if the remote configuration file <tt>remote.properties</tt>
377 * cannot be accessed or if the cache directory cannot be used
379 static private void config() {
381 ResourceBundle bundle
= Bundles
.getBundle("remote");
384 dir
= bundle
.getString("CLIENT_CACHE_DIR").trim();
386 cacheDir
= new File(dir
+ File
.separator
+ "current");
388 cacheDirOrig
= new File(dir
+ File
.separator
+ "original");
389 cacheDirOrig
.mkdir();
390 cacheDirOrigTS
= new File(dir
+ File
.separator
+ "timestamps");
391 cacheDirOrigTS
.mkdir();
393 if (!cacheDir
.exists() || !cacheDirOrig
.exists()) {
394 throw new IOException("Cannot open or create cache store at: "
398 String autoStr
= bundle
.getString("CLIENT_AUTO_SYNC");
399 if (autoStr
!= null && autoStr
.trim().equalsIgnoreCase("true")) {
403 } catch (MissingResourceException e
) {
404 throw new InvalidParameterException(
405 "Cannot access remote.properties configuration file");
406 } catch (Exception e
) {
407 throw new InvalidParameterException(
408 "Cannot open or create cache store at: " + dir
);