1 package be
.nikiroo
.jvcard
.remote
;
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
;
10 import java
.util
.HashMap
;
11 import java
.util
.LinkedList
;
12 import java
.util
.List
;
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
;
26 * This class implements a small server that can listen for requests to
27 * synchronise, get and put {@link Card}s.
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.
39 public class Server
implements Runnable
{
40 private ServerSocket ss
;
45 private Object clientsLock
= new Object();
46 private List
<SimpleSocket
> clients
= new LinkedList
<SimpleSocket
>();
48 private Object updateLock
= new Object();
49 private Map
<File
, Integer
> updates
= new HashMap
<File
, Integer
>();
52 * Create a new jVCard server on the given port.
60 public Server(int port
) throws IOException
{
62 RemoteBundle bundle
= new RemoteBundle();
64 String dir
= bundle
.getString(RemotingOption
.SERVER_DATA_PATH
);
65 dataDir
= new File(dir
);
68 if (!dataDir
.exists()) {
69 throw new IOException("Cannot open or create data store at: "
72 } catch (Exception e
) {
74 throw new IOException("Cannot open or create data store at: "
78 ss
= new ServerSocket(port
);
82 * Stop the server. It may take some time before returning, but will only
83 * return when the server is actually stopped.
88 SimpleSocket c
= new SimpleSocket(new Socket((String
) null, port
),
89 "special STOP client");
91 c
.sendCommand(Command
.STOP
);
93 } catch (UnknownHostException e
) {
95 } catch (IOException e
) {
99 if (clients
.size() > 0) {
102 } catch (InterruptedException e
) {
105 if (clients
.size() > 0) {
106 synchronized (clientsLock
) {
107 for (SimpleSocket s
: clients
) {
109 .println("Forcefully closing client connection");
123 final Socket s
= ss
.accept();
124 // TODO: thread pool?
125 new Thread(new Runnable() {
128 SimpleSocket ss
= new SimpleSocket(s
, "[request]");
134 while (processCmd(ss
)) {
135 // nothing to do: process the command
138 } catch (IOException e
) {
146 } catch (IOException ioe
) {
147 ioe
.printStackTrace();
153 * Add a client to the current count.
155 * @return the client index number
157 private void addClient(SimpleSocket s
) {
158 synchronized (clientsLock
) {
164 * Remove a client from the current count.
167 * the client index number
169 private void removeClient(SimpleSocket s
) {
170 synchronized (clientsLock
) {
176 * Process a first-level command.
179 * the {@link SimpleSocket} from which to get the command to
182 * @return TRUE if the client is ready for another command, FALSE when the
185 * @throws IOException
186 * in case of IO error
188 private boolean processCmd(SimpleSocket s
) throws IOException
{
189 CommandInstance cmd
= s
.receiveCommand();
190 Command command
= cmd
.getCommand();
195 boolean clientContinue
= true;
197 System
.out
.println(s
+ " -> " + command
198 + (cmd
.getParam() == null ?
"" : " " + cmd
.getParam()));
202 clientContinue
= false;
206 s
.sendLine("" + SimpleSocket
.CURRENT_VERSION
);
210 s
.sendLine(StringUtils
.fromTime(new Date().getTime()));
214 String name
= cmd
.getParam();
215 File file
= new File(dataDir
.getAbsolutePath() + File
.separator
217 if (name
== null || name
.length() == 0 || !file
.exists()) {
219 .println("SELECT: resource not found, closing connection: "
221 clientContinue
= false;
223 synchronized (updateLock
) {
224 for (File f
: updates
.keySet()) {
225 if (f
.getCanonicalPath()
226 .equals(file
.getCanonicalPath())) {
232 if (!updates
.containsKey(file
))
233 updates
.put(file
, 0);
234 updates
.put(file
, updates
.get(file
) + 1);
237 synchronized (file
) {
239 s
.sendLine(StringUtils
.fromTime(file
.lastModified()));
241 while (processLockedCmd(s
, name
)) {
242 // nothing to do: process the command
244 } catch (InvalidParameterException e
) {
246 .println("Unsupported command received from a client connection, closing it: "
247 + command
+ " (" + e
.getMessage() + ")");
248 clientContinue
= false;
252 synchronized (updateLock
) {
253 int num
= updates
.get(file
) - 1;
255 updates
.remove(file
);
257 updates
.put(file
, num
);
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()) + " "
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");
288 .println("Unsupported command received from a client connection, closing it: "
290 clientContinue
= false;
295 return clientContinue
;
299 * Process a subcommand while protected for resource <tt>name</tt>.
302 * the {@link SimpleSocket} to process
305 * the resource that is protected (and to target)
307 * @return TRUE if the client is ready for another command, FALSE when the
310 * @throws IOException
311 * in case of IO error
313 * @throw InvalidParameterException in case of invalid subcommand
315 private boolean processLockedCmd(SimpleSocket s
, String name
)
317 CommandInstance cmd
= s
.receiveCommand();
318 Command command
= cmd
.getCommand();
323 boolean clientContinue
= true;
325 System
.out
.println(s
+ " -> " + command
);
329 sendCardBlock(s
, name
);
333 s
.sendLine(doPostCard(name
, s
.receiveBlock()));
337 File vcf
= getFile(name
);
340 .println("Fail to update a card, file not available: "
342 clientContinue
= false;
344 Card card
= new Card(vcf
, Format
.VCard21
);
346 while (processContactCmd(s
, card
)) {
347 // nothing to do: process the command
350 s
.sendLine(StringUtils
.fromTime(card
.getLastModified()));
351 } catch (InvalidParameterException e
) {
353 .println("Unsupported command received from a client connection, closing it: "
354 + command
+ " (" + e
.getMessage() + ")");
355 clientContinue
= false;
363 .println("Unsupported command received from a client connection, closing it: "
365 clientContinue
= false;
369 clientContinue
= false;
373 throw new InvalidParameterException("command invalid here: "
378 return clientContinue
;
382 * Process a *_CONTACT subcommand.
385 * the {@link SimpleSocket} to process
387 * the target {@link Card}
389 * @return TRUE if the client is ready for another command, FALSE when the
392 * @throws IOException
393 * in case of IO error
395 * @throw InvalidParameterException in case of invalid subcommand
397 private boolean processContactCmd(SimpleSocket s
, Card card
)
399 CommandInstance cmd
= s
.receiveCommand();
400 Command command
= cmd
.getCommand();
405 boolean clientContinue
= true;
407 System
.out
.println(s
+ " -> " + command
);
411 Contact contact
= card
.getById(cmd
.getParam());
412 if (contact
!= null) {
413 BlockAppendable app
= s
.createBlockAppendable();
414 Vcard21Parser
.write(app
, contact
, -1);
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)
429 card
.add(newContact
);
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
);
441 while (processDataCmd(s
, contact
)) {
442 // nothing to do: process the command
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
);
458 String uid
= cmd
.getParam();
459 Contact contact
= card
.getById(uid
);
461 if (contact
== null) {
464 s
.sendLine(contact
.getContentState(true));
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) + " "
483 clientContinue
= false;
487 throw new InvalidParameterException("command invalid here: "
492 return clientContinue
;
496 * Process a *_DATA subcommand.
499 * the {@link SimpleSocket} to process
501 * the target {@link Contact}
503 * @return TRUE if the client is ready for another command, FALSE when the
506 * @throws IOException
507 * in case of IO error
509 * @throw InvalidParameterException in case of invalid subcommand
511 private boolean processDataCmd(SimpleSocket s
, Contact contact
)
513 CommandInstance cmd
= s
.receiveCommand();
514 Command command
= cmd
.getCommand();
519 boolean clientContinue
= true;
521 System
.out
.println(s
+ " -> " + command
);
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
536 String cstate
= cmd
.getParam();
538 for (Data d
: contact
) {
539 if (cstate
.equals(d
.getContentState(true)))
545 List
<Data
> list
= Vcard21Parser
.parseData(s
.receiveBlock());
546 if (list
.size() > 0) {
547 contact
.add(list
.get(0));
552 String cstate
= cmd
.getParam();
554 for (Data d
: contact
) {
555 if (cstate
.equals(d
.getContentState(true)))
560 throw new InvalidParameterException(
561 "Cannot find data to delete for content state: "
569 for (Data data
: contact
) {
570 if (data
.getId().equals(cmd
.getParam())) {
571 s
.send(data
.getContentState(true));
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());
590 clientContinue
= false;
594 throw new InvalidParameterException("command invalid here: "
599 return clientContinue
;
603 * Return the serialised {@link Card} (with timestamp).
606 * the resource name to load
608 * @return the serialised data
610 * @throws IOException
613 private void sendCardBlock(SimpleSocket s
, String name
) throws IOException
{
614 File vcf
= getFile(name
);
615 BlockAppendable app
= s
.createBlockAppendable();
617 if (vcf
!= null && vcf
.exists()) {
618 Card card
= new Card(vcf
, Format
.VCard21
);
621 app
.append(StringUtils
.fromTime(card
.getLastModified()) + "\r\n");
622 Vcard21Parser
.write(app
, card
);
629 * Save the data to the new given resource.
632 * the resource name to save
636 * @return the date of last modification
638 * @throws IOException
641 private String
doPostCard(String name
, List
<String
> data
)
644 File vcf
= getFile(name
);
647 Card card
= new Card(Vcard21Parser
.parseContact(data
));
648 card
.saveAs(vcf
, Format
.VCard21
);
650 return StringUtils
.fromTime(vcf
.lastModified());
657 * Return the {@link File} corresponding to the given resource name.
662 * @return the corresponding {@link File} or NULL if the name was NULL or
665 private File
getFile(String name
) {
666 if (name
!= null && name
.length() > 0) {
667 return new File(dataDir
.getAbsolutePath() + File
.separator
+ name
);