server: re-introduce the client/server versions
[fanfix.git] / src / be / nikiroo / utils / serial / server / ConnectAction.java
1 package be.nikiroo.utils.serial.server;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.io.OutputStream;
6 import java.net.Socket;
7
8 import javax.net.ssl.SSLException;
9
10 import be.nikiroo.utils.CryptUtils;
11 import be.nikiroo.utils.IOUtils;
12 import be.nikiroo.utils.StringUtils;
13 import be.nikiroo.utils.Version;
14 import be.nikiroo.utils.serial.Exporter;
15 import be.nikiroo.utils.serial.Importer;
16 import be.nikiroo.utils.streams.BufferedOutputStream;
17 import be.nikiroo.utils.streams.NextableInputStream;
18 import be.nikiroo.utils.streams.NextableInputStreamStep;
19 import be.nikiroo.utils.streams.ReplaceInputStream;
20 import be.nikiroo.utils.streams.ReplaceOutputStream;
21
22 /**
23 * Base class used for the client/server basic handling.
24 * <p>
25 * It represents a single action: a client is expected to only execute one
26 * action, while a server is expected to execute one action for each client
27 * action.
28 *
29 * @author niki
30 */
31 abstract class ConnectAction {
32 // We separate each "packet" we send with this character and make sure it
33 // does not occurs in the message itself.
34 static private char STREAM_SEP = '\b';
35 static private String[] STREAM_RAW = new String[] { "\\", "\b" };
36 static private String[] STREAM_CODED = new String[] { "\\\\", "\\b" };
37
38 private Socket s;
39 private boolean server;
40
41 private Version clientVersion;
42 private Version serverVersion;
43
44 private CryptUtils crypt;
45
46 private Object lock = new Object();
47 private NextableInputStream in;
48 private BufferedOutputStream out;
49 private boolean contentToSend;
50
51 /**
52 * Method that will be called when an action is performed on either the
53 * client or server this {@link ConnectAction} represent.
54 *
55 * @param version
56 * the version on the other side of the communication (client or
57 * server)
58 *
59 * @throws Exception
60 * in case of I/O error
61 */
62 abstract protected void action(Version version) throws Exception;
63
64 /**
65 * Method called when we negotiate the version with the client.
66 * <p>
67 * Thus, it is only called on the server.
68 * <p>
69 * Will return the actual server version by default.
70 *
71 * @param clientVersion
72 * the client version
73 *
74 * @return the version to send to the client
75 */
76 abstract protected Version negotiateVersion(Version clientVersion);
77
78 /**
79 * Handler called when an unexpected error occurs in the code.
80 *
81 * @param e
82 * the exception that occurred, SSLException usually denotes a
83 * crypt error
84 */
85 abstract protected void onError(Exception e);
86
87 /**
88 * Create a new {@link ConnectAction}.
89 *
90 * @param s
91 * the socket to bind to
92 * @param server
93 * TRUE for a server action, FALSE for a client action (will
94 * impact the process)
95 * @param key
96 * an optional key to encrypt all the communications (if NULL,
97 * everything will be sent in clear text)
98 * @param version
99 * the client-or-server version (depending upon the boolean
100 * parameter <tt>server</tt>)
101 */
102 protected ConnectAction(Socket s, boolean server, String key,
103 Version version) {
104 this.s = s;
105 this.server = server;
106 if (key != null) {
107 crypt = new CryptUtils(key);
108 }
109
110 if (version == null) {
111 version = new Version();
112 }
113
114 if (server) {
115 serverVersion = version;
116 } else {
117 clientVersion = version;
118 }
119 }
120
121 /**
122 * The version of this client-or-server.
123 *
124 * @return the version
125 */
126 public Version getVersion() {
127 if (server) {
128 return serverVersion;
129 }
130
131 return clientVersion;
132 }
133
134 /**
135 * The total amount of bytes received.
136 *
137 * @return the amount of bytes received
138 */
139 public long getBytesReceived() {
140 return in.getBytesRead();
141 }
142
143 /**
144 * The total amount of bytes sent.
145 *
146 * @return the amount of bytes sent
147 */
148 public long getBytesWritten() {
149 return out.getBytesWritten();
150 }
151
152 /**
153 * Actually start the process (this is synchronous).
154 */
155 public void connect() {
156 try {
157 in = new NextableInputStream(s.getInputStream(),
158 new NextableInputStreamStep(STREAM_SEP));
159 try {
160 out = new BufferedOutputStream(s.getOutputStream());
161 try {
162 action(server ? serverVersion : clientVersion);
163 } finally {
164 out.close();
165 }
166 } finally {
167 in.close();
168 }
169 } catch (Exception e) {
170 onError(e);
171 } finally {
172 try {
173 s.close();
174 } catch (Exception e) {
175 onError(e);
176 }
177 }
178 }
179
180 /**
181 * Serialise and send the given object to the counter part (and, only for
182 * client, return the deserialised answer -- the server will always receive
183 * NULL).
184 *
185 * @param data
186 * the data to send
187 *
188 * @return the answer (which can be NULL if no answer, or NULL for an answer
189 * which is NULL) if this action is a client, always NULL if it is a
190 * server
191 *
192 * @throws IOException
193 * in case of I/O error
194 * @throws NoSuchFieldException
195 * if the serialised data contains information about a field
196 * which does actually not exist in the class we know of
197 * @throws NoSuchMethodException
198 * if a class described in the serialised data cannot be created
199 * because it is not compatible with this code
200 * @throws ClassNotFoundException
201 * if a class described in the serialised data cannot be found
202 */
203 protected Object sendObject(Object data) throws IOException,
204 NoSuchFieldException, NoSuchMethodException, ClassNotFoundException {
205 return send(out, data, false);
206 }
207
208 /**
209 * Reserved for the server: flush the data to the client and retrieve its
210 * answer.
211 * <p>
212 * Also used internally for the client (only do something if there is
213 * contentToSend).
214 * <p>
215 * Will only flush the data if there is contentToSend.
216 *
217 * @return the deserialised answer (which can actually be NULL)
218 *
219 * @throws IOException
220 * in case of I/O error
221 * @throws NoSuchFieldException
222 * if the serialised data contains information about a field
223 * which does actually not exist in the class we know of
224 * @throws NoSuchMethodException
225 * if a class described in the serialised data cannot be created
226 * because it is not compatible with this code
227 * @throws ClassNotFoundException
228 * if a class described in the serialised data cannot be found
229 * @throws java.lang.NullPointerException
230 * if the counter part has no data to send
231 */
232 protected Object recObject() throws IOException, NoSuchFieldException,
233 NoSuchMethodException, ClassNotFoundException,
234 java.lang.NullPointerException {
235 return rec(false);
236 }
237
238 /**
239 * Send the given string to the counter part (and, only for client, return
240 * the answer -- the server will always receive NULL).
241 *
242 * @param line
243 * the data to send (we will add a line feed)
244 *
245 * @return the answer if this action is a client (without the added line
246 * feed), NULL if it is a server
247 *
248 * @throws IOException
249 * in case of I/O error
250 * @throws SSLException
251 * in case of crypt error
252 */
253 protected String sendString(String line) throws IOException {
254 try {
255 return (String) send(out, line, true);
256 } catch (NoSuchFieldException e) {
257 // Cannot happen
258 e.printStackTrace();
259 } catch (NoSuchMethodException e) {
260 // Cannot happen
261 e.printStackTrace();
262 } catch (ClassNotFoundException e) {
263 // Cannot happen
264 e.printStackTrace();
265 }
266
267 return null;
268 }
269
270 /**
271 * Reserved for the server (externally): flush the data to the client and
272 * retrieve its answer.
273 * <p>
274 * Also used internally for the client (only do something if there is
275 * contentToSend).
276 * <p>
277 * Will only flush the data if there is contentToSend.
278 *
279 * @return the answer (which can be NULL if no more content)
280 *
281 * @throws IOException
282 * in case of I/O error
283 * @throws SSLException
284 * in case of crypt error
285 */
286 protected String recString() throws IOException {
287 try {
288 return (String) rec(true);
289 } catch (NoSuchFieldException e) {
290 // Cannot happen
291 e.printStackTrace();
292 } catch (NoSuchMethodException e) {
293 // Cannot happen
294 e.printStackTrace();
295 } catch (ClassNotFoundException e) {
296 // Cannot happen
297 e.printStackTrace();
298 } catch (NullPointerException e) {
299 // Should happen
300 e.printStackTrace();
301 }
302
303 return null;
304 }
305
306 /**
307 * Serialise and send the given object to the counter part (and, only for
308 * client, return the deserialised answer -- the server will always receive
309 * NULL).
310 *
311 * @param out
312 * the stream to write to
313 * @param data
314 * the data to write
315 * @param asString
316 * TRUE to write it as a String, FALSE to write it as an Object
317 *
318 * @return the answer (which can be NULL if no answer, or NULL for an answer
319 * which is NULL) if this action is a client, always NULL if it is a
320 * server
321 *
322 * @throws IOException
323 * in case of I/O error
324 * @throws SSLException
325 * in case of crypt error
326 * @throws IOException
327 * in case of I/O error
328 * @throws NoSuchFieldException
329 * if the serialised data contains information about a field
330 * which does actually not exist in the class we know of
331 * @throws NoSuchMethodException
332 * if a class described in the serialised data cannot be created
333 * because it is not compatible with this code
334 * @throws ClassNotFoundException
335 * if a class described in the serialised data cannot be found
336 */
337 private Object send(BufferedOutputStream out, Object data, boolean asString)
338 throws IOException, NoSuchFieldException, NoSuchMethodException,
339 ClassNotFoundException, java.lang.NullPointerException {
340
341 synchronized (lock) {
342 OutputStream sub;
343 if (crypt != null) {
344 sub = crypt.encrypt64(out.open());
345 } else {
346 sub = out.open();
347 }
348
349 sub = new ReplaceOutputStream(sub, STREAM_RAW, STREAM_CODED);
350 try {
351 if (asString) {
352 sub.write(StringUtils.getBytes(data.toString()));
353 } else {
354 new Exporter(sub).append(data);
355 }
356 } finally {
357 sub.close();
358 }
359
360 out.write(STREAM_SEP);
361
362 if (server) {
363 out.flush();
364 return null;
365 }
366
367 contentToSend = true;
368 try {
369 return rec(asString);
370 } catch (NullPointerException e) {
371 // We accept no data here for Objects
372 }
373
374 return null;
375 }
376 }
377
378 /**
379 * Reserved for the server: flush the data to the client and retrieve its
380 * answer.
381 * <p>
382 * Also used internally for the client (only do something if there is
383 * contentToSend).
384 * <p>
385 * Will only flush the data if there is contentToSend.
386 * <p>
387 * Note that the behaviour is slightly different for String and Object
388 * reading regarding exceptions:
389 * <ul>
390 * <li>NULL means that the counter part has no more data to send</li>
391 * <li>All the exceptions except {@link IOException} are there for Object
392 * conversion</li>
393 * </ul>
394 *
395 * @param asString
396 * TRUE for String reading, FALSE for Object reading (which can
397 * still be a String)
398 *
399 * @return the deserialised answer (which can actually be NULL)
400 *
401 * @throws IOException
402 * in case of I/O error
403 * @throws NoSuchFieldException
404 * if the serialised data contains information about a field
405 * which does actually not exist in the class we know of
406 * @throws NoSuchMethodException
407 * if a class described in the serialised data cannot be created
408 * because it is not compatible with this code
409 * @throws ClassNotFoundException
410 * if a class described in the serialised data cannot be found
411 * @throws java.lang.NullPointerException
412 * for Objects only: if the counter part has no data to send
413 */
414 @SuppressWarnings("resource")
415 private Object rec(boolean asString) throws IOException,
416 NoSuchFieldException, NoSuchMethodException,
417 ClassNotFoundException, java.lang.NullPointerException {
418
419 synchronized (lock) {
420 if (server || contentToSend) {
421 if (contentToSend) {
422 out.flush();
423 contentToSend = false;
424 }
425
426 if (in.next() && !in.eof()) {
427 InputStream read = new ReplaceInputStream(in.open(),
428 STREAM_CODED, STREAM_RAW);
429 try {
430 if (crypt != null) {
431 read = crypt.decrypt64(read);
432 }
433
434 if (asString) {
435 return IOUtils.readSmallStream(read);
436 }
437
438 return new Importer().read(read).getValue();
439 } finally {
440 read.close();
441 }
442 }
443
444 if (!asString) {
445 throw new NullPointerException();
446 }
447 }
448
449 return null;
450 }
451 }
452 }