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
.time
.LocalDate
;
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
.parsers
.Format
;
22 import be
.nikiroo
.jvcard
.parsers
.Parser
;
23 import be
.nikiroo
.jvcard
.remote
.Command
.Verb
;
24 import be
.nikiroo
.jvcard
.resources
.Bundles
;
25 import be
.nikiroo
.jvcard
.tui
.StringUtils
;
28 * This class will synchronise {@link Card}s between a local instance an a
29 * remote jVCard server.
35 /** The time in ms after which we declare that 2 timestamps are different */
36 static private final int GRACE_TIME
= 2000;
38 /** Directory where to store local cache of remote {@link Card}s. */
39 static private File cacheDir
;
42 * Directory where to store cache of remote {@link Card}s without
43 * modifications since the last synchronisation.
45 static private File cacheDirOrig
;
46 /** Directory where to store timestamps for files in cacheDirOrig */
47 static private File cacheDirOrigTS
;
49 static private boolean autoSync
;
53 /** Resource name on the remote server. */
57 * Create a new {@link Sync} object, ready to operate for the given resource
58 * on the given server.
61 * Note that the format used is the standard "host:port_number/file", with
62 * an optional <tt>jvcard://</tt> prefix.
66 * E.g.: <tt>jvcard://localhost:4444/family.vcf</tt>
70 * the server and port to contact, optionally prefixed with
73 * @throws InvalidParameterException
74 * if the remote configuration file <tt>remote.properties</tt>
75 * cannot be accessed or if the cache directory cannot be used
77 public Sync(String url
) {
78 if (cacheDir
== null) {
83 url
= url
.replace("jvcard://", "");
84 int indexSl
= url
.indexOf('/');
85 this.name
= url
.substring(indexSl
+ 1);
86 url
= url
.substring(0, indexSl
);
87 this.host
= url
.split("\\:")[0];
88 this.port
= Integer
.parseInt(url
.split("\\:")[1]);
89 } catch (Exception e
) {
90 throw new InvalidParameterException(
91 "the given parameter was not a valid HOST:PORT value: "
97 * Create a new {@link Sync} object, ready to operate on the given server.
101 * the server to contact
105 * the resource name to synchronise to
107 public Sync(String host
, int port
, String name
) {
114 * Check if the synchronisation is available for this resource.
116 * @return TRUE if it is possible to contact the remote server and that this
117 * server has the resource available
119 public boolean isAvailable() {
121 SimpleSocket s
= new SimpleSocket(new Socket(host
, port
),
122 "check avail client");
124 s
.sendCommand(Verb
.LIST
);
125 List
<String
> timestampedFiles
= s
.receiveBlock();
128 for (String timestampedFile
: timestampedFiles
) {
129 String file
= timestampedFile
.substring(StringUtils
.fromTime(0)
131 if (file
.equals(name
)) {
135 } catch (Exception e
) {
141 // return: synced or not
142 public boolean sync(Card card
, boolean force
) throws UnknownHostException
,
145 long tsOriginal
= getLastModified();
147 // do NOT update unless we are in autoSync or forced mode or we don't
148 // have the file on cache
149 if (!autoSync
&& !force
&& tsOriginal
!= -1) {
153 SimpleSocket s
= new SimpleSocket(new Socket(host
, port
), "sync client");
155 // get the server time stamp
159 s
.sendCommand(Verb
.LIST
);
160 List
<String
> timestampedFiles
= s
.receiveBlock();
162 for (String timestampedFile
: timestampedFiles
) {
163 String file
= timestampedFile
.substring(StringUtils
.fromTime(0)
165 if (file
.equals(name
)) {
166 tsServer
= StringUtils
.toTime(timestampedFile
.substring(0,
167 StringUtils
.fromTime(0).length()));
171 } catch (IOException e
) {
174 } catch (Exception e
) {
181 // - file not preset neither in cache nor on server
182 // - remote < previous
183 if ((tsServer
== -1 && tsOriginal
== -1)
184 || (tsServer
!= -1 && tsOriginal
!= -1 && ((tsOriginal
- tsServer
) > GRACE_TIME
))) {
185 throw new IOException(
186 "The timestamps between server and client are invalid");
190 boolean serverChanges
= (tsServer
- tsOriginal
) > GRACE_TIME
;
191 boolean localChanges
= false;
192 if (tsOriginal
!= -1) {
193 Card local
= new Card(getCache(cacheDir
), Format
.VCard21
);
194 Card original
= new Card(getCache(cacheDirOrig
), Format
.VCard21
);
195 localChanges
= !local
.isEquals(original
, true);
200 // Sync to server if:
202 // TODO: sync instead (with PUT)
206 // Sync from server if
207 if (serverChanges
&& localChanges
) {
209 throw new IOException("Sync operation not supported yet :(");
212 // PUT the whole file if:
213 if (tsServer
== -1) {
217 // GET the whole file if:
218 if (tsOriginal
== -1 || serverChanges
) {
222 System
.err
.println("remote: " + (tsServer
/ 1000) % 1000 + " ("
224 System
.err
.println("previous: " + (tsOriginal
/ 1000) % 1000 + " ("
226 System
.err
.println("local changes: " + localChanges
);
227 System
.err
.println("server changes: " + serverChanges
);
228 System
.err
.println("choosen action: " + action
);
230 if (action
!= null) {
233 s
.sendCommand(Verb
.GET
, name
);
234 List
<String
> data
= s
.receiveBlock();
235 setLastModified(data
.remove(0));
236 Card server
= new Card(Parser
.parse(data
, Format
.VCard21
));
237 card
.replaceListContent(server
);
241 card
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
244 s
.sendCommand(Verb
.POST
, name
);
245 s
.sendLine(card
.toString(Format
.VCard21
));
246 card
.saveAs(getCache(cacheDirOrig
), Format
.VCard21
);
247 setLastModified(s
.receiveLine());
251 throw new IOException(action
252 + " operation not supported yet :(");
262 * Return the requested cache for the current resource.
267 * @return the cached {@link File}
269 private File
getCache(File dir
) {
270 return new File(dir
.getPath() + File
.separator
+ name
);
274 * Return the cached {@link File} corresponding to the current resource.
276 * @return the cached {@link File}
278 public File
getCache() {
279 return new File(cacheDir
.getPath() + File
.separator
+ name
);
283 * Get the last modified date of the current resource's original cached
284 * file, that is, the time the server reported as the "last modified time"
285 * when this resource was transfered.
287 * @return the last modified time from the server back when this resource
290 public long getLastModified() {
292 BufferedReader in
= new BufferedReader(new InputStreamReader(
293 new FileInputStream(cacheDirOrigTS
.getPath()
294 + File
.separator
+ name
)));
295 String line
= in
.readLine();
298 return StringUtils
.toTime(line
);
299 } catch (FileNotFoundException e
) {
301 } catch (Exception e
) {
307 * Set the last modified date of the current resource's original cached
308 * file, that is, the time the server reported as the "last modified time"
309 * when this resource was transfered.
312 * the last modified time from the server back when this resource
315 public void setLastModified(String time
) {
317 BufferedWriter out
= new BufferedWriter(new OutputStreamWriter(
318 new FileOutputStream(cacheDirOrigTS
.getPath()
319 + File
.separator
+ name
)));
323 } catch (FileNotFoundException e
) {
325 } catch (IOException e
) {
331 * Configure the synchronisation mechanism (cache, auto update...).
333 * @throws InvalidParameterException
334 * if the remote configuration file <tt>remote.properties</tt>
335 * cannot be accessed or if the cache directory cannot be used
337 static private void config() {
339 ResourceBundle bundle
= Bundles
.getBundle("remote");
342 dir
= bundle
.getString("CLIENT_CACHE_DIR").trim();
344 cacheDir
= new File(dir
+ File
.separator
+ "current");
346 cacheDirOrig
= new File(dir
+ File
.separator
+ "original");
347 cacheDirOrig
.mkdir();
348 cacheDirOrigTS
= new File(dir
+ File
.separator
+ "timestamps");
349 cacheDirOrigTS
.mkdir();
351 if (!cacheDir
.exists() || !cacheDirOrig
.exists()) {
352 throw new IOException("Cannot open or create cache store at: "
356 String autoStr
= bundle
.getString("CLIENT_AUTO_SYNC");
357 if (autoStr
!= null && autoStr
.trim().equalsIgnoreCase("true")) {
361 } catch (MissingResourceException e
) {
362 throw new InvalidParameterException(
363 "Cannot access remote.properties configuration file");
364 } catch (Exception e
) {
365 throw new InvalidParameterException(
366 "Cannot open or create cache store at: " + dir
);