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
.remote
.Command
.Verb
;
22 import be
.nikiroo
.jvcard
.resources
.Bundles
;
23 import be
.nikiroo
.jvcard
.resources
.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 ResourceBundle bundle
= Bundles
.getBundle("remote");
64 String dir
= bundle
.getString("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(Verb
.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
))
137 } catch (IOException e
) {
145 } catch (IOException ioe
) {
146 ioe
.printStackTrace();
152 * Add a client to the current count.
154 * @return the client index number
156 private void addClient(SimpleSocket s
) {
157 synchronized (clientsLock
) {
163 * Remove a client from the current count.
166 * the client index number
168 private void removeClient(SimpleSocket s
) {
169 synchronized (clientsLock
) {
175 * Process a first-level command.
178 * the {@link SimpleSocket} from which to get the command to
181 * @return TRUE if the client is ready for another command, FALSE when the
184 * @throws IOException
185 * in case of IO error
187 private boolean processCmd(SimpleSocket s
) throws IOException
{
188 Command cmd
= s
.receiveCommand();
189 Command
.Verb verb
= cmd
.getVerb();
194 boolean clientContinue
= true;
196 System
.out
.println(s
+ " -> " + verb
197 + (cmd
.getParam() == null ?
"" : " " + cmd
.getParam()));
201 clientContinue
= false;
205 s
.sendCommand(Verb
.VERSION
);
209 s
.sendLine(StringUtils
.fromTime(new Date().getTime()));
213 String name
= cmd
.getParam();
214 File file
= new File(dataDir
.getAbsolutePath() + File
.separator
216 if (name
== null || name
.length() == 0 || !file
.exists()) {
218 .println("SELECT: resource not found, closing connection: "
220 clientContinue
= false;
222 synchronized (updateLock
) {
223 for (File f
: updates
.keySet()) {
224 if (f
.getCanonicalPath()
225 .equals(file
.getCanonicalPath())) {
231 if (!updates
.containsKey(file
))
232 updates
.put(file
, 0);
233 updates
.put(file
, updates
.get(file
) + 1);
236 synchronized (file
) {
238 s
.sendLine(StringUtils
.fromTime(file
.lastModified()));
240 while (processLockedCmd(s
, name
))
242 } catch (InvalidParameterException e
) {
244 .println("Unsupported command received from a client connection, closing it: "
245 + verb
+ " (" + e
.getMessage() + ")");
246 clientContinue
= false;
250 synchronized (updateLock
) {
251 int num
= updates
.get(file
) - 1;
253 updates
.remove(file
);
255 updates
.put(file
, num
);
262 for (File file
: dataDir
.listFiles()) {
263 if (cmd
.getParam() == null || cmd
.getParam().length() == 0
264 || file
.getName().contains(cmd
.getParam())) {
265 s
.send(StringUtils
.fromTime(file
.lastModified()) + " "
274 s
.send("The following commands are available:");
275 s
.send("- TIME: get the server time");
276 s
.send("- HELP: this help screen");
277 s
.send("- LIST: list the available cards on this server");
278 s
.send("- VERSION/GET/PUT/POST/DELETE/STOP: TODO");
284 .println("Unsupported command received from a client connection, closing it: "
286 clientContinue
= false;
291 return clientContinue
;
295 * Process a subcommand while protected for resource <tt>name</tt>.
298 * the {@link SimpleSocket} to process
301 * the resource that is protected (and to target)
303 * @return TRUE if the client is ready for another command, FALSE when the
306 * @throws IOException
307 * in case of IO error
309 * @throw InvalidParameterException in case of invalid subcommand
311 private boolean processLockedCmd(SimpleSocket s
, String name
)
313 Command cmd
= s
.receiveCommand();
314 Command
.Verb verb
= cmd
.getVerb();
319 boolean clientContinue
= true;
321 System
.out
.println(s
+ " -> " + verb
);
325 s
.sendBlock(doGetCard(name
));
329 s
.sendLine(doPostCard(name
, s
.receiveBlock()));
333 File vcf
= getFile(name
);
336 .println("Fail to update a card, file not available: "
338 clientContinue
= false;
340 Card card
= new Card(vcf
, Format
.VCard21
);
342 while (processContactCmd(s
, card
))
345 } catch (InvalidParameterException e
) {
347 .println("Unsupported command received from a client connection, closing it: "
348 + verb
+ " (" + 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 Command cmd
= s
.receiveCommand();
393 Command
.Verb verb
= cmd
.getVerb();
398 boolean clientContinue
= true;
400 System
.out
.println(s
+ " -> " + verb
);
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 clientContinue
= false;
451 throw new InvalidParameterException("command invalid here");
455 return clientContinue
;
459 * Process a *_DATA subcommand.
462 * the {@link SimpleSocket} to process
464 * the target {@link Contact}
466 * @return TRUE if the client is ready for another command, FALSE when the
469 * @throws IOException
470 * in case of IO error
472 * @throw InvalidParameterException in case of invalid subcommand
474 private boolean processDataCmd(SimpleSocket s
, Contact contact
)
476 Command cmd
= s
.receiveCommand();
477 Command
.Verb verb
= cmd
.getVerb();
482 boolean clientContinue
= true;
484 System
.out
.println(s
+ " -> " + verb
);
488 Data data
= contact
.getById(cmd
.getParam());
490 s
.sendBlock(Vcard21Parser
.toStrings(data
));
496 String cstate
= cmd
.getParam();
498 for (Data d
: contact
) {
499 if (cstate
.equals(d
.getContentState()))
505 List
<Data
> list
= Vcard21Parser
.parseData(s
.receiveBlock());
506 if (list
.size() > 0) {
507 contact
.add(list
.get(0));
512 String cstate
= cmd
.getParam();
514 for (Data d
: contact
) {
515 if (cstate
.equals(d
.getContentState()))
520 throw new InvalidParameterException(
521 "Cannot find data to delete for content state: "
529 clientContinue
= false;
533 throw new InvalidParameterException("command invalid here");
537 return clientContinue
;
541 * Return the serialised {@link Card} (with timestamp).
544 * the resource name to load
546 * @return the serialised data
548 * @throws IOException
551 private List
<String
> doGetCard(String name
) throws IOException
{
552 List
<String
> lines
= new LinkedList
<String
>();
554 File vcf
= getFile(name
);
556 if (vcf
!= null && vcf
.exists()) {
557 Card card
= new Card(vcf
, Format
.VCard21
);
560 lines
.add(StringUtils
.fromTime(card
.getLastModified()));
561 lines
.addAll(Vcard21Parser
.toStrings(card
));
568 * Save the data to the new given resource.
571 * the resource name to save
575 * @return the date of last modification
577 * @throws IOException
580 private String
doPostCard(String name
, List
<String
> data
)
583 File vcf
= getFile(name
);
586 Card card
= new Card(Vcard21Parser
.parseContact(data
));
587 card
.saveAs(vcf
, Format
.VCard21
);
589 return StringUtils
.fromTime(vcf
.lastModified());
596 * Return the {@link File} corresponding to the given resource name.
601 * @return the corresponding {@link File} or NULL if the name was NULL or
604 private File
getFile(String name
) {
605 if (name
!= null && name
.length() > 0) {
606 return new File(dataDir
.getAbsolutePath() + File
.separator
+ name
);