9fff9b8985056039e42c14c51dfb7013efb1412f
[jvcard.git] / src / be / nikiroo / jvcard / remote / Sync.java
1 package be.nikiroo.jvcard.remote;
2
3 import java.io.BufferedReader;
4 import java.io.BufferedWriter;
5 import java.io.File;
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;
19
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;
26
27 /**
28 * This class will synchronise {@link Card}s between a local instance an a
29 * remote jVCard server.
30 *
31 * @author niki
32 *
33 */
34 public class Sync {
35 /** The time in ms after which we declare that 2 timestamps are different */
36 static private final int GRACE_TIME = 2000;
37
38 /** Directory where to store local cache of remote {@link Card}s. */
39 static private File cacheDir;
40
41 /**
42 * Directory where to store cache of remote {@link Card}s without
43 * modifications since the last synchronisation.
44 */
45 static private File cacheDirOrig;
46 /** Directory where to store timestamps for files in cacheDirOrig */
47 static private File cacheDirOrigTS;
48
49 static private boolean autoSync;
50 private String host;
51 private int port;
52
53 /** Resource name on the remote server. */
54 private String name;
55
56 /**
57 * Create a new {@link Sync} object, ready to operate for the given resource
58 * on the given server.
59 *
60 * <p>
61 * Note that the format used is the standard "host:port_number/file", with
62 * an optional <tt>jvcard://</tt> prefix.
63 * </p>
64 *
65 * <p>
66 * E.g.: <tt>jvcard://localhost:4444/family.vcf</tt>
67 * </p>
68 *
69 * @param url
70 * the server and port to contact, optionally prefixed with
71 * <tt>jvcard://</tt>
72 *
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
76 */
77 public Sync(String url) {
78 if (cacheDir == null) {
79 config();
80 }
81
82 try {
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: "
92 + url);
93 }
94 }
95
96 /**
97 * Create a new {@link Sync} object, ready to operate on the given server.
98 *
99 *
100 * @param host
101 * the server to contact
102 * @param port
103 * the port to use
104 * @param name
105 * the resource name to synchronise to
106 */
107 public Sync(String host, int port, String name) {
108 this.host = host;
109 this.port = port;
110 this.name = name;
111 }
112
113 /**
114 * Check if the synchronisation is available for this resource.
115 *
116 * @return TRUE if it is possible to contact the remote server and that this
117 * server has the resource available
118 */
119 public boolean isAvailable() {
120 try {
121 SimpleSocket s = new SimpleSocket(new Socket(host, port),
122 "check avail client");
123 s.open(true);
124 s.sendCommand(Verb.LIST);
125 List<String> timestampedFiles = s.receiveBlock();
126 s.close();
127
128 for (String timestampedFile : timestampedFiles) {
129 String file = timestampedFile.substring(StringUtils.fromTime(0)
130 .length() + 1);
131 if (file.equals(name)) {
132 return true;
133 }
134 }
135 } catch (Exception e) {
136 }
137
138 return false;
139 }
140
141 // return: synced or not
142 public boolean sync(Card card, boolean force) throws UnknownHostException,
143 IOException {
144
145 long tsOriginal = getLastModified();
146
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) {
150 return false;
151 }
152
153 SimpleSocket s = new SimpleSocket(new Socket(host, port), "sync client");
154
155 // get the server time stamp
156 long tsServer = -1;
157 try {
158 s.open(true);
159 s.sendCommand(Verb.LIST);
160 List<String> timestampedFiles = s.receiveBlock();
161
162 for (String timestampedFile : timestampedFiles) {
163 String file = timestampedFile.substring(StringUtils.fromTime(0)
164 .length() + 1);
165 if (file.equals(name)) {
166 tsServer = StringUtils.toTime(timestampedFile.substring(0,
167 StringUtils.fromTime(0).length()));
168 break;
169 }
170 }
171 } catch (IOException e) {
172 s.close();
173 throw e;
174 } catch (Exception e) {
175 e.printStackTrace();
176 s.close();
177 return false;
178 }
179
180 // Error cases:
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");
187 }
188
189 // Check changes
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);
196 }
197
198 Verb action = null;
199
200 // Sync to server if:
201 if (localChanges) {
202 // TODO: sync instead (with PUT)
203 action = Verb.POST;
204 }
205
206 // Sync from server if
207 if (serverChanges && localChanges) {
208 // TODO
209 throw new IOException("Sync operation not supported yet :(");
210 }
211
212 // PUT the whole file if:
213 if (tsServer == -1) {
214 action = Verb.POST;
215 }
216
217 // GET the whole file if:
218 if (tsOriginal == -1 || serverChanges) {
219 action = Verb.GET;
220 }
221
222 System.err.println("remote: " + (tsServer / 1000) % 1000 + " ("
223 + tsServer + ")");
224 System.err.println("previous: " + (tsOriginal / 1000) % 1000 + " ("
225 + tsOriginal + ")");
226 System.err.println("local changes: " + localChanges);
227 System.err.println("server changes: " + serverChanges);
228 System.err.println("choosen action: " + action);
229
230 if (action != null) {
231 switch (action) {
232 case GET:
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);
238
239 if (card.isDirty())
240 card.save();
241 card.saveAs(getCache(cacheDirOrig), Format.VCard21);
242 break;
243 case POST:
244 s.sendCommand(Verb.POST, name);
245 s.sendLine(card.toString(Format.VCard21));
246 card.saveAs(getCache(cacheDirOrig), Format.VCard21);
247 setLastModified(s.receiveLine());
248 break;
249 default:
250 // TODO
251 throw new IOException(action
252 + " operation not supported yet :(");
253 }
254 }
255
256 s.close();
257
258 return true;
259 }
260
261 /**
262 * Return the requested cache for the current resource.
263 *
264 * @param dir
265 * the cache to use
266 *
267 * @return the cached {@link File}
268 */
269 private File getCache(File dir) {
270 return new File(dir.getPath() + File.separator + name);
271 }
272
273 /**
274 * Return the cached {@link File} corresponding to the current resource.
275 *
276 * @return the cached {@link File}
277 */
278 public File getCache() {
279 return new File(cacheDir.getPath() + File.separator + name);
280 }
281
282 /**
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.
286 *
287 * @return the last modified time from the server back when this resource
288 * was transfered
289 */
290 public long getLastModified() {
291 try {
292 BufferedReader in = new BufferedReader(new InputStreamReader(
293 new FileInputStream(cacheDirOrigTS.getPath()
294 + File.separator + name)));
295 String line = in.readLine();
296 in.close();
297
298 return StringUtils.toTime(line);
299 } catch (FileNotFoundException e) {
300 return -1;
301 } catch (Exception e) {
302 return -1;
303 }
304 }
305
306 /**
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.
310 *
311 * @param time
312 * the last modified time from the server back when this resource
313 * was transfered
314 */
315 public void setLastModified(String time) {
316 try {
317 BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
318 new FileOutputStream(cacheDirOrigTS.getPath()
319 + File.separator + name)));
320 out.append(time);
321 out.newLine();
322 out.close();
323 } catch (FileNotFoundException e) {
324 e.printStackTrace();
325 } catch (IOException e) {
326 e.printStackTrace();
327 }
328 }
329
330 /**
331 * Configure the synchronisation mechanism (cache, auto update...).
332 *
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
336 */
337 static private void config() {
338 String dir = null;
339 ResourceBundle bundle = Bundles.getBundle("remote");
340
341 try {
342 dir = bundle.getString("CLIENT_CACHE_DIR").trim();
343
344 cacheDir = new File(dir + File.separator + "current");
345 cacheDir.mkdir();
346 cacheDirOrig = new File(dir + File.separator + "original");
347 cacheDirOrig.mkdir();
348 cacheDirOrigTS = new File(dir + File.separator + "timestamps");
349 cacheDirOrigTS.mkdir();
350
351 if (!cacheDir.exists() || !cacheDirOrig.exists()) {
352 throw new IOException("Cannot open or create cache store at: "
353 + dir);
354 }
355
356 String autoStr = bundle.getString("CLIENT_AUTO_SYNC");
357 if (autoStr != null && autoStr.trim().equalsIgnoreCase("true")) {
358 autoSync = true;
359 }
360
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);
367 }
368 }
369 }