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