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
.sendCommand(Command
.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 || cmd
.getParam().length() == 0
263 || file
.getName().contains(cmd
.getParam())) {
264 s
.send(StringUtils
.fromTime(file
.lastModified()) + " "
273 s
.send("The following commands are available:");
274 s
.send("- TIME: get the server time");
275 s
.send("- HELP: this help screen");
276 s
.send("- LIST: list the available cards on this server");
277 s
.send("- VERSION/GET/PUT/POST/DELETE/STOP: TODO");
283 .println("Unsupported command received from a client connection, closing it: "
285 clientContinue
= false;
290 return clientContinue
;
294 * Process a subcommand while protected for resource <tt>name</tt>.
297 * the {@link SimpleSocket} to process
300 * the resource that is protected (and to target)
302 * @return TRUE if the client is ready for another command, FALSE when the
305 * @throws IOException
306 * in case of IO error
308 * @throw InvalidParameterException in case of invalid subcommand
310 private boolean processLockedCmd(SimpleSocket s
, String name
)
312 CommandInstance cmd
= s
.receiveCommand();
313 Command command
= cmd
.getCommand();
318 boolean clientContinue
= true;
320 System
.out
.println(s
+ " -> " + command
);
324 s
.sendBlock(doGetCard(name
));
328 s
.sendLine(doPostCard(name
, s
.receiveBlock()));
332 File vcf
= getFile(name
);
335 .println("Fail to update a card, file not available: "
337 clientContinue
= false;
339 Card card
= new Card(vcf
, Format
.VCard21
);
341 while (processContactCmd(s
, card
))
344 s
.sendLine(StringUtils
.fromTime(card
.getLastModified()));
345 } catch (InvalidParameterException e
) {
347 .println("Unsupported command received from a client connection, closing it: "
348 + command
+ " (" + e
.getMessage() + ")");
349 clientContinue
= false;
357 .println("Unsupported command received from a client connection, closing it: "
359 clientContinue
= false;
363 clientContinue
= false;
367 throw new InvalidParameterException("command invalid here");
371 return clientContinue
;
375 * Process a *_CONTACT subcommand.
378 * the {@link SimpleSocket} to process
380 * the target {@link Card}
382 * @return TRUE if the client is ready for another command, FALSE when the
385 * @throws IOException
386 * in case of IO error
388 * @throw InvalidParameterException in case of invalid subcommand
390 private boolean processContactCmd(SimpleSocket s
, Card card
)
392 CommandInstance cmd
= s
.receiveCommand();
393 Command command
= cmd
.getCommand();
398 boolean clientContinue
= true;
400 System
.out
.println(s
+ " -> " + command
);
404 Contact contact
= card
.getById(cmd
.getParam());
406 s
.sendBlock(Vcard21Parser
.toStrings(contact
, -1));
412 String uid
= cmd
.getParam();
413 Contact contact
= card
.getById(uid
);
416 List
<Contact
> list
= Vcard21Parser
.parseContact(s
.receiveBlock());
417 if (list
.size() > 0) {
418 contact
= list
.get(0);
419 contact
.getPreferredData("UID").setValue(uid
);
425 String uid
= cmd
.getParam();
426 Contact contact
= card
.getById(uid
);
427 if (contact
== null) {
428 throw new InvalidParameterException(
429 "Cannot find contact to modify for UID: " + uid
);
431 while (processDataCmd(s
, contact
))
435 case DELETE_CONTACT
: {
436 String uid
= cmd
.getParam();
437 Contact contact
= card
.getById(uid
);
438 if (contact
== null) {
439 throw new InvalidParameterException(
440 "Cannot find contact to delete for UID: " + uid
);
447 String uid
= cmd
.getParam();
448 Contact contact
= card
.getById(uid
);
450 if (contact
== null) {
453 s
.sendLine(contact
.getContentState());
458 for (Contact contact
: card
) {
459 s
.send(contact
.getContentState() + " " + contact
.getId());
465 clientContinue
= false;
469 throw new InvalidParameterException("command invalid here");
473 return clientContinue
;
477 * Process a *_DATA subcommand.
480 * the {@link SimpleSocket} to process
482 * the target {@link Contact}
484 * @return TRUE if the client is ready for another command, FALSE when the
487 * @throws IOException
488 * in case of IO error
490 * @throw InvalidParameterException in case of invalid subcommand
492 private boolean processDataCmd(SimpleSocket s
, Contact contact
)
494 CommandInstance cmd
= s
.receiveCommand();
495 Command command
= cmd
.getCommand();
500 boolean clientContinue
= true;
502 System
.out
.println(s
+ " -> " + command
);
506 for (Data data
: contact
) {
507 if (data
.getName().equals(cmd
.getParam())) {
508 for (String line
: Vcard21Parser
.toStrings(data
)) {
517 String cstate
= cmd
.getParam();
519 for (Data d
: contact
) {
520 if (cstate
.equals(d
.getContentState()))
526 List
<Data
> list
= Vcard21Parser
.parseData(s
.receiveBlock());
527 if (list
.size() > 0) {
528 contact
.add(list
.get(0));
533 String cstate
= cmd
.getParam();
535 for (Data d
: contact
) {
536 if (cstate
.equals(d
.getContentState()))
541 throw new InvalidParameterException(
542 "Cannot find data to delete for content state: "
550 for (Data data
: contact
) {
551 if (data
.getId().equals(cmd
.getParam())) {
552 s
.send(data
.getContentState());
559 for (Data data
: contact
) {
560 s
.send(data
.getContentState() + " " + data
.getName());
566 clientContinue
= false;
570 throw new InvalidParameterException("command invalid here");
574 return clientContinue
;
578 * Return the serialised {@link Card} (with timestamp).
581 * the resource name to load
583 * @return the serialised data
585 * @throws IOException
588 private List
<String
> doGetCard(String name
) throws IOException
{
589 List
<String
> lines
= new LinkedList
<String
>();
591 File vcf
= getFile(name
);
593 if (vcf
!= null && vcf
.exists()) {
594 Card card
= new Card(vcf
, Format
.VCard21
);
597 lines
.add(StringUtils
.fromTime(card
.getLastModified()));
598 lines
.addAll(Vcard21Parser
.toStrings(card
));
605 * Save the data to the new given resource.
608 * the resource name to save
612 * @return the date of last modification
614 * @throws IOException
617 private String
doPostCard(String name
, List
<String
> data
)
620 File vcf
= getFile(name
);
623 Card card
= new Card(Vcard21Parser
.parseContact(data
));
624 card
.saveAs(vcf
, Format
.VCard21
);
626 return StringUtils
.fromTime(vcf
.lastModified());
633 * Return the {@link File} corresponding to the given resource name.
638 * @return the corresponding {@link File} or NULL if the name was NULL or
641 private File
getFile(String name
) {
642 if (name
!= null && name
.length() > 0) {
643 return new File(dataDir
.getAbsolutePath() + File
.separator
+ name
);