60f323ad15df89fd148cf06a0cce8fab125cf2aa
[jvcard.git] / 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.HashMap;
11 import java.util.LinkedList;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.ResourceBundle;
15
16 import be.nikiroo.jvcard.Card;
17 import be.nikiroo.jvcard.Contact;
18 import be.nikiroo.jvcard.Data;
19 import be.nikiroo.jvcard.parsers.Format;
20 import be.nikiroo.jvcard.parsers.Vcard21Parser;
21 import be.nikiroo.jvcard.resources.Bundles;
22 import be.nikiroo.jvcard.resources.StringUtils;
23
24 /**
25 * This class implements a small server that can listen for requests to
26 * synchronise, get and put {@link Card}s.
27 *
28 * <p>
29 * It is <b>NOT</b> secured in any way (it even is nice enough to give you a
30 * help message when you connect in raw mode via nc on how to use it), so do
31 * <b>NOT</b> enable such a server to be accessible from internet. This is not
32 * safe. Use a ssh/openssl tunnel or similar.
33 * </p>
34 *
35 * @author niki
36 *
37 */
38 public class Server implements Runnable {
39 private ServerSocket ss;
40 private int port;
41 private boolean stop;
42 private File dataDir;
43
44 private Object clientsLock = new Object();
45 private List<SimpleSocket> clients = new LinkedList<SimpleSocket>();
46
47 private Object updateLock = new Object();
48 private Map<File, Integer> updates = new HashMap<File, Integer>();
49
50 /**
51 * Create a new jVCard server on the given port.
52 *
53 * @param port
54 * the port to run on
55 *
56 * @throws IOException
57 * in case of IO error
58 */
59 public Server(int port) throws IOException {
60 this.port = port;
61 ResourceBundle bundle = Bundles.getBundle("remote");
62 try {
63 String dir = bundle.getString("SERVER_DATA_PATH");
64 dataDir = new File(dir);
65 dataDir.mkdir();
66
67 if (!dataDir.exists()) {
68 throw new IOException("Cannot open or create data store at: "
69 + dataDir);
70 }
71 } catch (Exception e) {
72 e.printStackTrace();
73 throw new IOException("Cannot open or create data store at: "
74 + dataDir, e);
75 }
76
77 ss = new ServerSocket(port);
78 }
79
80 /**
81 * Stop the server. It may take some time before returning, but will only
82 * return when the server is actually stopped.
83 */
84 public void stop() {
85 stop = true;
86 try {
87 SimpleSocket c = new SimpleSocket(new Socket((String) null, port),
88 "special STOP client");
89 c.open(true);
90 c.sendCommand(Command.STOP);
91 c.close();
92 } catch (UnknownHostException e) {
93 e.printStackTrace();
94 } catch (IOException e) {
95 e.printStackTrace();
96 }
97
98 if (clients.size() > 0) {
99 try {
100 Thread.sleep(100);
101 } catch (InterruptedException e) {
102 }
103
104 if (clients.size() > 0) {
105 synchronized (clientsLock) {
106 for (SimpleSocket s : clients) {
107 System.err
108 .println("Forcefully closing client connection");
109 s.close();
110 }
111
112 clients.clear();
113 }
114 }
115 }
116 }
117
118 @Override
119 public void run() {
120 while (!stop) {
121 try {
122 final Socket s = ss.accept();
123 // TODO: thread pool?
124 new Thread(new Runnable() {
125 @Override
126 public void run() {
127 SimpleSocket ss = new SimpleSocket(s, "[request]");
128
129 addClient(ss);
130 try {
131 ss.open(false);
132
133 while (processCmd(ss))
134 ;
135
136 } catch (IOException e) {
137 e.printStackTrace();
138 } finally {
139 ss.close();
140 }
141 removeClient(ss);
142 }
143 }).start();
144 } catch (IOException ioe) {
145 ioe.printStackTrace();
146 }
147 }
148 }
149
150 /**
151 * Add a client to the current count.
152 *
153 * @return the client index number
154 */
155 private void addClient(SimpleSocket s) {
156 synchronized (clientsLock) {
157 clients.add(s);
158 }
159 }
160
161 /**
162 * Remove a client from the current count.
163 *
164 * @param index
165 * the client index number
166 */
167 private void removeClient(SimpleSocket s) {
168 synchronized (clientsLock) {
169 clients.remove(s);
170 }
171 }
172
173 /**
174 * Process a first-level command.
175 *
176 * @param s
177 * the {@link SimpleSocket} from which to get the command to
178 * process
179 *
180 * @return TRUE if the client is ready for another command, FALSE when the
181 * client exited
182 *
183 * @throws IOException
184 * in case of IO error
185 */
186 private boolean processCmd(SimpleSocket s) throws IOException {
187 CommandInstance cmd = s.receiveCommand();
188 Command command = cmd.getCommand();
189
190 if (command == null)
191 return false;
192
193 boolean clientContinue = true;
194
195 System.out.println(s + " -> " + command
196 + (cmd.getParam() == null ? "" : " " + cmd.getParam()));
197
198 switch (command) {
199 case STOP: {
200 clientContinue = false;
201 break;
202 }
203 case VERSION: {
204 s.sendLine("" + SimpleSocket.CURRENT_VERSION);
205 break;
206 }
207 case TIME: {
208 s.sendLine(StringUtils.fromTime(new Date().getTime()));
209 break;
210 }
211 case SELECT: {
212 String name = cmd.getParam();
213 File file = new File(dataDir.getAbsolutePath() + File.separator
214 + name);
215 if (name == null || name.length() == 0 || !file.exists()) {
216 System.err
217 .println("SELECT: resource not found, closing connection: "
218 + name);
219 clientContinue = false;
220 } else {
221 synchronized (updateLock) {
222 for (File f : updates.keySet()) {
223 if (f.getCanonicalPath()
224 .equals(file.getCanonicalPath())) {
225 file = f;
226 break;
227 }
228 }
229
230 if (!updates.containsKey(file))
231 updates.put(file, 0);
232 updates.put(file, updates.get(file) + 1);
233 }
234
235 synchronized (file) {
236 try {
237 s.sendLine(StringUtils.fromTime(file.lastModified()));
238
239 while (processLockedCmd(s, name))
240 ;
241 } catch (InvalidParameterException e) {
242 System.err
243 .println("Unsupported command received from a client connection, closing it: "
244 + command + " (" + e.getMessage() + ")");
245 clientContinue = false;
246 }
247 }
248
249 synchronized (updateLock) {
250 int num = updates.get(file) - 1;
251 if (num == 0) {
252 updates.remove(file);
253 } else {
254 updates.put(file, num);
255 }
256 }
257 }
258 break;
259 }
260 case LIST_CARD: {
261 for (File file : dataDir.listFiles()) {
262 if (cmd.getParam() == null
263 || cmd.getParam().length() == 0
264 || file.getName().toLowerCase()
265 .contains(cmd.getParam().toLowerCase())) {
266 s.send(StringUtils.fromTime(file.lastModified()) + " "
267 + file.getName());
268 }
269 }
270 s.sendBlock();
271 break;
272 }
273 case HELP: {
274 // TODO: i18n
275 s.send("The following commands are available:");
276 s.send("- TIME: get the server time");
277 s.send("- HELP: this help screen");
278 s.send("- LIST_CARD: list the available cards on this server");
279 s.send("- VERSION/GET_*/PUT_*/POST_*/DELETE_*/STOP: TODO");
280 s.sendBlock();
281 break;
282 }
283 default: {
284 System.err
285 .println("Unsupported command received from a client connection, closing it: "
286 + command);
287 clientContinue = false;
288 break;
289 }
290 }
291
292 return clientContinue;
293 }
294
295 /**
296 * Process a subcommand while protected for resource <tt>name</tt>.
297 *
298 * @param s
299 * the {@link SimpleSocket} to process
300 *
301 * @param name
302 * the resource that is protected (and to target)
303 *
304 * @return TRUE if the client is ready for another command, FALSE when the
305 * client is done
306 *
307 * @throws IOException
308 * in case of IO error
309 *
310 * @throw InvalidParameterException in case of invalid subcommand
311 */
312 private boolean processLockedCmd(SimpleSocket s, String name)
313 throws IOException {
314 CommandInstance cmd = s.receiveCommand();
315 Command command = cmd.getCommand();
316
317 if (command == null)
318 return false;
319
320 boolean clientContinue = true;
321
322 System.out.println(s + " -> " + command);
323
324 switch (command) {
325 case GET_CARD: {
326 s.sendBlock(doGetCard(name));
327 break;
328 }
329 case POST_CARD: {
330 s.sendLine(doPostCard(name, s.receiveBlock()));
331 break;
332 }
333 case PUT_CARD: {
334 File vcf = getFile(name);
335 if (vcf == null) {
336 System.err
337 .println("Fail to update a card, file not available: "
338 + name);
339 clientContinue = false;
340 } else {
341 Card card = new Card(vcf, Format.VCard21);
342 try {
343 while (processContactCmd(s, card))
344 ;
345 card.save();
346 s.sendLine(StringUtils.fromTime(card.getLastModified()));
347 } catch (InvalidParameterException e) {
348 System.err
349 .println("Unsupported command received from a client connection, closing it: "
350 + command + " (" + e.getMessage() + ")");
351 clientContinue = false;
352 }
353 }
354 break;
355 }
356 case DELETE_CARD: {
357 // TODO
358 System.err
359 .println("Unsupported command received from a client connection, closing it: "
360 + command);
361 clientContinue = false;
362 break;
363 }
364 case SELECT: {
365 clientContinue = false;
366 break;
367 }
368 default: {
369 throw new InvalidParameterException("command invalid here: "
370 + command);
371 }
372 }
373
374 return clientContinue;
375 }
376
377 /**
378 * Process a *_CONTACT subcommand.
379 *
380 * @param s
381 * the {@link SimpleSocket} to process
382 * @param card
383 * the target {@link Card}
384 *
385 * @return TRUE if the client is ready for another command, FALSE when the
386 * client is done
387 *
388 * @throws IOException
389 * in case of IO error
390 *
391 * @throw InvalidParameterException in case of invalid subcommand
392 */
393 private boolean processContactCmd(SimpleSocket s, Card card)
394 throws IOException {
395 CommandInstance cmd = s.receiveCommand();
396 Command command = cmd.getCommand();
397
398 if (command == null)
399 return false;
400
401 boolean clientContinue = true;
402
403 System.out.println(s + " -> " + command);
404
405 switch (command) {
406 case GET_CONTACT: {
407 Contact contact = card.getById(cmd.getParam());
408 if (contact != null)
409 s.sendBlock(Vcard21Parser.toStrings(contact, -1));
410 else
411 s.sendBlock();
412 break;
413 }
414 case POST_CONTACT: {
415 List<Contact> list = Vcard21Parser.parseContact(s.receiveBlock());
416 if (list.size() > 0) {
417 Contact newContact = list.get(0);
418 String uid = newContact.getPreferredDataValue("UID");
419 Contact oldContact = card.getById(uid);
420 if (oldContact != null)
421 oldContact.delete();
422 card.add(newContact);
423 }
424
425 break;
426 }
427 case PUT_CONTACT: {
428 String uid = cmd.getParam();
429 Contact contact = card.getById(uid);
430 if (contact == null) {
431 throw new InvalidParameterException(
432 "Cannot find contact to modify for UID: " + uid);
433 }
434 while (processDataCmd(s, contact))
435 ;
436 break;
437 }
438 case DELETE_CONTACT: {
439 String uid = cmd.getParam();
440 Contact contact = card.getById(uid);
441 if (contact == null) {
442 throw new InvalidParameterException(
443 "Cannot find contact to delete for UID: " + uid);
444 }
445
446 contact.delete();
447 break;
448 }
449 case HASH_CONTACT: {
450 String uid = cmd.getParam();
451 Contact contact = card.getById(uid);
452
453 if (contact == null) {
454 s.sendBlock();
455 } else {
456 s.sendLine(contact.getContentState(true));
457 }
458 break;
459 }
460 case LIST_CONTACT: {
461 for (Contact contact : card) {
462 if (cmd.getParam() == null
463 || cmd.getParam().length() == 0
464 || (contact.getPreferredDataValue("FN") + contact
465 .getPreferredDataValue("N")).toLowerCase()
466 .contains(cmd.getParam().toLowerCase())) {
467 s.send(contact.getContentState(true) + " "
468 + contact.getId());
469 }
470 }
471 s.sendBlock();
472 break;
473 }
474 case PUT_CARD: {
475 clientContinue = false;
476 break;
477 }
478 default: {
479 throw new InvalidParameterException("command invalid here: "
480 + command);
481 }
482 }
483
484 return clientContinue;
485 }
486
487 /**
488 * Process a *_DATA subcommand.
489 *
490 * @param s
491 * the {@link SimpleSocket} to process
492 * @param card
493 * the target {@link Contact}
494 *
495 * @return TRUE if the client is ready for another command, FALSE when the
496 * client is done
497 *
498 * @throws IOException
499 * in case of IO error
500 *
501 * @throw InvalidParameterException in case of invalid subcommand
502 */
503 private boolean processDataCmd(SimpleSocket s, Contact contact)
504 throws IOException {
505 CommandInstance cmd = s.receiveCommand();
506 Command command = cmd.getCommand();
507
508 if (command == null)
509 return false;
510
511 boolean clientContinue = true;
512
513 System.out.println(s + " -> " + command);
514
515 switch (command) {
516 case GET_DATA: {
517 for (Data data : contact) {
518 if (data.getName().equals(cmd.getParam())) {
519 for (String line : Vcard21Parser.toStrings(data)) {
520 s.send(line);
521 }
522 }
523 }
524 s.sendBlock();
525 break;
526 }
527 case POST_DATA: {
528 String cstate = cmd.getParam();
529 Data data = null;
530 for (Data d : contact) {
531 if (cstate.equals(d.getContentState(true)))
532 data = d;
533 }
534
535 if (data != null)
536 data.delete();
537 List<Data> list = Vcard21Parser.parseData(s.receiveBlock());
538 if (list.size() > 0) {
539 contact.add(list.get(0));
540 }
541 break;
542 }
543 case DELETE_DATA: {
544 String cstate = cmd.getParam();
545 Data data = null;
546 for (Data d : contact) {
547 if (cstate.equals(d.getContentState(true)))
548 data = d;
549 }
550
551 if (data == null) {
552 throw new InvalidParameterException(
553 "Cannot find data to delete for content state: "
554 + cstate);
555 }
556
557 contact.delete();
558 break;
559 }
560 case HASH_DATA: {
561 for (Data data : contact) {
562 if (data.getId().equals(cmd.getParam())) {
563 s.send(data.getContentState(true));
564 }
565 }
566 s.sendBlock();
567 break;
568 }
569 case LIST_DATA: {
570 for (Data data : contact) {
571 if (cmd.getParam() == null
572 || cmd.getParam().length() == 0
573 || data.getName().toLowerCase()
574 .contains(cmd.getParam().toLowerCase())) {
575 s.send(data.getContentState(true) + " " + data.getName());
576 }
577 }
578 s.sendBlock();
579 break;
580 }
581 case PUT_CONTACT: {
582 clientContinue = false;
583 break;
584 }
585 default: {
586 throw new InvalidParameterException("command invalid here: "
587 + command);
588 }
589 }
590
591 return clientContinue;
592 }
593
594 /**
595 * Return the serialised {@link Card} (with timestamp).
596 *
597 * @param name
598 * the resource name to load
599 *
600 * @return the serialised data
601 *
602 * @throws IOException
603 * in case of error
604 */
605 private List<String> doGetCard(String name) throws IOException {
606 List<String> lines = new LinkedList<String>();
607
608 File vcf = getFile(name);
609
610 if (vcf != null && vcf.exists()) {
611 Card card = new Card(vcf, Format.VCard21);
612
613 // timestamp + data
614 lines.add(StringUtils.fromTime(card.getLastModified()));
615 lines.addAll(Vcard21Parser.toStrings(card));
616 }
617
618 return lines;
619 }
620
621 /**
622 * Save the data to the new given resource.
623 *
624 * @param name
625 * the resource name to save
626 * @param data
627 * the data to save
628 *
629 * @return the date of last modification
630 *
631 * @throws IOException
632 * in case of error
633 */
634 private String doPostCard(String name, List<String> data)
635 throws IOException {
636
637 File vcf = getFile(name);
638
639 if (vcf != null) {
640 Card card = new Card(Vcard21Parser.parseContact(data));
641 card.saveAs(vcf, Format.VCard21);
642
643 return StringUtils.fromTime(vcf.lastModified());
644 }
645
646 return "";
647 }
648
649 /**
650 * Return the {@link File} corresponding to the given resource name.
651 *
652 * @param name
653 * the resource name
654 *
655 * @return the corresponding {@link File} or NULL if the name was NULL or
656 * empty
657 */
658 private File getFile(String name) {
659 if (name != null && name.length() > 0) {
660 return new File(dataDir.getAbsolutePath() + File.separator + name);
661 }
662
663 return null;
664 }
665 }