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
;
14 import java
.util
.ResourceBundle
;
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
;
25 * This class implements a small server that can listen for requests to
26 * synchronise, get and put {@link Card}s.
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.
38 public class Server
implements Runnable
{
39 private ServerSocket ss
;
44 private Object clientsLock
= new Object();
45 private List
<SimpleSocket
> clients
= new LinkedList
<SimpleSocket
>();
47 private Object updateLock
= new Object();
48 private Map
<File
, Integer
> updates
= new HashMap
<File
, Integer
>();
51 * Create a new jVCard server on the given port.
59 public Server(int port
) throws IOException
{
61 ResourceBundle bundle
= Bundles
.getBundle("remote");
63 String dir
= bundle
.getString("SERVER_DATA_PATH");
64 dataDir
= new File(dir
);
67 if (!dataDir
.exists()) {
68 throw new IOException("Cannot open or create data store at: "
71 } catch (Exception e
) {
73 throw new IOException("Cannot open or create data store at: "
77 ss
= new ServerSocket(port
);
81 * Stop the server. It may take some time before returning, but will only
82 * return when the server is actually stopped.
87 SimpleSocket c
= new SimpleSocket(new Socket((String
) null, port
),
88 "special STOP client");
90 c
.sendCommand(Command
.STOP
);
92 } catch (UnknownHostException e
) {
94 } catch (IOException e
) {
98 if (clients
.size() > 0) {
101 } catch (InterruptedException e
) {
104 if (clients
.size() > 0) {
105 synchronized (clientsLock
) {
106 for (SimpleSocket s
: clients
) {
108 .println("Forcefully closing client connection");
122 final Socket s
= ss
.accept();
123 // TODO: thread pool?
124 new Thread(new Runnable() {
127 SimpleSocket ss
= new SimpleSocket(s
, "[request]");
133 while (processCmd(ss
))
136 } catch (IOException e
) {
144 } catch (IOException ioe
) {
145 ioe
.printStackTrace();
151 * Add a client to the current count.
153 * @return the client index number
155 private void addClient(SimpleSocket s
) {
156 synchronized (clientsLock
) {
162 * Remove a client from the current count.
165 * the client index number
167 private void removeClient(SimpleSocket s
) {
168 synchronized (clientsLock
) {
174 * Process a first-level command.
177 * the {@link SimpleSocket} from which to get the command to
180 * @return TRUE if the client is ready for another command, FALSE when the
183 * @throws IOException
184 * in case of IO error
186 private boolean processCmd(SimpleSocket s
) throws IOException
{
187 CommandInstance cmd
= s
.receiveCommand();
188 Command command
= cmd
.getCommand();
193 boolean clientContinue
= true;
195 System
.out
.println(s
+ " -> " + command
196 + (cmd
.getParam() == null ?
"" : " " + cmd
.getParam()));
200 clientContinue
= false;
204 s
.sendLine("" + SimpleSocket
.CURRENT_VERSION
);
208 s
.sendLine(StringUtils
.fromTime(new Date().getTime()));
212 String name
= cmd
.getParam();
213 File file
= new File(dataDir
.getAbsolutePath() + File
.separator
215 if (name
== null || name
.length() == 0 || !file
.exists()) {
217 .println("SELECT: resource not found, closing connection: "
219 clientContinue
= false;
221 synchronized (updateLock
) {
222 for (File f
: updates
.keySet()) {
223 if (f
.getCanonicalPath()
224 .equals(file
.getCanonicalPath())) {
230 if (!updates
.containsKey(file
))
231 updates
.put(file
, 0);
232 updates
.put(file
, updates
.get(file
) + 1);
235 synchronized (file
) {
237 s
.sendLine(StringUtils
.fromTime(file
.lastModified()));
239 while (processLockedCmd(s
, name
))
241 } catch (InvalidParameterException e
) {
243 .println("Unsupported command received from a client connection, closing it: "
244 + command
+ " (" + e
.getMessage() + ")");
245 clientContinue
= false;
249 synchronized (updateLock
) {
250 int num
= updates
.get(file
) - 1;
252 updates
.remove(file
);
254 updates
.put(file
, num
);
261 for (File file
: dataDir
.listFiles()) {
262 if (cmd
.getParam() == null
263 || cmd
.getParam().length() == 0
264 || file
.getName().toLowerCase()
265 .contains(cmd
.getParam().toLowerCase())) {
266 s
.send(StringUtils
.fromTime(file
.lastModified()) + " "
275 s
.send("The following commands are available:");
276 s
.send("- TIME: get the server time");
277 s
.send("- HELP: this help screen");
278 s
.send("- LIST_CARD: list the available cards on this server");
279 s
.send("- VERSION/GET_*/PUT_*/POST_*/DELETE_*/STOP: TODO");
285 .println("Unsupported command received from a client connection, closing it: "
287 clientContinue
= false;
292 return clientContinue
;
296 * Process a subcommand while protected for resource <tt>name</tt>.
299 * the {@link SimpleSocket} to process
302 * the resource that is protected (and to target)
304 * @return TRUE if the client is ready for another command, FALSE when the
307 * @throws IOException
308 * in case of IO error
310 * @throw InvalidParameterException in case of invalid subcommand
312 private boolean processLockedCmd(SimpleSocket s
, String name
)
314 CommandInstance cmd
= s
.receiveCommand();
315 Command command
= cmd
.getCommand();
320 boolean clientContinue
= true;
322 System
.out
.println(s
+ " -> " + command
);
326 s
.sendBlock(doGetCard(name
));
330 s
.sendLine(doPostCard(name
, s
.receiveBlock()));
334 File vcf
= getFile(name
);
337 .println("Fail to update a card, file not available: "
339 clientContinue
= false;
341 Card card
= new Card(vcf
, Format
.VCard21
);
343 while (processContactCmd(s
, card
))
346 s
.sendLine(StringUtils
.fromTime(card
.getLastModified()));
347 } catch (InvalidParameterException e
) {
349 .println("Unsupported command received from a client connection, closing it: "
350 + command
+ " (" + e
.getMessage() + ")");
351 clientContinue
= false;
359 .println("Unsupported command received from a client connection, closing it: "
361 clientContinue
= false;
365 clientContinue
= false;
369 throw new InvalidParameterException("command invalid here: "
374 return clientContinue
;
378 * Process a *_CONTACT subcommand.
381 * the {@link SimpleSocket} to process
383 * the target {@link Card}
385 * @return TRUE if the client is ready for another command, FALSE when the
388 * @throws IOException
389 * in case of IO error
391 * @throw InvalidParameterException in case of invalid subcommand
393 private boolean processContactCmd(SimpleSocket s
, Card card
)
395 CommandInstance cmd
= s
.receiveCommand();
396 Command command
= cmd
.getCommand();
401 boolean clientContinue
= true;
403 System
.out
.println(s
+ " -> " + command
);
407 Contact contact
= card
.getById(cmd
.getParam());
409 s
.sendBlock(Vcard21Parser
.toStrings(contact
, -1));
415 List
<Contact
> list
= Vcard21Parser
.parseContact(s
.receiveBlock());
416 if (list
.size() > 0) {
417 Contact newContact
= list
.get(0);
418 String uid
= newContact
.getPreferredDataValue("UID");
419 Contact oldContact
= card
.getById(uid
);
420 if (oldContact
!= null)
422 card
.add(newContact
);
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
);
434 while (processDataCmd(s
, contact
))
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
);
450 String uid
= cmd
.getParam();
451 Contact contact
= card
.getById(uid
);
453 if (contact
== null) {
456 s
.sendLine(contact
.getContentState(true));
461 for (Contact contact
: card
) {
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(true) + " "
475 clientContinue
= false;
479 throw new InvalidParameterException("command invalid here: "
484 return clientContinue
;
488 * Process a *_DATA subcommand.
491 * the {@link SimpleSocket} to process
493 * the target {@link Contact}
495 * @return TRUE if the client is ready for another command, FALSE when the
498 * @throws IOException
499 * in case of IO error
501 * @throw InvalidParameterException in case of invalid subcommand
503 private boolean processDataCmd(SimpleSocket s
, Contact contact
)
505 CommandInstance cmd
= s
.receiveCommand();
506 Command command
= cmd
.getCommand();
511 boolean clientContinue
= true;
513 System
.out
.println(s
+ " -> " + command
);
517 for (Data data
: contact
) {
518 if (data
.getName().equals(cmd
.getParam())) {
519 for (String line
: Vcard21Parser
.toStrings(data
)) {
528 String cstate
= cmd
.getParam();
530 for (Data d
: contact
) {
531 if (cstate
.equals(d
.getContentState(true)))
537 List
<Data
> list
= Vcard21Parser
.parseData(s
.receiveBlock());
538 if (list
.size() > 0) {
539 contact
.add(list
.get(0));
544 String cstate
= cmd
.getParam();
546 for (Data d
: contact
) {
547 if (cstate
.equals(d
.getContentState(true)))
552 throw new InvalidParameterException(
553 "Cannot find data to delete for content state: "
561 for (Data data
: contact
) {
562 if (data
.getId().equals(cmd
.getParam())) {
563 s
.send(data
.getContentState(true));
570 for (Data data
: contact
) {
571 if (cmd
.getParam() == null
572 || cmd
.getParam().length() == 0
573 || data
.getName().toLowerCase()
574 .contains(cmd
.getParam().toLowerCase())) {
575 s
.send(data
.getContentState(true) + " " + data
.getName());
582 clientContinue
= false;
586 throw new InvalidParameterException("command invalid here: "
591 return clientContinue
;
595 * Return the serialised {@link Card} (with timestamp).
598 * the resource name to load
600 * @return the serialised data
602 * @throws IOException
605 private List
<String
> doGetCard(String name
) throws IOException
{
606 List
<String
> lines
= new LinkedList
<String
>();
608 File vcf
= getFile(name
);
610 if (vcf
!= null && vcf
.exists()) {
611 Card card
= new Card(vcf
, Format
.VCard21
);
614 lines
.add(StringUtils
.fromTime(card
.getLastModified()));
615 lines
.addAll(Vcard21Parser
.toStrings(card
));
622 * Save the data to the new given resource.
625 * the resource name to save
629 * @return the date of last modification
631 * @throws IOException
634 private String
doPostCard(String name
, List
<String
> data
)
637 File vcf
= getFile(name
);
640 Card card
= new Card(Vcard21Parser
.parseContact(data
));
641 card
.saveAs(vcf
, Format
.VCard21
);
643 return StringUtils
.fromTime(vcf
.lastModified());
650 * Return the {@link File} corresponding to the given resource name.
655 * @return the corresponding {@link File} or NULL if the name was NULL or
658 private File
getFile(String name
) {
659 if (name
!= null && name
.length() > 0) {
660 return new File(dataDir
.getAbsolutePath() + File
.separator
+ name
);