567df69c83f12b735d33a2c330ff6b4a96a657f3
[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.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.sendCommand(Command.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 || cmd.getParam().length() == 0
263 || file.getName().contains(cmd.getParam())) {
264 s.send(StringUtils.fromTime(file.lastModified()) + " "
265 + file.getName());
266 }
267 }
268 s.sendBlock();
269 break;
270 }
271 case HELP: {
272 // TODO: i18n
273 s.send("The following commands are available:");
274 s.send("- TIME: get the server time");
275 s.send("- HELP: this help screen");
276 s.send("- LIST: list the available cards on this server");
277 s.send("- VERSION/GET/PUT/POST/DELETE/STOP: TODO");
278 s.sendBlock();
279 break;
280 }
281 default: {
282 System.err
283 .println("Unsupported command received from a client connection, closing it: "
284 + command);
285 clientContinue = false;
286 break;
287 }
288 }
289
290 return clientContinue;
291 }
292
293 /**
294 * Process a subcommand while protected for resource <tt>name</tt>.
295 *
296 * @param s
297 * the {@link SimpleSocket} to process
298 *
299 * @param name
300 * the resource that is protected (and to target)
301 *
302 * @return TRUE if the client is ready for another command, FALSE when the
303 * client is done
304 *
305 * @throws IOException
306 * in case of IO error
307 *
308 * @throw InvalidParameterException in case of invalid subcommand
309 */
310 private boolean processLockedCmd(SimpleSocket s, String name)
311 throws IOException {
312 CommandInstance cmd = s.receiveCommand();
313 Command command = cmd.getCommand();
314
315 if (command == null)
316 return false;
317
318 boolean clientContinue = true;
319
320 System.out.println(s + " -> " + command);
321
322 switch (command) {
323 case GET_CARD: {
324 s.sendBlock(doGetCard(name));
325 break;
326 }
327 case POST_CARD: {
328 s.sendLine(doPostCard(name, s.receiveBlock()));
329 break;
330 }
331 case PUT_CARD: {
332 File vcf = getFile(name);
333 if (vcf == null) {
334 System.err
335 .println("Fail to update a card, file not available: "
336 + name);
337 clientContinue = false;
338 } else {
339 Card card = new Card(vcf, Format.VCard21);
340 try {
341 while (processContactCmd(s, card))
342 ;
343 card.save();
344 s.sendLine(StringUtils.fromTime(card.getLastModified()));
345 } catch (InvalidParameterException e) {
346 System.err
347 .println("Unsupported command received from a client connection, closing it: "
348 + command + " (" + e.getMessage() + ")");
349 clientContinue = false;
350 }
351 }
352 break;
353 }
354 case DELETE_CARD: {
355 // TODO
356 System.err
357 .println("Unsupported command received from a client connection, closing it: "
358 + command);
359 clientContinue = false;
360 break;
361 }
362 case SELECT: {
363 clientContinue = false;
364 break;
365 }
366 default: {
367 throw new InvalidParameterException("command invalid here");
368 }
369 }
370
371 return clientContinue;
372 }
373
374 /**
375 * Process a *_CONTACT subcommand.
376 *
377 * @param s
378 * the {@link SimpleSocket} to process
379 * @param card
380 * the target {@link Card}
381 *
382 * @return TRUE if the client is ready for another command, FALSE when the
383 * client is done
384 *
385 * @throws IOException
386 * in case of IO error
387 *
388 * @throw InvalidParameterException in case of invalid subcommand
389 */
390 private boolean processContactCmd(SimpleSocket s, Card card)
391 throws IOException {
392 CommandInstance cmd = s.receiveCommand();
393 Command command = cmd.getCommand();
394
395 if (command == null)
396 return false;
397
398 boolean clientContinue = true;
399
400 System.out.println(s + " -> " + command);
401
402 switch (command) {
403 case GET_CONTACT: {
404 Contact contact = card.getById(cmd.getParam());
405 if (contact != null)
406 s.sendBlock(Vcard21Parser.toStrings(contact, -1));
407 else
408 s.sendBlock();
409 break;
410 }
411 case POST_CONTACT: {
412 String uid = cmd.getParam();
413 Contact contact = card.getById(uid);
414 if (contact != null)
415 contact.delete();
416 List<Contact> list = Vcard21Parser.parseContact(s.receiveBlock());
417 if (list.size() > 0) {
418 contact = list.get(0);
419 contact.getPreferredData("UID").setValue(uid);
420 card.add(contact);
421 }
422 break;
423 }
424 case PUT_CONTACT: {
425 String uid = cmd.getParam();
426 Contact contact = card.getById(uid);
427 if (contact == null) {
428 throw new InvalidParameterException(
429 "Cannot find contact to modify for UID: " + uid);
430 }
431 while (processDataCmd(s, contact))
432 ;
433 break;
434 }
435 case DELETE_CONTACT: {
436 String uid = cmd.getParam();
437 Contact contact = card.getById(uid);
438 if (contact == null) {
439 throw new InvalidParameterException(
440 "Cannot find contact to delete for UID: " + uid);
441 }
442
443 contact.delete();
444 break;
445 }
446 case HASH_CONTACT: {
447 String uid = cmd.getParam();
448 Contact contact = card.getById(uid);
449
450 if (contact == null) {
451 s.sendBlock();
452 } else {
453 s.sendLine(contact.getContentState());
454 }
455 break;
456 }
457 case LIST_CONTACT: {
458 for (Contact contact : card) {
459 s.send(contact.getContentState() + " " + contact.getId());
460 }
461 s.sendBlock();
462 break;
463 }
464 case PUT_CARD: {
465 clientContinue = false;
466 break;
467 }
468 default: {
469 throw new InvalidParameterException("command invalid here");
470 }
471 }
472
473 return clientContinue;
474 }
475
476 /**
477 * Process a *_DATA subcommand.
478 *
479 * @param s
480 * the {@link SimpleSocket} to process
481 * @param card
482 * the target {@link Contact}
483 *
484 * @return TRUE if the client is ready for another command, FALSE when the
485 * client is done
486 *
487 * @throws IOException
488 * in case of IO error
489 *
490 * @throw InvalidParameterException in case of invalid subcommand
491 */
492 private boolean processDataCmd(SimpleSocket s, Contact contact)
493 throws IOException {
494 CommandInstance cmd = s.receiveCommand();
495 Command command = cmd.getCommand();
496
497 if (command == null)
498 return false;
499
500 boolean clientContinue = true;
501
502 System.out.println(s + " -> " + command);
503
504 switch (command) {
505 case GET_DATA: {
506 for (Data data : contact) {
507 if (data.getName().equals(cmd.getParam())) {
508 for (String line : Vcard21Parser.toStrings(data)) {
509 s.send(line);
510 }
511 }
512 }
513 s.sendBlock();
514 break;
515 }
516 case POST_DATA: {
517 String cstate = cmd.getParam();
518 Data data = null;
519 for (Data d : contact) {
520 if (cstate.equals(d.getContentState()))
521 data = d;
522 }
523
524 if (data != null)
525 data.delete();
526 List<Data> list = Vcard21Parser.parseData(s.receiveBlock());
527 if (list.size() > 0) {
528 contact.add(list.get(0));
529 }
530 break;
531 }
532 case DELETE_DATA: {
533 String cstate = cmd.getParam();
534 Data data = null;
535 for (Data d : contact) {
536 if (cstate.equals(d.getContentState()))
537 data = d;
538 }
539
540 if (data == null) {
541 throw new InvalidParameterException(
542 "Cannot find data to delete for content state: "
543 + cstate);
544 }
545
546 contact.delete();
547 break;
548 }
549 case HASH_DATA: {
550 for (Data data : contact) {
551 if (data.getId().equals(cmd.getParam())) {
552 s.send(data.getContentState());
553 }
554 }
555 s.sendBlock();
556 break;
557 }
558 case LIST_DATA: {
559 for (Data data : contact) {
560 s.send(data.getContentState() + " " + data.getName());
561 }
562 s.sendBlock();
563 break;
564 }
565 case PUT_CONTACT: {
566 clientContinue = false;
567 break;
568 }
569 default: {
570 throw new InvalidParameterException("command invalid here");
571 }
572 }
573
574 return clientContinue;
575 }
576
577 /**
578 * Return the serialised {@link Card} (with timestamp).
579 *
580 * @param name
581 * the resource name to load
582 *
583 * @return the serialised data
584 *
585 * @throws IOException
586 * in case of error
587 */
588 private List<String> doGetCard(String name) throws IOException {
589 List<String> lines = new LinkedList<String>();
590
591 File vcf = getFile(name);
592
593 if (vcf != null && vcf.exists()) {
594 Card card = new Card(vcf, Format.VCard21);
595
596 // timestamp:
597 lines.add(StringUtils.fromTime(card.getLastModified()));
598 lines.addAll(Vcard21Parser.toStrings(card));
599 }
600
601 return lines;
602 }
603
604 /**
605 * Save the data to the new given resource.
606 *
607 * @param name
608 * the resource name to save
609 * @param data
610 * the data to save
611 *
612 * @return the date of last modification
613 *
614 * @throws IOException
615 * in case of error
616 */
617 private String doPostCard(String name, List<String> data)
618 throws IOException {
619
620 File vcf = getFile(name);
621
622 if (vcf != null) {
623 Card card = new Card(Vcard21Parser.parseContact(data));
624 card.saveAs(vcf, Format.VCard21);
625
626 return StringUtils.fromTime(vcf.lastModified());
627 }
628
629 return "";
630 }
631
632 /**
633 * Return the {@link File} corresponding to the given resource name.
634 *
635 * @param name
636 * the resource name
637 *
638 * @return the corresponding {@link File} or NULL if the name was NULL or
639 * empty
640 */
641 private File getFile(String name) {
642 if (name != null && name.length() > 0) {
643 return new File(dataDir.getAbsolutePath() + File.separator + name);
644 }
645
646 return null;
647 }
648 }