New launcher class to start all 3 modes:
[jvcard.git] / src / be / nikiroo / jvcard / remote / Server.java
CommitLineData
a046fa49
NR
1package be.nikiroo.jvcard.remote;
2
3import java.io.File;
4import java.io.IOException;
5import java.net.ServerSocket;
6import java.net.Socket;
7import java.net.UnknownHostException;
0b6140e4 8import java.security.InvalidParameterException;
a046fa49
NR
9import java.util.Date;
10import java.util.LinkedList;
11import java.util.List;
12import java.util.ResourceBundle;
13
14import be.nikiroo.jvcard.Card;
0b6140e4
NR
15import be.nikiroo.jvcard.Contact;
16import be.nikiroo.jvcard.Data;
a046fa49 17import be.nikiroo.jvcard.parsers.Format;
0b6140e4 18import be.nikiroo.jvcard.parsers.Vcard21Parser;
a046fa49
NR
19import be.nikiroo.jvcard.remote.Command.Verb;
20import be.nikiroo.jvcard.resources.Bundles;
7da41ecd 21import be.nikiroo.jvcard.resources.StringUtils;
a046fa49
NR
22
23/**
24 * This class implements a small server that can listen for requests to
25 * synchronise, get and put {@link Card}s.
26 *
27 * <p>
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.
32 * </p>
33 *
34 * @author niki
35 *
36 */
37public class Server implements Runnable {
38 private ServerSocket ss;
39 private int port;
40 private boolean stop;
41 private File dataDir;
42
43 private Object clientsLock = new Object();
44 private List<SimpleSocket> clients = new LinkedList<SimpleSocket>();
45
0b6140e4 46 private Object updateLock = new Object();
a046fa49 47
a046fa49 48 /**
7da41ecd 49 * Create a new jVCard server on the given port.
a046fa49
NR
50 *
51 * @param port
52 * the port to run on
53 *
54 * @throws IOException
55 * in case of IO error
56 */
57 public Server(int port) throws IOException {
58 this.port = port;
59 ResourceBundle bundle = Bundles.getBundle("remote");
60 try {
61 String dir = bundle.getString("SERVER_DATA_PATH");
62 dataDir = new File(dir);
63 dataDir.mkdir();
64
65 if (!dataDir.exists()) {
66 throw new IOException("Cannot open or create data store at: "
67 + dataDir);
68 }
69 } catch (Exception e) {
70 e.printStackTrace();
71 throw new IOException("Cannot open or create data store at: "
72 + dataDir, e);
73 }
74
75 ss = new ServerSocket(port);
76 }
77
78 /**
79 * Stop the server. It may take some time before returning, but will only
80 * return when the server is actually stopped.
81 */
82 public void stop() {
83 stop = true;
84 try {
85 SimpleSocket c = new SimpleSocket(new Socket((String) null, port),
86 "special STOP client");
87 c.open(true);
88 c.sendCommand(Verb.STOP);
89 c.close();
90 } catch (UnknownHostException e) {
91 e.printStackTrace();
92 } catch (IOException e) {
93 e.printStackTrace();
94 }
95
96 if (clients.size() > 0) {
97 try {
98 Thread.sleep(100);
99 } catch (InterruptedException e) {
100 }
101
102 if (clients.size() > 0) {
103 synchronized (clientsLock) {
104 for (SimpleSocket s : clients) {
105 System.err
106 .println("Forcefully closing client connection");
107 s.close();
108 }
109
110 clients.clear();
111 }
112 }
113 }
114 }
115
116 @Override
117 public void run() {
118 while (!stop) {
119 try {
120 final Socket s = ss.accept();
121 // TODO: thread pool?
122 new Thread(new Runnable() {
123 @Override
124 public void run() {
0b6140e4
NR
125 SimpleSocket ss = new SimpleSocket(s, "[request]");
126
127 addClient(ss);
128 try {
129 ss.open(false);
130
131 while (processCmd(ss))
132 ;
133
134 } catch (IOException e) {
135 e.printStackTrace();
136 } finally {
137 ss.close();
138 }
139 removeClient(ss);
a046fa49
NR
140 }
141 }).start();
142 } catch (IOException ioe) {
143 ioe.printStackTrace();
144 }
145 }
146 }
147
148 /**
149 * Add a client to the current count.
150 *
151 * @return the client index number
152 */
153 private void addClient(SimpleSocket s) {
154 synchronized (clientsLock) {
155 clients.add(s);
156 }
157 }
158
159 /**
160 * Remove a client from the current count.
161 *
162 * @param index
163 * the client index number
164 */
165 private void removeClient(SimpleSocket s) {
166 synchronized (clientsLock) {
167 clients.remove(s);
168 }
169 }
170
171 /**
0b6140e4 172 * Process a command.
a046fa49
NR
173 *
174 * @param s
0b6140e4
NR
175 * the {@link SimpleSocket} from which to get the command to
176 * process
177 *
178 * @return TRUE if the client is ready for another command, FALSE when the
179 * client exited
180 *
181 * @throws IOException
182 * in case of IO error
a046fa49 183 */
0b6140e4
NR
184 private boolean processCmd(SimpleSocket s) throws IOException {
185 Command cmd = s.receiveCommand();
186 Command.Verb verb = cmd.getVerb();
187
188 if (verb == null)
189 return false;
190
191 boolean clientContinue = true;
192
193 System.out.println(s + " -> " + verb);
194
195 switch (verb) {
196 case STOP:
197 clientContinue = false;
198 break;
199 case VERSION:
200 s.sendCommand(Verb.VERSION);
201 break;
202 case TIME:
203 s.sendLine(StringUtils.fromTime(new Date().getTime()));
204 break;
205 case GET_CARD:
206 synchronized (updateLock) {
207 s.sendBlock(doGetCard(cmd.getParam()));
208 }
209 break;
210 case POST_CARD:
211 synchronized (updateLock) {
212 s.sendLine(doPostCard(cmd.getParam(), s.receiveBlock()));
213 }
214 break;
215 case PUT_CARD:
216 synchronized (updateLock) {
217 File vcf = getFile(cmd.getParam());
218 if (vcf == null) {
a046fa49 219 System.err
0b6140e4
NR
220 .println("Fail to update a card, file not available: "
221 + cmd.getParam());
222 clientContinue = false;
223 } else {
224 Card card = new Card(vcf, Format.VCard21);
225 try {
226 while (processContactCmd(s, card))
227 ;
228 card.save();
229 } catch (InvalidParameterException e) {
230 System.err
231 .println("Unsupported command received from a client connection, closing it: "
232 + verb + " (" + e.getMessage() + ")");
233 clientContinue = false;
234 }
a046fa49
NR
235 }
236 }
0b6140e4
NR
237 break;
238 case DELETE_CARD:
239 // TODO
240 System.err
241 .println("Unsupported command received from a client connection, closing it: "
242 + verb);
243 clientContinue = false;
244 break;
245 case LIST:
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()) + " "
250 + file.getName());
251 }
252 }
253 s.sendBlock();
254 break;
255 case HELP:
256 // TODO: i18n
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");
262 s.sendBlock();
263 break;
264 default:
265 System.err
266 .println("Unsupported command received from a client connection, closing it: "
267 + verb);
268 clientContinue = false;
269 break;
270 }
271
272 return clientContinue;
273 }
274
275 /**
276 * Process a *_CONTACT subcommand.
277 *
278 * @param s
279 * the {@link SimpleSocket} to process
280 * @param card
281 * the target {@link Card}
282 *
283 * @return TRUE if the client is ready for another command, FALSE when the
284 * client is done
285 *
286 * @throws IOException
287 * in case of IO error
288 *
289 * @throw InvalidParameterException in case of invalid subcommand
290 */
291 private boolean processContactCmd(SimpleSocket s, Card card)
292 throws IOException {
293 Command cmd = s.receiveCommand();
294 Command.Verb verb = cmd.getVerb();
295
296 if (verb == null)
297 return false;
298
299 boolean clientContinue = true;
300
301 System.out.println(s + " -> " + verb);
302
303 switch (verb) {
304 case GET_CONTACT: {
305 Contact contact = card.getById(cmd.getParam());
306 if (contact != null)
307 s.sendBlock(Vcard21Parser.toStrings(contact, -1));
308 else
309 s.sendBlock();
310 break;
311 }
312 case POST_CONTACT: {
313 String uid = cmd.getParam();
314 Contact contact = card.getById(uid);
315 if (contact != null)
316 contact.delete();
317 List<Contact> list = Vcard21Parser.parseContact(s.receiveBlock());
318 if (list.size() > 0) {
319 contact = list.get(0);
320 contact.getPreferredData("UID").setValue(uid);
321 card.add(contact);
322 }
323 break;
324 }
325 case PUT_CONTACT: {
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);
331 }
332 while (processDataCmd(s, contact))
333 ;
334 break;
335 }
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);
342 }
343
344 contact.delete();
345 break;
346 }
347 case PUT_CARD: {
348 clientContinue = false;
349 break;
350 }
351 default: {
352 throw new InvalidParameterException("command invalid here");
353 }
a046fa49
NR
354 }
355
0b6140e4
NR
356 return clientContinue;
357 }
358
359 /**
360 * Process a *_DATA subcommand.
361 *
362 * @param s
363 * the {@link SimpleSocket} to process
364 * @param card
365 * the target {@link Contact}
366 *
367 * @return TRUE if the client is ready for another command, FALSE when the
368 * client is done
369 *
370 * @throws IOException
371 * in case of IO error
372 *
373 * @throw InvalidParameterException in case of invalid subcommand
374 */
375 private boolean processDataCmd(SimpleSocket s, Contact contact)
376 throws IOException {
377 Command cmd = s.receiveCommand();
378 Command.Verb verb = cmd.getVerb();
379
380 if (verb == null)
381 return false;
382
383 boolean clientContinue = true;
384
385 System.out.println(s + " -> " + verb);
386
387 switch (verb) {
388 case GET_DATA: {
389 Data data = contact.getById(cmd.getParam());
390 if (data != null)
391 s.sendBlock(Vcard21Parser.toStrings(data));
392 else
393 s.sendBlock();
394 break;
395 }
396 case POST_DATA: {
397 String cstate = cmd.getParam();
398 Data data = null;
399 for (Data d : contact) {
400 if (cstate.equals(d.getContentState()))
401 data = d;
402 }
403
404 if (data != null)
405 data.delete();
406 List<Data> list = Vcard21Parser.parseData(s.receiveBlock());
407 if (list.size() > 0) {
408 contact.add(list.get(0));
409 }
410 break;
411 }
412 case DELETE_DATA: {
413 String cstate = cmd.getParam();
414 Data data = null;
415 for (Data d : contact) {
416 if (cstate.equals(d.getContentState()))
417 data = d;
418 }
419
420 if (data == null) {
421 throw new InvalidParameterException(
422 "Cannot find data to delete for content state: "
423 + cstate);
424 }
425
426 contact.delete();
427 break;
428 }
429 case PUT_CONTACT: {
430 clientContinue = false;
431 break;
432 }
433 default: {
434 throw new InvalidParameterException("command invalid here");
435 }
436 }
437
438 return clientContinue;
a046fa49
NR
439 }
440
441 /**
442 * Return the serialised {@link Card} (with timestamp).
443 *
444 * @param name
445 * the resource name to load
446 *
447 * @return the serialised data
448 *
449 * @throws IOException
450 * in case of error
451 */
452 private List<String> doGetCard(String name) throws IOException {
453 List<String> lines = new LinkedList<String>();
454
0b6140e4 455 File vcf = getFile(name);
a046fa49 456
0b6140e4
NR
457 if (vcf != null && vcf.exists()) {
458 Card card = new Card(vcf, Format.VCard21);
a046fa49 459
0b6140e4
NR
460 // timestamp:
461 lines.add(StringUtils.fromTime(card.getLastModified()));
462 lines.addAll(Vcard21Parser.toStrings(card));
a046fa49
NR
463 }
464
465 return lines;
466 }
467
468 /**
469 * Save the data to the new given resource.
470 *
471 * @param name
472 * the resource name to save
473 * @param data
474 * the data to save
475 *
cf77cb35
NR
476 * @return the date of last modification
477 *
a046fa49
NR
478 * @throws IOException
479 * in case of error
480 */
cf77cb35
NR
481 private String doPostCard(String name, List<String> data)
482 throws IOException {
a046fa49 483
0b6140e4
NR
484 File vcf = getFile(name);
485
486 if (vcf != null) {
487 Card card = new Card(Vcard21Parser.parseContact(data));
a046fa49 488 card.saveAs(vcf, Format.VCard21);
cf77cb35
NR
489
490 return StringUtils.fromTime(vcf.lastModified());
a046fa49 491 }
cf77cb35
NR
492
493 return "";
a046fa49 494 }
0b6140e4
NR
495
496 /**
497 * Return the {@link File} corresponding to the given resource name.
498 *
499 * @param name
500 * the resource name
501 *
502 * @return the corresponding {@link File} or NULL if the name was NULL or
503 * empty
504 */
505 private File getFile(String name) {
506 if (name != null && name.length() > 0) {
507 return new File(dataDir.getAbsolutePath() + File.separator + name);
508 }
509
510 return null;
511 }
a046fa49 512}