4b9936573fdecda0beac378d377fa19e2c4c2c1e
[jvcard.git] / src / be / nikiroo / jvcard / remote / Server.java
1 package be.nikiroo.jvcard.remote;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.net.ServerSocket;
6 import java.net.Socket;
7 import java.net.UnknownHostException;
8 import java.security.InvalidParameterException;
9 import java.util.Date;
10 import java.util.LinkedList;
11 import java.util.List;
12 import java.util.ResourceBundle;
13
14 import be.nikiroo.jvcard.Card;
15 import be.nikiroo.jvcard.Contact;
16 import be.nikiroo.jvcard.Data;
17 import be.nikiroo.jvcard.parsers.Format;
18 import be.nikiroo.jvcard.parsers.Vcard21Parser;
19 import be.nikiroo.jvcard.remote.Command.Verb;
20 import be.nikiroo.jvcard.resources.Bundles;
21 import be.nikiroo.jvcard.tui.StringUtils;
22
23 /**
24 * This class implements a small server that can listen for requests to
25 * synchronise, get and put {@link Card}s.
26 *
27 * <p>
28 * It is <b>NOT</b> secured in any way (it even is nice enough to give you a
29 * help message when you connect in raw mode via nc on how to use it), so do
30 * <b>NOT</b> enable such a server to be accessible from internet. This is not
31 * safe. Use a ssh/openssl tunnel or similar.
32 * </p>
33 *
34 * @author niki
35 *
36 */
37 public class Server implements Runnable {
38 private ServerSocket ss;
39 private int port;
40 private boolean stop;
41 private File dataDir;
42
43 private Object clientsLock = new Object();
44 private List<SimpleSocket> clients = new LinkedList<SimpleSocket>();
45
46 private Object updateLock = new Object();
47
48 public static void main(String[] args) throws IOException {
49 Server server = new Server(4444);
50 server.run();
51 }
52
53 /**
54 * Create a new jVCard sercer on the given port.
55 *
56 * @param port
57 * the port to run on
58 *
59 * @throws IOException
60 * in case of IO error
61 */
62 public Server(int port) throws IOException {
63 this.port = port;
64 ResourceBundle bundle = Bundles.getBundle("remote");
65 try {
66 String dir = bundle.getString("SERVER_DATA_PATH");
67 dataDir = new File(dir);
68 dataDir.mkdir();
69
70 if (!dataDir.exists()) {
71 throw new IOException("Cannot open or create data store at: "
72 + dataDir);
73 }
74 } catch (Exception e) {
75 e.printStackTrace();
76 throw new IOException("Cannot open or create data store at: "
77 + dataDir, e);
78 }
79
80 ss = new ServerSocket(port);
81 }
82
83 /**
84 * Stop the server. It may take some time before returning, but will only
85 * return when the server is actually stopped.
86 */
87 public void stop() {
88 stop = true;
89 try {
90 SimpleSocket c = new SimpleSocket(new Socket((String) null, port),
91 "special STOP client");
92 c.open(true);
93 c.sendCommand(Verb.STOP);
94 c.close();
95 } catch (UnknownHostException e) {
96 e.printStackTrace();
97 } catch (IOException e) {
98 e.printStackTrace();
99 }
100
101 if (clients.size() > 0) {
102 try {
103 Thread.sleep(100);
104 } catch (InterruptedException e) {
105 }
106
107 if (clients.size() > 0) {
108 synchronized (clientsLock) {
109 for (SimpleSocket s : clients) {
110 System.err
111 .println("Forcefully closing client connection");
112 s.close();
113 }
114
115 clients.clear();
116 }
117 }
118 }
119 }
120
121 @Override
122 public void run() {
123 while (!stop) {
124 try {
125 final Socket s = ss.accept();
126 // TODO: thread pool?
127 new Thread(new Runnable() {
128 @Override
129 public void run() {
130 SimpleSocket ss = new SimpleSocket(s, "[request]");
131
132 addClient(ss);
133 try {
134 ss.open(false);
135
136 while (processCmd(ss))
137 ;
138
139 } catch (IOException e) {
140 e.printStackTrace();
141 } finally {
142 ss.close();
143 }
144 removeClient(ss);
145 }
146 }).start();
147 } catch (IOException ioe) {
148 ioe.printStackTrace();
149 }
150 }
151 }
152
153 /**
154 * Add a client to the current count.
155 *
156 * @return the client index number
157 */
158 private void addClient(SimpleSocket s) {
159 synchronized (clientsLock) {
160 clients.add(s);
161 }
162 }
163
164 /**
165 * Remove a client from the current count.
166 *
167 * @param index
168 * the client index number
169 */
170 private void removeClient(SimpleSocket s) {
171 synchronized (clientsLock) {
172 clients.remove(s);
173 }
174 }
175
176 /**
177 * Process a command.
178 *
179 * @param s
180 * the {@link SimpleSocket} from which to get the command to
181 * process
182 *
183 * @return TRUE if the client is ready for another command, FALSE when the
184 * client exited
185 *
186 * @throws IOException
187 * in case of IO error
188 */
189 private boolean processCmd(SimpleSocket s) throws IOException {
190 Command cmd = s.receiveCommand();
191 Command.Verb verb = cmd.getVerb();
192
193 if (verb == null)
194 return false;
195
196 boolean clientContinue = true;
197
198 System.out.println(s + " -> " + verb);
199
200 switch (verb) {
201 case STOP:
202 clientContinue = false;
203 break;
204 case VERSION:
205 s.sendCommand(Verb.VERSION);
206 break;
207 case TIME:
208 s.sendLine(StringUtils.fromTime(new Date().getTime()));
209 break;
210 case GET_CARD:
211 synchronized (updateLock) {
212 s.sendBlock(doGetCard(cmd.getParam()));
213 }
214 break;
215 case POST_CARD:
216 synchronized (updateLock) {
217 s.sendLine(doPostCard(cmd.getParam(), s.receiveBlock()));
218 }
219 break;
220 case PUT_CARD:
221 synchronized (updateLock) {
222 File vcf = getFile(cmd.getParam());
223 if (vcf == null) {
224 System.err
225 .println("Fail to update a card, file not available: "
226 + cmd.getParam());
227 clientContinue = false;
228 } else {
229 Card card = new Card(vcf, Format.VCard21);
230 try {
231 while (processContactCmd(s, card))
232 ;
233 card.save();
234 } catch (InvalidParameterException e) {
235 System.err
236 .println("Unsupported command received from a client connection, closing it: "
237 + verb + " (" + e.getMessage() + ")");
238 clientContinue = false;
239 }
240 }
241 }
242 break;
243 case DELETE_CARD:
244 // TODO
245 System.err
246 .println("Unsupported command received from a client connection, closing it: "
247 + verb);
248 clientContinue = false;
249 break;
250 case LIST:
251 for (File file : dataDir.listFiles()) {
252 if (cmd.getParam() == null || cmd.getParam().length() == 0
253 || file.getName().contains(cmd.getParam())) {
254 s.send(StringUtils.fromTime(file.lastModified()) + " "
255 + file.getName());
256 }
257 }
258 s.sendBlock();
259 break;
260 case HELP:
261 // TODO: i18n
262 s.send("The following commands are available:");
263 s.send("- TIME: get the server time");
264 s.send("- HELP: this help screen");
265 s.send("- LIST: list the available cards on this server");
266 s.send("- VERSION/GET/PUT/POST/DELETE/STOP: TODO");
267 s.sendBlock();
268 break;
269 default:
270 System.err
271 .println("Unsupported command received from a client connection, closing it: "
272 + verb);
273 clientContinue = false;
274 break;
275 }
276
277 return clientContinue;
278 }
279
280 /**
281 * Process a *_CONTACT subcommand.
282 *
283 * @param s
284 * the {@link SimpleSocket} to process
285 * @param card
286 * the target {@link Card}
287 *
288 * @return TRUE if the client is ready for another command, FALSE when the
289 * client is done
290 *
291 * @throws IOException
292 * in case of IO error
293 *
294 * @throw InvalidParameterException in case of invalid subcommand
295 */
296 private boolean processContactCmd(SimpleSocket s, Card card)
297 throws IOException {
298 Command cmd = s.receiveCommand();
299 Command.Verb verb = cmd.getVerb();
300
301 if (verb == null)
302 return false;
303
304 boolean clientContinue = true;
305
306 System.out.println(s + " -> " + verb);
307
308 switch (verb) {
309 case GET_CONTACT: {
310 Contact contact = card.getById(cmd.getParam());
311 if (contact != null)
312 s.sendBlock(Vcard21Parser.toStrings(contact, -1));
313 else
314 s.sendBlock();
315 break;
316 }
317 case POST_CONTACT: {
318 String uid = cmd.getParam();
319 Contact contact = card.getById(uid);
320 if (contact != null)
321 contact.delete();
322 List<Contact> list = Vcard21Parser.parseContact(s.receiveBlock());
323 if (list.size() > 0) {
324 contact = list.get(0);
325 contact.getPreferredData("UID").setValue(uid);
326 card.add(contact);
327 }
328 break;
329 }
330 case PUT_CONTACT: {
331 String uid = cmd.getParam();
332 Contact contact = card.getById(uid);
333 if (contact == null) {
334 throw new InvalidParameterException(
335 "Cannot find contact to modify for UID: " + uid);
336 }
337 while (processDataCmd(s, contact))
338 ;
339 break;
340 }
341 case DELETE_CONTACT: {
342 String uid = cmd.getParam();
343 Contact contact = card.getById(uid);
344 if (contact == null) {
345 throw new InvalidParameterException(
346 "Cannot find contact to delete for UID: " + uid);
347 }
348
349 contact.delete();
350 break;
351 }
352 case PUT_CARD: {
353 clientContinue = false;
354 break;
355 }
356 default: {
357 throw new InvalidParameterException("command invalid here");
358 }
359 }
360
361 return clientContinue;
362 }
363
364 /**
365 * Process a *_DATA subcommand.
366 *
367 * @param s
368 * the {@link SimpleSocket} to process
369 * @param card
370 * the target {@link Contact}
371 *
372 * @return TRUE if the client is ready for another command, FALSE when the
373 * client is done
374 *
375 * @throws IOException
376 * in case of IO error
377 *
378 * @throw InvalidParameterException in case of invalid subcommand
379 */
380 private boolean processDataCmd(SimpleSocket s, Contact contact)
381 throws IOException {
382 Command cmd = s.receiveCommand();
383 Command.Verb verb = cmd.getVerb();
384
385 if (verb == null)
386 return false;
387
388 boolean clientContinue = true;
389
390 System.out.println(s + " -> " + verb);
391
392 switch (verb) {
393 case GET_DATA: {
394 Data data = contact.getById(cmd.getParam());
395 if (data != null)
396 s.sendBlock(Vcard21Parser.toStrings(data));
397 else
398 s.sendBlock();
399 break;
400 }
401 case POST_DATA: {
402 String cstate = cmd.getParam();
403 Data data = null;
404 for (Data d : contact) {
405 if (cstate.equals(d.getContentState()))
406 data = d;
407 }
408
409 if (data != null)
410 data.delete();
411 List<Data> list = Vcard21Parser.parseData(s.receiveBlock());
412 if (list.size() > 0) {
413 contact.add(list.get(0));
414 }
415 break;
416 }
417 case DELETE_DATA: {
418 String cstate = cmd.getParam();
419 Data data = null;
420 for (Data d : contact) {
421 if (cstate.equals(d.getContentState()))
422 data = d;
423 }
424
425 if (data == null) {
426 throw new InvalidParameterException(
427 "Cannot find data to delete for content state: "
428 + cstate);
429 }
430
431 contact.delete();
432 break;
433 }
434 case PUT_CONTACT: {
435 clientContinue = false;
436 break;
437 }
438 default: {
439 throw new InvalidParameterException("command invalid here");
440 }
441 }
442
443 return clientContinue;
444 }
445
446 /**
447 * Return the serialised {@link Card} (with timestamp).
448 *
449 * @param name
450 * the resource name to load
451 *
452 * @return the serialised data
453 *
454 * @throws IOException
455 * in case of error
456 */
457 private List<String> doGetCard(String name) throws IOException {
458 List<String> lines = new LinkedList<String>();
459
460 File vcf = getFile(name);
461
462 if (vcf != null && vcf.exists()) {
463 Card card = new Card(vcf, Format.VCard21);
464
465 // timestamp:
466 lines.add(StringUtils.fromTime(card.getLastModified()));
467 lines.addAll(Vcard21Parser.toStrings(card));
468 }
469
470 return lines;
471 }
472
473 /**
474 * Save the data to the new given resource.
475 *
476 * @param name
477 * the resource name to save
478 * @param data
479 * the data to save
480 *
481 * @return the date of last modification
482 *
483 * @throws IOException
484 * in case of error
485 */
486 private String doPostCard(String name, List<String> data)
487 throws IOException {
488
489 File vcf = getFile(name);
490
491 if (vcf != null) {
492 Card card = new Card(Vcard21Parser.parseContact(data));
493 card.saveAs(vcf, Format.VCard21);
494
495 return StringUtils.fromTime(vcf.lastModified());
496 }
497
498 return "";
499 }
500
501 /**
502 * Return the {@link File} corresponding to the given resource name.
503 *
504 * @param name
505 * the resource name
506 *
507 * @return the corresponding {@link File} or NULL if the name was NULL or
508 * empty
509 */
510 private File getFile(String name) {
511 if (name != null && name.length() > 0) {
512 return new File(dataDir.getAbsolutePath() + File.separator + name);
513 }
514
515 return null;
516 }
517 }