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
.LinkedList
;
11 import java
.util
.List
;
12 import java
.util
.ResourceBundle
;
14 import be
.nikiroo
.jvcard
.Card
;
15 import be
.nikiroo
.jvcard
.Contact
;
16 import be
.nikiroo
.jvcard
.Data
;
17 import be
.nikiroo
.jvcard
.parsers
.Format
;
18 import be
.nikiroo
.jvcard
.parsers
.Vcard21Parser
;
19 import be
.nikiroo
.jvcard
.remote
.Command
.Verb
;
20 import be
.nikiroo
.jvcard
.resources
.Bundles
;
21 import be
.nikiroo
.jvcard
.resources
.StringUtils
;
24 * This class implements a small server that can listen for requests to
25 * synchronise, get and put {@link Card}s.
28 * It is <b>NOT</b> secured in any way (it even is nice enough to give you a
29 * help message when you connect in raw mode via nc on how to use it), so do
30 * <b>NOT</b> enable such a server to be accessible from internet. This is not
31 * safe. Use a ssh/openssl tunnel or similar.
37 public class Server
implements Runnable
{
38 private ServerSocket ss
;
43 private Object clientsLock
= new Object();
44 private List
<SimpleSocket
> clients
= new LinkedList
<SimpleSocket
>();
46 private Object updateLock
= new Object();
49 * Create a new jVCard server on the given port.
57 public Server(int port
) throws IOException
{
59 ResourceBundle bundle
= Bundles
.getBundle("remote");
61 String dir
= bundle
.getString("SERVER_DATA_PATH");
62 dataDir
= new File(dir
);
65 if (!dataDir
.exists()) {
66 throw new IOException("Cannot open or create data store at: "
69 } catch (Exception e
) {
71 throw new IOException("Cannot open or create data store at: "
75 ss
= new ServerSocket(port
);
79 * Stop the server. It may take some time before returning, but will only
80 * return when the server is actually stopped.
85 SimpleSocket c
= new SimpleSocket(new Socket((String
) null, port
),
86 "special STOP client");
88 c
.sendCommand(Verb
.STOP
);
90 } catch (UnknownHostException e
) {
92 } catch (IOException e
) {
96 if (clients
.size() > 0) {
99 } catch (InterruptedException e
) {
102 if (clients
.size() > 0) {
103 synchronized (clientsLock
) {
104 for (SimpleSocket s
: clients
) {
106 .println("Forcefully closing client connection");
120 final Socket s
= ss
.accept();
121 // TODO: thread pool?
122 new Thread(new Runnable() {
125 SimpleSocket ss
= new SimpleSocket(s
, "[request]");
131 while (processCmd(ss
))
134 } catch (IOException e
) {
142 } catch (IOException ioe
) {
143 ioe
.printStackTrace();
149 * Add a client to the current count.
151 * @return the client index number
153 private void addClient(SimpleSocket s
) {
154 synchronized (clientsLock
) {
160 * Remove a client from the current count.
163 * the client index number
165 private void removeClient(SimpleSocket s
) {
166 synchronized (clientsLock
) {
175 * the {@link SimpleSocket} from which to get the command to
178 * @return TRUE if the client is ready for another command, FALSE when the
181 * @throws IOException
182 * in case of IO error
184 private boolean processCmd(SimpleSocket s
) throws IOException
{
185 Command cmd
= s
.receiveCommand();
186 Command
.Verb verb
= cmd
.getVerb();
191 boolean clientContinue
= true;
193 System
.out
.println(s
+ " -> " + verb
);
197 clientContinue
= false;
200 s
.sendCommand(Verb
.VERSION
);
203 s
.sendLine(StringUtils
.fromTime(new Date().getTime()));
206 synchronized (updateLock
) {
207 s
.sendBlock(doGetCard(cmd
.getParam()));
211 synchronized (updateLock
) {
212 s
.sendLine(doPostCard(cmd
.getParam(), s
.receiveBlock()));
216 synchronized (updateLock
) {
217 File vcf
= getFile(cmd
.getParam());
220 .println("Fail to update a card, file not available: "
222 clientContinue
= false;
224 Card card
= new Card(vcf
, Format
.VCard21
);
226 while (processContactCmd(s
, card
))
229 } catch (InvalidParameterException e
) {
231 .println("Unsupported command received from a client connection, closing it: "
232 + verb
+ " (" + e
.getMessage() + ")");
233 clientContinue
= false;
241 .println("Unsupported command received from a client connection, closing it: "
243 clientContinue
= false;
246 for (File file
: dataDir
.listFiles()) {
247 if (cmd
.getParam() == null || cmd
.getParam().length() == 0
248 || file
.getName().contains(cmd
.getParam())) {
249 s
.send(StringUtils
.fromTime(file
.lastModified()) + " "
257 s
.send("The following commands are available:");
258 s
.send("- TIME: get the server time");
259 s
.send("- HELP: this help screen");
260 s
.send("- LIST: list the available cards on this server");
261 s
.send("- VERSION/GET/PUT/POST/DELETE/STOP: TODO");
266 .println("Unsupported command received from a client connection, closing it: "
268 clientContinue
= false;
272 return clientContinue
;
276 * Process a *_CONTACT subcommand.
279 * the {@link SimpleSocket} to process
281 * the target {@link Card}
283 * @return TRUE if the client is ready for another command, FALSE when the
286 * @throws IOException
287 * in case of IO error
289 * @throw InvalidParameterException in case of invalid subcommand
291 private boolean processContactCmd(SimpleSocket s
, Card card
)
293 Command cmd
= s
.receiveCommand();
294 Command
.Verb verb
= cmd
.getVerb();
299 boolean clientContinue
= true;
301 System
.out
.println(s
+ " -> " + verb
);
305 Contact contact
= card
.getById(cmd
.getParam());
307 s
.sendBlock(Vcard21Parser
.toStrings(contact
, -1));
313 String uid
= cmd
.getParam();
314 Contact contact
= card
.getById(uid
);
317 List
<Contact
> list
= Vcard21Parser
.parseContact(s
.receiveBlock());
318 if (list
.size() > 0) {
319 contact
= list
.get(0);
320 contact
.getPreferredData("UID").setValue(uid
);
326 String uid
= cmd
.getParam();
327 Contact contact
= card
.getById(uid
);
328 if (contact
== null) {
329 throw new InvalidParameterException(
330 "Cannot find contact to modify for UID: " + uid
);
332 while (processDataCmd(s
, contact
))
336 case DELETE_CONTACT
: {
337 String uid
= cmd
.getParam();
338 Contact contact
= card
.getById(uid
);
339 if (contact
== null) {
340 throw new InvalidParameterException(
341 "Cannot find contact to delete for UID: " + uid
);
348 clientContinue
= false;
352 throw new InvalidParameterException("command invalid here");
356 return clientContinue
;
360 * Process a *_DATA subcommand.
363 * the {@link SimpleSocket} to process
365 * the target {@link Contact}
367 * @return TRUE if the client is ready for another command, FALSE when the
370 * @throws IOException
371 * in case of IO error
373 * @throw InvalidParameterException in case of invalid subcommand
375 private boolean processDataCmd(SimpleSocket s
, Contact contact
)
377 Command cmd
= s
.receiveCommand();
378 Command
.Verb verb
= cmd
.getVerb();
383 boolean clientContinue
= true;
385 System
.out
.println(s
+ " -> " + verb
);
389 Data data
= contact
.getById(cmd
.getParam());
391 s
.sendBlock(Vcard21Parser
.toStrings(data
));
397 String cstate
= cmd
.getParam();
399 for (Data d
: contact
) {
400 if (cstate
.equals(d
.getContentState()))
406 List
<Data
> list
= Vcard21Parser
.parseData(s
.receiveBlock());
407 if (list
.size() > 0) {
408 contact
.add(list
.get(0));
413 String cstate
= cmd
.getParam();
415 for (Data d
: contact
) {
416 if (cstate
.equals(d
.getContentState()))
421 throw new InvalidParameterException(
422 "Cannot find data to delete for content state: "
430 clientContinue
= false;
434 throw new InvalidParameterException("command invalid here");
438 return clientContinue
;
442 * Return the serialised {@link Card} (with timestamp).
445 * the resource name to load
447 * @return the serialised data
449 * @throws IOException
452 private List
<String
> doGetCard(String name
) throws IOException
{
453 List
<String
> lines
= new LinkedList
<String
>();
455 File vcf
= getFile(name
);
457 if (vcf
!= null && vcf
.exists()) {
458 Card card
= new Card(vcf
, Format
.VCard21
);
461 lines
.add(StringUtils
.fromTime(card
.getLastModified()));
462 lines
.addAll(Vcard21Parser
.toStrings(card
));
469 * Save the data to the new given resource.
472 * the resource name to save
476 * @return the date of last modification
478 * @throws IOException
481 private String
doPostCard(String name
, List
<String
> data
)
484 File vcf
= getFile(name
);
487 Card card
= new Card(Vcard21Parser
.parseContact(data
));
488 card
.saveAs(vcf
, Format
.VCard21
);
490 return StringUtils
.fromTime(vcf
.lastModified());
497 * Return the {@link File} corresponding to the given resource name.
502 * @return the corresponding {@link File} or NULL if the name was NULL or
505 private File
getFile(String name
) {
506 if (name
!= null && name
.length() > 0) {
507 return new File(dataDir
.getAbsolutePath() + File
.separator
+ name
);