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());
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() + " " + contact
.getId());
474 clientContinue
= false;
478 throw new InvalidParameterException("command invalid here: "
483 return clientContinue
;
487 * Process a *_DATA subcommand.
490 * the {@link SimpleSocket} to process
492 * the target {@link Contact}
494 * @return TRUE if the client is ready for another command, FALSE when the
497 * @throws IOException
498 * in case of IO error
500 * @throw InvalidParameterException in case of invalid subcommand
502 private boolean processDataCmd(SimpleSocket s
, Contact contact
)
504 CommandInstance cmd
= s
.receiveCommand();
505 Command command
= cmd
.getCommand();
510 boolean clientContinue
= true;
512 System
.out
.println(s
+ " -> " + command
);
516 for (Data data
: contact
) {
517 if (data
.getName().equals(cmd
.getParam())) {
518 for (String line
: Vcard21Parser
.toStrings(data
)) {
527 String cstate
= cmd
.getParam();
529 for (Data d
: contact
) {
530 if (cstate
.equals(d
.getContentState()))
536 List
<Data
> list
= Vcard21Parser
.parseData(s
.receiveBlock());
537 if (list
.size() > 0) {
538 contact
.add(list
.get(0));
543 String cstate
= cmd
.getParam();
545 for (Data d
: contact
) {
546 if (cstate
.equals(d
.getContentState()))
551 throw new InvalidParameterException(
552 "Cannot find data to delete for content state: "
560 for (Data data
: contact
) {
561 if (data
.getId().equals(cmd
.getParam())) {
562 s
.send(data
.getContentState());
569 for (Data data
: contact
) {
570 if (cmd
.getParam() == null
571 || cmd
.getParam().length() == 0
572 || data
.getName().toLowerCase()
573 .contains(cmd
.getParam().toLowerCase())) {
574 s
.send(data
.getContentState() + " " + data
.getName());
581 clientContinue
= false;
585 throw new InvalidParameterException("command invalid here: "
590 return clientContinue
;
594 * Return the serialised {@link Card} (with timestamp).
597 * the resource name to load
599 * @return the serialised data
601 * @throws IOException
604 private List
<String
> doGetCard(String name
) throws IOException
{
605 List
<String
> lines
= new LinkedList
<String
>();
607 File vcf
= getFile(name
);
609 if (vcf
!= null && vcf
.exists()) {
610 Card card
= new Card(vcf
, Format
.VCard21
);
613 lines
.add(StringUtils
.fromTime(card
.getLastModified()));
614 lines
.addAll(Vcard21Parser
.toStrings(card
));
621 * Save the data to the new given resource.
624 * the resource name to save
628 * @return the date of last modification
630 * @throws IOException
633 private String
doPostCard(String name
, List
<String
> data
)
636 File vcf
= getFile(name
);
639 Card card
= new Card(Vcard21Parser
.parseContact(data
));
640 card
.saveAs(vcf
, Format
.VCard21
);
642 return StringUtils
.fromTime(vcf
.lastModified());
649 * Return the {@link File} corresponding to the given resource name.
654 * @return the corresponding {@link File} or NULL if the name was NULL or
657 private File
getFile(String name
) {
658 if (name
!= null && name
.length() > 0) {
659 return new File(dataDir
.getAbsolutePath() + File
.separator
+ name
);