e90e42d6ff1e76c5a1caec0d91d2e5c2a80ec808
[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 s.sendLine(doPostCard(cmd.getParam(), s.receiveBlock()));
199 break;
200 }
201 case LIST:
202 for (File file : dataDir.listFiles()) {
203 if (cmd.getParam() == null
204 || cmd.getParam().length() == 0
205 || file.getName().contains(cmd.getParam())) {
206 s.send(StringUtils.fromTime(file.lastModified())
207 + " " + file.getName());
208 }
209 }
210 s.sendBlock();
211 break;
212 case HELP:
213 // TODO: i18n
214 s.send("The following commands are available:");
215 s.send("- TIME: get the server time");
216 s.send("- HELP: this help screen");
217 s.send("- LIST: list the available cards on this server");
218 s.send("- VERSION/GET/PUT/POST/DELETE/STOP: TODO");
219 s.sendBlock();
220 break;
221 default:
222 System.err
223 .println("Unsupported command received from a client connection, closing it: "
224 + verb);
225 clientStop = true;
226 break;
227 }
228 }
229 } catch (IOException e) {
230 e.printStackTrace();
231 } finally {
232 s.close();
233 }
234
235 removeClient(s);
236 }
237
238 /**
239 * Return the serialised {@link Card} (with timestamp).
240 *
241 * @param name
242 * the resource name to load
243 *
244 * @return the serialised data
245 *
246 * @throws IOException
247 * in case of error
248 */
249 private List<String> doGetCard(String name) throws IOException {
250 List<String> lines = new LinkedList<String>();
251
252 if (name != null && name.length() > 0) {
253 File vcf = new File(dataDir.getAbsolutePath() + File.separator
254 + name);
255
256 if (vcf.exists()) {
257 Card card = new Card(vcf, Format.VCard21);
258
259 // timestamp:
260 lines.add(StringUtils.fromTime(card.getLastModified()));
261 lines.addAll(Parser.toStrings(card, Format.VCard21));
262 }
263 }
264
265 return lines;
266 }
267
268 /**
269 * Save the data to the new given resource.
270 *
271 * @param name
272 * the resource name to save
273 * @param data
274 * the data to save
275 *
276 * @return the date of last modification
277 *
278 * @throws IOException
279 * in case of error
280 */
281 private String doPostCard(String name, List<String> data)
282 throws IOException {
283 if (name != null && name.length() > 0) {
284 File vcf = new File(dataDir.getAbsolutePath() + File.separator
285 + name);
286
287 Card card = new Card(Parser.parse(data, Format.VCard21));
288 card.saveAs(vcf, Format.VCard21);
289
290 return StringUtils.fromTime(vcf.lastModified());
291 }
292
293 return "";
294 }
295 }