Remote: jdoc + description + some fixes:
[jvcard.git] / src / be / nikiroo / jvcard / remote / Server.java
CommitLineData
a046fa49
NR
1package be.nikiroo.jvcard.remote;
2
3import java.io.File;
4import java.io.IOException;
5import java.net.ServerSocket;
6import java.net.Socket;
7import java.net.UnknownHostException;
0b6140e4 8import java.security.InvalidParameterException;
a046fa49 9import java.util.Date;
4298276a 10import java.util.HashMap;
a046fa49
NR
11import java.util.LinkedList;
12import java.util.List;
4298276a 13import java.util.Map;
a046fa49
NR
14import java.util.ResourceBundle;
15
16import be.nikiroo.jvcard.Card;
0b6140e4
NR
17import be.nikiroo.jvcard.Contact;
18import be.nikiroo.jvcard.Data;
a046fa49 19import be.nikiroo.jvcard.parsers.Format;
0b6140e4 20import be.nikiroo.jvcard.parsers.Vcard21Parser;
a046fa49 21import be.nikiroo.jvcard.resources.Bundles;
7da41ecd 22import be.nikiroo.jvcard.resources.StringUtils;
a046fa49
NR
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 */
38public 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
0b6140e4 47 private Object updateLock = new Object();
4298276a 48 private Map<File, Integer> updates = new HashMap<File, Integer>();
a046fa49 49
a046fa49 50 /**
7da41ecd 51 * Create a new jVCard server on the given port.
a046fa49
NR
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);
845fb1d7 90 c.sendCommand(Command.STOP);
a046fa49
NR
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() {
0b6140e4
NR
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);
a046fa49
NR
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 /**
4298276a 174 * Process a first-level command.
a046fa49
NR
175 *
176 * @param s
0b6140e4
NR
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
a046fa49 185 */
0b6140e4 186 private boolean processCmd(SimpleSocket s) throws IOException {
845fb1d7
NR
187 CommandInstance cmd = s.receiveCommand();
188 Command command = cmd.getCommand();
0b6140e4 189
845fb1d7 190 if (command == null)
0b6140e4
NR
191 return false;
192
193 boolean clientContinue = true;
194
845fb1d7 195 System.out.println(s + " -> " + command
4298276a 196 + (cmd.getParam() == null ? "" : " " + cmd.getParam()));
0b6140e4 197
845fb1d7 198 switch (command) {
4298276a 199 case STOP: {
0b6140e4
NR
200 clientContinue = false;
201 break;
4298276a
NR
202 }
203 case VERSION: {
e4444b0b 204 s.sendLine("" + SimpleSocket.CURRENT_VERSION);
0b6140e4 205 break;
4298276a
NR
206 }
207 case TIME: {
0b6140e4
NR
208 s.sendLine(StringUtils.fromTime(new Date().getTime()));
209 break;
4298276a
NR
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) {
0b6140e4 236 try {
4298276a
NR
237 s.sendLine(StringUtils.fromTime(file.lastModified()));
238
239 while (processLockedCmd(s, name))
0b6140e4 240 ;
0b6140e4
NR
241 } catch (InvalidParameterException e) {
242 System.err
243 .println("Unsupported command received from a client connection, closing it: "
845fb1d7 244 + command + " (" + e.getMessage() + ")");
0b6140e4
NR
245 clientContinue = false;
246 }
a046fa49 247 }
4298276a
NR
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 }
a046fa49 257 }
0b6140e4 258 break;
4298276a 259 }
845fb1d7 260 case LIST_CARD: {
0b6140e4 261 for (File file : dataDir.listFiles()) {
e4444b0b
NR
262 if (cmd.getParam() == null
263 || cmd.getParam().length() == 0
264 || file.getName().toLowerCase()
265 .contains(cmd.getParam().toLowerCase())) {
0b6140e4
NR
266 s.send(StringUtils.fromTime(file.lastModified()) + " "
267 + file.getName());
268 }
269 }
270 s.sendBlock();
271 break;
4298276a
NR
272 }
273 case HELP: {
0b6140e4
NR
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");
e4444b0b
NR
278 s.send("- LIST_CARD: list the available cards on this server");
279 s.send("- VERSION/GET_*/PUT_*/POST_*/DELETE_*/STOP: TODO");
0b6140e4
NR
280 s.sendBlock();
281 break;
4298276a
NR
282 }
283 default: {
284 System.err
285 .println("Unsupported command received from a client connection, closing it: "
845fb1d7 286 + command);
4298276a
NR
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 {
845fb1d7
NR
314 CommandInstance cmd = s.receiveCommand();
315 Command command = cmd.getCommand();
4298276a 316
845fb1d7 317 if (command == null)
4298276a
NR
318 return false;
319
320 boolean clientContinue = true;
321
845fb1d7 322 System.out.println(s + " -> " + command);
4298276a 323
845fb1d7 324 switch (command) {
4298276a
NR
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();
845fb1d7 346 s.sendLine(StringUtils.fromTime(card.getLastModified()));
4298276a
NR
347 } catch (InvalidParameterException e) {
348 System.err
349 .println("Unsupported command received from a client connection, closing it: "
845fb1d7 350 + command + " (" + e.getMessage() + ")");
4298276a
NR
351 clientContinue = false;
352 }
353 }
354 break;
355 }
356 case DELETE_CARD: {
357 // TODO
0b6140e4
NR
358 System.err
359 .println("Unsupported command received from a client connection, closing it: "
845fb1d7 360 + command);
0b6140e4
NR
361 clientContinue = false;
362 break;
363 }
4298276a
NR
364 case SELECT: {
365 clientContinue = false;
366 break;
367 }
368 default: {
e4444b0b
NR
369 throw new InvalidParameterException("command invalid here: "
370 + command);
4298276a
NR
371 }
372 }
0b6140e4
NR
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 {
845fb1d7
NR
395 CommandInstance cmd = s.receiveCommand();
396 Command command = cmd.getCommand();
0b6140e4 397
845fb1d7 398 if (command == null)
0b6140e4
NR
399 return false;
400
401 boolean clientContinue = true;
402
845fb1d7 403 System.out.println(s + " -> " + command);
0b6140e4 404
845fb1d7 405 switch (command) {
0b6140e4
NR
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: {
0b6140e4
NR
415 List<Contact> list = Vcard21Parser.parseContact(s.receiveBlock());
416 if (list.size() > 0) {
e4444b0b
NR
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);
0b6140e4 423 }
e4444b0b 424
0b6140e4
NR
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 }
845fb1d7
NR
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());
457 }
458 break;
459 }
460 case LIST_CONTACT: {
461 for (Contact contact : card) {
e4444b0b
NR
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() + " " + contact.getId());
468 }
845fb1d7
NR
469 }
470 s.sendBlock();
471 break;
472 }
0b6140e4
NR
473 case PUT_CARD: {
474 clientContinue = false;
475 break;
476 }
477 default: {
e4444b0b
NR
478 throw new InvalidParameterException("command invalid here: "
479 + command);
0b6140e4 480 }
a046fa49
NR
481 }
482
0b6140e4
NR
483 return clientContinue;
484 }
485
486 /**
487 * Process a *_DATA subcommand.
488 *
489 * @param s
490 * the {@link SimpleSocket} to process
491 * @param card
492 * the target {@link Contact}
493 *
494 * @return TRUE if the client is ready for another command, FALSE when the
495 * client is done
496 *
497 * @throws IOException
498 * in case of IO error
499 *
500 * @throw InvalidParameterException in case of invalid subcommand
501 */
502 private boolean processDataCmd(SimpleSocket s, Contact contact)
503 throws IOException {
845fb1d7
NR
504 CommandInstance cmd = s.receiveCommand();
505 Command command = cmd.getCommand();
0b6140e4 506
845fb1d7 507 if (command == null)
0b6140e4
NR
508 return false;
509
510 boolean clientContinue = true;
511
845fb1d7 512 System.out.println(s + " -> " + command);
0b6140e4 513
845fb1d7 514 switch (command) {
0b6140e4 515 case GET_DATA: {
845fb1d7
NR
516 for (Data data : contact) {
517 if (data.getName().equals(cmd.getParam())) {
518 for (String line : Vcard21Parser.toStrings(data)) {
519 s.send(line);
520 }
521 }
522 }
523 s.sendBlock();
0b6140e4
NR
524 break;
525 }
526 case POST_DATA: {
527 String cstate = cmd.getParam();
528 Data data = null;
529 for (Data d : contact) {
530 if (cstate.equals(d.getContentState()))
531 data = d;
532 }
533
534 if (data != null)
535 data.delete();
536 List<Data> list = Vcard21Parser.parseData(s.receiveBlock());
537 if (list.size() > 0) {
538 contact.add(list.get(0));
539 }
540 break;
541 }
542 case DELETE_DATA: {
543 String cstate = cmd.getParam();
544 Data data = null;
545 for (Data d : contact) {
546 if (cstate.equals(d.getContentState()))
547 data = d;
548 }
549
550 if (data == null) {
551 throw new InvalidParameterException(
552 "Cannot find data to delete for content state: "
553 + cstate);
554 }
555
556 contact.delete();
557 break;
558 }
845fb1d7
NR
559 case HASH_DATA: {
560 for (Data data : contact) {
561 if (data.getId().equals(cmd.getParam())) {
562 s.send(data.getContentState());
563 }
564 }
565 s.sendBlock();
566 break;
567 }
568 case LIST_DATA: {
569 for (Data data : contact) {
e4444b0b
NR
570 if (cmd.getParam() == null
571 || cmd.getParam().length() == 0
572 || data.getName().toLowerCase()
573 .contains(cmd.getParam().toLowerCase())) {
574 s.send(data.getContentState() + " " + data.getName());
575 }
845fb1d7
NR
576 }
577 s.sendBlock();
578 break;
579 }
0b6140e4
NR
580 case PUT_CONTACT: {
581 clientContinue = false;
582 break;
583 }
584 default: {
e4444b0b
NR
585 throw new InvalidParameterException("command invalid here: "
586 + command);
0b6140e4
NR
587 }
588 }
589
590 return clientContinue;
a046fa49
NR
591 }
592
593 /**
594 * Return the serialised {@link Card} (with timestamp).
595 *
596 * @param name
597 * the resource name to load
598 *
599 * @return the serialised data
600 *
601 * @throws IOException
602 * in case of error
603 */
604 private List<String> doGetCard(String name) throws IOException {
605 List<String> lines = new LinkedList<String>();
606
0b6140e4 607 File vcf = getFile(name);
a046fa49 608
0b6140e4
NR
609 if (vcf != null && vcf.exists()) {
610 Card card = new Card(vcf, Format.VCard21);
a046fa49 611
e4444b0b 612 // timestamp + data
0b6140e4
NR
613 lines.add(StringUtils.fromTime(card.getLastModified()));
614 lines.addAll(Vcard21Parser.toStrings(card));
a046fa49
NR
615 }
616
617 return lines;
618 }
619
620 /**
621 * Save the data to the new given resource.
622 *
623 * @param name
624 * the resource name to save
625 * @param data
626 * the data to save
627 *
cf77cb35
NR
628 * @return the date of last modification
629 *
a046fa49
NR
630 * @throws IOException
631 * in case of error
632 */
cf77cb35
NR
633 private String doPostCard(String name, List<String> data)
634 throws IOException {
a046fa49 635
0b6140e4
NR
636 File vcf = getFile(name);
637
638 if (vcf != null) {
639 Card card = new Card(Vcard21Parser.parseContact(data));
a046fa49 640 card.saveAs(vcf, Format.VCard21);
cf77cb35
NR
641
642 return StringUtils.fromTime(vcf.lastModified());
a046fa49 643 }
cf77cb35
NR
644
645 return "";
a046fa49 646 }
0b6140e4
NR
647
648 /**
649 * Return the {@link File} corresponding to the given resource name.
650 *
651 * @param name
652 * the resource name
653 *
654 * @return the corresponding {@link File} or NULL if the name was NULL or
655 * empty
656 */
657 private File getFile(String name) {
658 if (name != null && name.length() > 0) {
659 return new File(dataDir.getAbsolutePath() + File.separator + name);
660 }
661
662 return null;
663 }
a046fa49 664}