jvcard remote support (initial commit, not ready for use yet)
[jvcard.git] / src / be / nikiroo / jvcard / remote / Server.java
1 package be.nikiroo.jvcard.remote;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.net.ServerSocket;
6 import java.net.Socket;
7 import java.net.UnknownHostException;
8 import java.util.Date;
9 import java.util.LinkedList;
10 import java.util.List;
11 import java.util.ResourceBundle;
12
13 import be.nikiroo.jvcard.Card;
14 import be.nikiroo.jvcard.parsers.Format;
15 import be.nikiroo.jvcard.parsers.Parser;
16 import be.nikiroo.jvcard.remote.Command.Verb;
17 import be.nikiroo.jvcard.resources.Bundles;
18 import be.nikiroo.jvcard.tui.StringUtils;
19
20 /**
21 * This class implements a small server that can listen for requests to
22 * synchronise, get and put {@link Card}s.
23 *
24 * <p>
25 * It is <b>NOT</b> secured in any way (it even is nice enough to give you a
26 * help message when you connect in raw mode via nc on how to use it), so do
27 * <b>NOT</b> enable such a server to be accessible from internet. This is not
28 * safe. Use a ssh/openssl tunnel or similar.
29 * </p>
30 *
31 * @author niki
32 *
33 */
34 public class Server implements Runnable {
35 private ServerSocket ss;
36 private int port;
37 private boolean stop;
38 private File dataDir;
39
40 private Object clientsLock = new Object();
41 private List<SimpleSocket> clients = new LinkedList<SimpleSocket>();
42
43 private Object cardsLock = new Object();
44
45 public static void main(String[] args) throws IOException {
46 Server server = new Server(4444);
47 server.run();
48 }
49
50 /**
51 * Create a new jVCard sercer on the given port.
52 *
53 * @param port
54 * the port to run on
55 *
56 * @throws IOException
57 * in case of IO error
58 */
59 public Server(int port) throws IOException {
60 this.port = port;
61 ResourceBundle bundle = Bundles.getBundle("remote");
62 try {
63 String dir = bundle.getString("SERVER_DATA_PATH");
64 dataDir = new File(dir);
65 dataDir.mkdir();
66
67 if (!dataDir.exists()) {
68 throw new IOException("Cannot open or create data store at: "
69 + dataDir);
70 }
71 } catch (Exception e) {
72 e.printStackTrace();
73 throw new IOException("Cannot open or create data store at: "
74 + dataDir, e);
75 }
76
77 ss = new ServerSocket(port);
78 }
79
80 /**
81 * Stop the server. It may take some time before returning, but will only
82 * return when the server is actually stopped.
83 */
84 public void stop() {
85 stop = true;
86 try {
87 SimpleSocket c = new SimpleSocket(new Socket((String) null, port),
88 "special STOP client");
89 c.open(true);
90 c.sendCommand(Verb.STOP);
91 c.close();
92 } catch (UnknownHostException e) {
93 e.printStackTrace();
94 } catch (IOException e) {
95 e.printStackTrace();
96 }
97
98 if (clients.size() > 0) {
99 try {
100 Thread.sleep(100);
101 } catch (InterruptedException e) {
102 }
103
104 if (clients.size() > 0) {
105 synchronized (clientsLock) {
106 for (SimpleSocket s : clients) {
107 System.err
108 .println("Forcefully closing client connection");
109 s.close();
110 }
111
112 clients.clear();
113 }
114 }
115 }
116 }
117
118 @Override
119 public void run() {
120 while (!stop) {
121 try {
122 final Socket s = ss.accept();
123 // TODO: thread pool?
124 new Thread(new Runnable() {
125 @Override
126 public void run() {
127 accept(new SimpleSocket(s, "[request]"));
128 }
129 }).start();
130 } catch (IOException ioe) {
131 ioe.printStackTrace();
132 }
133 }
134 }
135
136 /**
137 * Add a client to the current count.
138 *
139 * @return the client index number
140 */
141 private void addClient(SimpleSocket s) {
142 synchronized (clientsLock) {
143 clients.add(s);
144 }
145 }
146
147 /**
148 * Remove a client from the current count.
149 *
150 * @param index
151 * the client index number
152 */
153 private void removeClient(SimpleSocket s) {
154 synchronized (clientsLock) {
155 clients.remove(s);
156 }
157 }
158
159 /**
160 * Accept a client and process it.
161 *
162 * @param s
163 * the client to process
164 */
165 private void accept(SimpleSocket s) {
166 addClient(s);
167
168 try {
169 s.open(false);
170
171 boolean clientStop = false;
172 while (!clientStop) {
173 Command cmd = s.receiveCommand();
174 Command.Verb verb = cmd.getVerb();
175
176 if (verb == null)
177 break;
178
179 System.out.println(s + " -> " + verb);
180
181 switch (verb) {
182 case STOP:
183 clientStop = true;
184 break;
185 case VERSION:
186 s.sendCommand(Verb.VERSION);
187 break;
188 case TIME:
189 s.sendLine(StringUtils.fromTime(new Date().getTime()));
190 break;
191 case GET:
192 synchronized (cardsLock) {
193 s.sendBlock(doGetCard(cmd.getParam()));
194 }
195 break;
196 case POST:
197 synchronized (cardsLock) {
198 doPostCard(cmd.getParam(), s.receiveBlock());
199 s.sendBlock();
200 break;
201 }
202 case LIST:
203 for (File file : dataDir.listFiles()) {
204 if (cmd.getParam() == null
205 || cmd.getParam().length() == 0
206 || file.getName().contains(cmd.getParam())) {
207 s.send(StringUtils.fromTime(file.lastModified())
208 + " " + file.getName());
209 }
210 }
211 s.sendBlock();
212 break;
213 case HELP:
214 // TODO: i18n
215 s.send("The following commands are available:");
216 s.send("- TIME: get the server time");
217 s.send("- HELP: this help screen");
218 s.send("- LIST: list the available cards on this server");
219 s.send("- VERSION/GET/PUT/POST/DELETE/STOP: TODO");
220 s.sendBlock();
221 break;
222 default:
223 System.err
224 .println("Unsupported command received from a client connection, closing it: "
225 + verb);
226 clientStop = true;
227 break;
228 }
229 }
230 } catch (IOException e) {
231 e.printStackTrace();
232 } finally {
233 s.close();
234 }
235
236 removeClient(s);
237 }
238
239 /**
240 * Return the serialised {@link Card} (with timestamp).
241 *
242 * @param name
243 * the resource name to load
244 *
245 * @return the serialised data
246 *
247 * @throws IOException
248 * in case of error
249 */
250 private List<String> doGetCard(String name) throws IOException {
251 List<String> lines = new LinkedList<String>();
252
253 if (name != null && name.length() > 0) {
254 File vcf = new File(dataDir.getAbsolutePath() + File.separator
255 + name);
256
257 if (vcf.exists()) {
258 Card card = new Card(vcf, Format.VCard21);
259
260 // timestamp:
261 lines.add(StringUtils.fromTime(card.getLastModified()));
262
263 // TODO: !!! fix this !!!
264 for (String line : card.toString(Format.VCard21).split("\r\n")) {
265 lines.add(line);
266 }
267 }
268 }
269
270 return lines;
271 }
272
273 /**
274 * Save the data to the new given resource.
275 *
276 * @param name
277 * the resource name to save
278 * @param data
279 * the data to save
280 *
281 * @throws IOException
282 * in case of error
283 */
284 private void doPostCard(String name, List<String> data) throws IOException {
285 if (name != null && name.length() > 0) {
286 File vcf = new File(dataDir.getAbsolutePath() + File.separator
287 + name);
288
289 Card card = new Card(Parser.parse(data, Format.VCard21));
290 card.saveAs(vcf, Format.VCard21);
291 }
292 }
293 }