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