jvcard remote support (initial commit, not ready for use yet)
[jvcard.git] / src / be / nikiroo / jvcard / remote / Server.java
diff --git a/src/be/nikiroo/jvcard/remote/Server.java b/src/be/nikiroo/jvcard/remote/Server.java
new file mode 100644 (file)
index 0000000..2ead82b
--- /dev/null
@@ -0,0 +1,293 @@
+package be.nikiroo.jvcard.remote;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ResourceBundle;
+
+import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.parsers.Format;
+import be.nikiroo.jvcard.parsers.Parser;
+import be.nikiroo.jvcard.remote.Command.Verb;
+import be.nikiroo.jvcard.resources.Bundles;
+import be.nikiroo.jvcard.tui.StringUtils;
+
+/**
+ * This class implements a small server that can listen for requests to
+ * synchronise, get and put {@link Card}s.
+ * 
+ * <p>
+ * It is <b>NOT</b> secured in any way (it even is nice enough to give you a
+ * help message when you connect in raw mode via nc on how to use it), so do
+ * <b>NOT</b> enable such a server to be accessible from internet. This is not
+ * safe. Use a ssh/openssl tunnel or similar.
+ * </p>
+ * 
+ * @author niki
+ *
+ */
+public class Server implements Runnable {
+       private ServerSocket ss;
+       private int port;
+       private boolean stop;
+       private File dataDir;
+
+       private Object clientsLock = new Object();
+       private List<SimpleSocket> clients = new LinkedList<SimpleSocket>();
+
+       private Object cardsLock = new Object();
+
+       public static void main(String[] args) throws IOException {
+               Server server = new Server(4444);
+               server.run();
+       }
+
+       /**
+        * Create a new jVCard sercer on the given port.
+        * 
+        * @param port
+        *            the port to run on
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       public Server(int port) throws IOException {
+               this.port = port;
+               ResourceBundle bundle = Bundles.getBundle("remote");
+               try {
+                       String dir = bundle.getString("SERVER_DATA_PATH");
+                       dataDir = new File(dir);
+                       dataDir.mkdir();
+
+                       if (!dataDir.exists()) {
+                               throw new IOException("Cannot open or create data store at: "
+                                               + dataDir);
+                       }
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       throw new IOException("Cannot open or create data store at: "
+                                       + dataDir, e);
+               }
+
+               ss = new ServerSocket(port);
+       }
+
+       /**
+        * Stop the server. It may take some time before returning, but will only
+        * return when the server is actually stopped.
+        */
+       public void stop() {
+               stop = true;
+               try {
+                       SimpleSocket c = new SimpleSocket(new Socket((String) null, port),
+                                       "special STOP client");
+                       c.open(true);
+                       c.sendCommand(Verb.STOP);
+                       c.close();
+               } catch (UnknownHostException e) {
+                       e.printStackTrace();
+               } catch (IOException e) {
+                       e.printStackTrace();
+               }
+
+               if (clients.size() > 0) {
+                       try {
+                               Thread.sleep(100);
+                       } catch (InterruptedException e) {
+                       }
+
+                       if (clients.size() > 0) {
+                               synchronized (clientsLock) {
+                                       for (SimpleSocket s : clients) {
+                                               System.err
+                                                               .println("Forcefully closing client connection");
+                                               s.close();
+                                       }
+
+                                       clients.clear();
+                               }
+                       }
+               }
+       }
+
+       @Override
+       public void run() {
+               while (!stop) {
+                       try {
+                               final Socket s = ss.accept();
+                               // TODO: thread pool?
+                               new Thread(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               accept(new SimpleSocket(s, "[request]"));
+                                       }
+                               }).start();
+                       } catch (IOException ioe) {
+                               ioe.printStackTrace();
+                       }
+               }
+       }
+
+       /**
+        * Add a client to the current count.
+        * 
+        * @return the client index number
+        */
+       private void addClient(SimpleSocket s) {
+               synchronized (clientsLock) {
+                       clients.add(s);
+               }
+       }
+
+       /**
+        * Remove a client from the current count.
+        * 
+        * @param index
+        *            the client index number
+        */
+       private void removeClient(SimpleSocket s) {
+               synchronized (clientsLock) {
+                       clients.remove(s);
+               }
+       }
+
+       /**
+        * Accept a client and process it.
+        * 
+        * @param s
+        *            the client to process
+        */
+       private void accept(SimpleSocket s) {
+               addClient(s);
+
+               try {
+                       s.open(false);
+
+                       boolean clientStop = false;
+                       while (!clientStop) {
+                               Command cmd = s.receiveCommand();
+                               Command.Verb verb = cmd.getVerb();
+
+                               if (verb == null)
+                                       break;
+
+                               System.out.println(s + " ->  " + verb);
+
+                               switch (verb) {
+                               case STOP:
+                                       clientStop = true;
+                                       break;
+                               case VERSION:
+                                       s.sendCommand(Verb.VERSION);
+                                       break;
+                               case TIME:
+                                       s.sendLine(StringUtils.fromTime(new Date().getTime()));
+                                       break;
+                               case GET:
+                                       synchronized (cardsLock) {
+                                               s.sendBlock(doGetCard(cmd.getParam()));
+                                       }
+                                       break;
+                               case POST:
+                                       synchronized (cardsLock) {
+                                               doPostCard(cmd.getParam(), s.receiveBlock());
+                                               s.sendBlock();
+                                               break;
+                                       }
+                               case LIST:
+                                       for (File file : dataDir.listFiles()) {
+                                               if (cmd.getParam() == null
+                                                               || cmd.getParam().length() == 0
+                                                               || file.getName().contains(cmd.getParam())) {
+                                                       s.send(StringUtils.fromTime(file.lastModified())
+                                                                       + " " + file.getName());
+                                               }
+                                       }
+                                       s.sendBlock();
+                                       break;
+                               case HELP:
+                                       // TODO: i18n
+                                       s.send("The following commands are available:");
+                                       s.send("- TIME: get the server time");
+                                       s.send("- HELP: this help screen");
+                                       s.send("- LIST: list the available cards on this server");
+                                       s.send("- VERSION/GET/PUT/POST/DELETE/STOP: TODO");
+                                       s.sendBlock();
+                                       break;
+                               default:
+                                       System.err
+                                                       .println("Unsupported command received from a client connection, closing it: "
+                                                                       + verb);
+                                       clientStop = true;
+                                       break;
+                               }
+                       }
+               } catch (IOException e) {
+                       e.printStackTrace();
+               } finally {
+                       s.close();
+               }
+
+               removeClient(s);
+       }
+
+       /**
+        * Return the serialised {@link Card} (with timestamp).
+        * 
+        * @param name
+        *            the resource name to load
+        * 
+        * @return the serialised data
+        * 
+        * @throws IOException
+        *             in case of error
+        */
+       private List<String> doGetCard(String name) throws IOException {
+               List<String> lines = new LinkedList<String>();
+
+               if (name != null && name.length() > 0) {
+                       File vcf = new File(dataDir.getAbsolutePath() + File.separator
+                                       + name);
+
+                       if (vcf.exists()) {
+                               Card card = new Card(vcf, Format.VCard21);
+
+                               // timestamp:
+                               lines.add(StringUtils.fromTime(card.getLastModified()));
+
+                               // TODO: !!! fix this !!!
+                               for (String line : card.toString(Format.VCard21).split("\r\n")) {
+                                       lines.add(line);
+                               }
+                       }
+               }
+
+               return lines;
+       }
+
+       /**
+        * Save the data to the new given resource.
+        * 
+        * @param name
+        *            the resource name to save
+        * @param data
+        *            the data to save
+        * 
+        * @throws IOException
+        *             in case of error
+        */
+       private void doPostCard(String name, List<String> data) throws IOException {
+               if (name != null && name.length() > 0) {
+                       File vcf = new File(dataDir.getAbsolutePath() + File.separator
+                                       + name);
+
+                       Card card = new Card(Parser.parse(data, Format.VCard21));
+                       card.saveAs(vcf, Format.VCard21);
+               }
+       }
+}