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
.StringUtils
;
22 import be
.nikiroo
.jvcard
.resources
.bundles
.RemoteBundle
;
23 import be
.nikiroo
.jvcard
.resources
.enums
.RemotingOption
;
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
))
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 CommandInstance cmd
= s
.receiveCommand();
189 Command command
= cmd
.getCommand();
194 boolean clientContinue
= true;
196 System
.out
.println(s
+ " -> " + command
197 + (cmd
.getParam() == null ?
"" : " " + cmd
.getParam()));
201 clientContinue
= false;
205 s
.sendLine("" + SimpleSocket
.CURRENT_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 + command
+ " (" + 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
264 || cmd
.getParam().length() == 0
265 || file
.getName().toLowerCase()
266 .contains(cmd
.getParam().toLowerCase())) {
267 s
.send(StringUtils
.fromTime(file
.lastModified()) + " "
276 s
.send("The following commands are available:");
277 s
.send("- TIME: get the server time");
278 s
.send("- HELP: this help screen");
279 s
.send("- LIST_CARD: list the available cards on this server");
280 s
.send("- VERSION/GET_*/PUT_*/POST_*/DELETE_*/STOP: TODO");
286 .println("Unsupported command received from a client connection, closing it: "
288 clientContinue
= false;
293 return clientContinue
;
297 * Process a subcommand while protected for resource <tt>name</tt>.
300 * the {@link SimpleSocket} to process
303 * the resource that is protected (and to target)
305 * @return TRUE if the client is ready for another command, FALSE when the
308 * @throws IOException
309 * in case of IO error
311 * @throw InvalidParameterException in case of invalid subcommand
313 private boolean processLockedCmd(SimpleSocket s
, String name
)
315 CommandInstance cmd
= s
.receiveCommand();
316 Command command
= cmd
.getCommand();
321 boolean clientContinue
= true;
323 System
.out
.println(s
+ " -> " + command
);
327 sendCardBlock(s
, name
);
331 s
.sendLine(doPostCard(name
, s
.receiveBlock()));
335 File vcf
= getFile(name
);
338 .println("Fail to update a card, file not available: "
340 clientContinue
= false;
342 Card card
= new Card(vcf
, Format
.VCard21
);
344 while (processContactCmd(s
, card
))
347 s
.sendLine(StringUtils
.fromTime(card
.getLastModified()));
348 } catch (InvalidParameterException e
) {
350 .println("Unsupported command received from a client connection, closing it: "
351 + command
+ " (" + e
.getMessage() + ")");
352 clientContinue
= false;
360 .println("Unsupported command received from a client connection, closing it: "
362 clientContinue
= false;
366 clientContinue
= false;
370 throw new InvalidParameterException("command invalid here: "
375 return clientContinue
;
379 * Process a *_CONTACT subcommand.
382 * the {@link SimpleSocket} to process
384 * the target {@link Card}
386 * @return TRUE if the client is ready for another command, FALSE when the
389 * @throws IOException
390 * in case of IO error
392 * @throw InvalidParameterException in case of invalid subcommand
394 private boolean processContactCmd(SimpleSocket s
, Card card
)
396 CommandInstance cmd
= s
.receiveCommand();
397 Command command
= cmd
.getCommand();
402 boolean clientContinue
= true;
404 System
.out
.println(s
+ " -> " + command
);
408 Contact contact
= card
.getById(cmd
.getParam());
409 if (contact
!= null) {
410 BlockAppendable app
= s
.createBlockAppendable();
411 Vcard21Parser
.write(app
, contact
, -1);
419 List
<Contact
> list
= Vcard21Parser
.parseContact(s
.receiveBlock());
420 if (list
.size() > 0) {
421 Contact newContact
= list
.get(0);
422 String uid
= newContact
.getPreferredDataValue("UID");
423 Contact oldContact
= card
.getById(uid
);
424 if (oldContact
!= null)
426 card
.add(newContact
);
432 String uid
= cmd
.getParam();
433 Contact contact
= card
.getById(uid
);
434 if (contact
== null) {
435 throw new InvalidParameterException(
436 "Cannot find contact to modify for UID: " + uid
);
438 while (processDataCmd(s
, contact
))
442 case DELETE_CONTACT
: {
443 String uid
= cmd
.getParam();
444 Contact contact
= card
.getById(uid
);
445 if (contact
== null) {
446 throw new InvalidParameterException(
447 "Cannot find contact to delete for UID: " + uid
);
454 String uid
= cmd
.getParam();
455 Contact contact
= card
.getById(uid
);
457 if (contact
== null) {
460 s
.sendLine(contact
.getContentState(true));
465 for (Contact contact
: card
) {
466 if (cmd
.getParam() == null
467 || cmd
.getParam().length() == 0
468 || (contact
.getPreferredDataValue("FN") + contact
469 .getPreferredDataValue("N")).toLowerCase()
470 .contains(cmd
.getParam().toLowerCase())) {
471 s
.send(contact
.getContentState(true) + " "
479 clientContinue
= false;
483 throw new InvalidParameterException("command invalid here: "
488 return clientContinue
;
492 * Process a *_DATA subcommand.
495 * the {@link SimpleSocket} to process
497 * the target {@link Contact}
499 * @return TRUE if the client is ready for another command, FALSE when the
502 * @throws IOException
503 * in case of IO error
505 * @throw InvalidParameterException in case of invalid subcommand
507 private boolean processDataCmd(SimpleSocket s
, Contact contact
)
509 CommandInstance cmd
= s
.receiveCommand();
510 Command command
= cmd
.getCommand();
515 boolean clientContinue
= true;
517 System
.out
.println(s
+ " -> " + command
);
521 for (Data data
: contact
) {
522 if (data
.getName().equals(cmd
.getParam())) {
523 BlockAppendable app
= s
.createBlockAppendable();
524 Vcard21Parser
.write(app
, data
);
525 // note: we do NOT close 'app', since it would send an EOB
532 String cstate
= cmd
.getParam();
534 for (Data d
: contact
) {
535 if (cstate
.equals(d
.getContentState(true)))
541 List
<Data
> list
= Vcard21Parser
.parseData(s
.receiveBlock());
542 if (list
.size() > 0) {
543 contact
.add(list
.get(0));
548 String cstate
= cmd
.getParam();
550 for (Data d
: contact
) {
551 if (cstate
.equals(d
.getContentState(true)))
556 throw new InvalidParameterException(
557 "Cannot find data to delete for content state: "
565 for (Data data
: contact
) {
566 if (data
.getId().equals(cmd
.getParam())) {
567 s
.send(data
.getContentState(true));
574 for (Data data
: contact
) {
575 if (cmd
.getParam() == null
576 || cmd
.getParam().length() == 0
577 || data
.getName().toLowerCase()
578 .contains(cmd
.getParam().toLowerCase())) {
579 s
.send(data
.getContentState(true) + " " + data
.getName());
586 clientContinue
= false;
590 throw new InvalidParameterException("command invalid here: "
595 return clientContinue
;
599 * Return the serialised {@link Card} (with timestamp).
602 * the resource name to load
604 * @return the serialised data
606 * @throws IOException
609 private void sendCardBlock(SimpleSocket s
, String name
) throws IOException
{
610 File vcf
= getFile(name
);
611 BlockAppendable app
= s
.createBlockAppendable();
613 if (vcf
!= null && vcf
.exists()) {
614 Card card
= new Card(vcf
, Format
.VCard21
);
617 app
.append(StringUtils
.fromTime(card
.getLastModified()) + "\r\n");
618 Vcard21Parser
.write(app
, card
);
625 * Save the data to the new given resource.
628 * the resource name to save
632 * @return the date of last modification
634 * @throws IOException
637 private String
doPostCard(String name
, List
<String
> data
)
640 File vcf
= getFile(name
);
643 Card card
= new Card(Vcard21Parser
.parseContact(data
));
644 card
.saveAs(vcf
, Format
.VCard21
);
646 return StringUtils
.fromTime(vcf
.lastModified());
653 * Return the {@link File} corresponding to the given resource name.
658 * @return the corresponding {@link File} or NULL if the name was NULL or
661 private File
getFile(String name
) {
662 if (name
!= null && name
.length() > 0) {
663 return new File(dataDir
.getAbsolutePath() + File
.separator
+ name
);