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