aafad62facfa5a3a3433df3a7aa54de8f23806b4
[nikiroo-utils.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.OutputStream;
5 import java.net.Socket;
6
7 import javax.net.ssl.SSLException;
8
9 import be.nikiroo.utils.CryptUtils;
10 import be.nikiroo.utils.IOUtils;
11 import be.nikiroo.utils.serial.Exporter;
12 import be.nikiroo.utils.serial.Importer;
13 import be.nikiroo.utils.streams.NextableInputStream;
14 import be.nikiroo.utils.streams.NextableInputStreamStep;
15
16 /**
17 * Base class used for the client/server basic handling.
18 * <p>
19 * It represents a single action: a client is expected to only execute one
20 * action, while a server is expected to execute one action for each client
21 * action.
22 *
23 * @author niki
24 */
25 abstract class ConnectAction {
26 private Socket s;
27 private boolean server;
28
29 private CryptUtils crypt;
30
31 private Object lock = new Object();
32 private NextableInputStream in;
33 private OutputStream out;
34 private boolean contentToSend;
35
36 private long bytesReceived;
37 private long bytesSent;
38
39 /**
40 * Method that will be called when an action is performed on either the
41 * client or server this {@link ConnectAction} represent.
42 *
43 * @throws Exception
44 * in case of I/O error
45 */
46 abstract protected void action() throws Exception;
47
48 /**
49 * Handler called when an unexpected error occurs in the code.
50 *
51 * @param e
52 * the exception that occurred, SSLException usually denotes a
53 * crypt error
54 */
55 abstract protected void onError(Exception e);
56
57 /**
58 * Create a new {@link ConnectAction}.
59 *
60 * @param s
61 * the socket to bind to
62 * @param server
63 * TRUE for a server action, FALSE for a client action (will
64 * impact the process)
65 * @param key
66 * an optional key to encrypt all the communications (if NULL,
67 * everything will be sent in clear text)
68 */
69 protected ConnectAction(Socket s, boolean server, String key) {
70 this.s = s;
71 this.server = server;
72 if (key != null) {
73 crypt = new CryptUtils(key);
74 }
75 }
76
77 /**
78 * The total amount of bytes received.
79 *
80 * @return the amount of bytes received
81 */
82 public long getBytesReceived() {
83 return bytesReceived;
84 }
85
86 /**
87 * The total amount of bytes sent.
88 *
89 * @return the amount of bytes sent
90 */
91 public long getBytesSent() {
92 return bytesSent;
93 }
94
95 /**
96 * Actually start the process (this is synchronous).
97 */
98 public void connect() {
99 try {
100 in = new NextableInputStream(s.getInputStream(),
101 new NextableInputStreamStep('\b'));
102
103 try {
104 out = s.getOutputStream();
105 try {
106 action();
107 } finally {
108 out.close();
109 out = null;
110 }
111 } finally {
112 in.close();
113 in = null;
114 }
115 } catch (Exception e) {
116 onError(e);
117 } finally {
118 try {
119 s.close();
120 } catch (Exception e) {
121 onError(e);
122 }
123 }
124 }
125
126 /**
127 * Serialise and send the given object to the counter part (and, only for
128 * client, return the deserialised answer -- the server will always receive
129 * NULL).
130 *
131 * @param data
132 * the data to send
133 *
134 * @return the answer (which can be NULL if no answer, or NULL for an answer
135 * which is NULL) if this action is a client, always NULL if it is a
136 * server
137 *
138 * @throws IOException
139 * in case of I/O error
140 * @throws NoSuchFieldException
141 * if the serialised data contains information about a field
142 * which does actually not exist in the class we know of
143 * @throws NoSuchMethodException
144 * if a class described in the serialised data cannot be created
145 * because it is not compatible with this code
146 * @throws ClassNotFoundException
147 * if a class described in the serialised data cannot be found
148 */
149 protected Object sendObject(Object data) throws IOException,
150 NoSuchFieldException, NoSuchMethodException, ClassNotFoundException {
151 synchronized (lock) {
152
153 new Exporter(out).append(data);
154 out.write('\b');
155
156 if (server) {
157 out.flush();
158 return null;
159 }
160
161 contentToSend = true;
162 try {
163 return recObject();
164 } catch (NullPointerException e) {
165 // We accept no data here
166 }
167
168 return null;
169 }
170 }
171
172 /**
173 * Reserved for the server: flush the data to the client and retrieve its
174 * answer.
175 * <p>
176 * Also used internally for the client (only do something if there is
177 * contentToSend).
178 * <p>
179 * Will only flush the data if there is contentToSend.
180 *
181 * @return the deserialised answer (which can actually be NULL)
182 *
183 * @throws IOException
184 * in case of I/O error
185 * @throws NoSuchFieldException
186 * if the serialised data contains information about a field
187 * which does actually not exist in the class we know of
188 * @throws NoSuchMethodException
189 * if a class described in the serialised data cannot be created
190 * because it is not compatible with this code
191 * @throws ClassNotFoundException
192 * if a class described in the serialised data cannot be found
193 * @throws java.lang.NullPointerException
194 * if the counter part has no data to send
195 */
196 protected Object recObject() throws IOException, NoSuchFieldException,
197 NoSuchMethodException, ClassNotFoundException,
198 java.lang.NullPointerException {
199 synchronized (lock) {
200 if (server || contentToSend) {
201 if (contentToSend) {
202 out.flush();
203 contentToSend = false;
204 }
205
206 if (in.next()) {
207 return new Importer().read(in).getValue();
208 }
209
210 throw new NullPointerException();
211 }
212
213 return null;
214 }
215 }
216
217 /**
218 * Send the given string to the counter part (and, only for client, return
219 * the answer -- the server will always receive NULL).
220 *
221 * @param line
222 * the data to send (we will add a line feed)
223 *
224 * @return the answer if this action is a client (without the added line
225 * feed), NULL if it is a server
226 *
227 * @throws IOException
228 * in case of I/O error
229 * @throws SSLException
230 * in case of crypt error
231 */
232 protected String sendString(String line) throws IOException {
233 synchronized (lock) {
234 writeLine(out, line);
235
236 if (server) {
237 out.flush();
238 return null;
239 }
240
241 contentToSend = true;
242 return recString();
243 }
244 }
245
246 /**
247 * Reserved for the server (externally): flush the data to the client and
248 * retrieve its answer.
249 * <p>
250 * Also used internally for the client (only do something if there is
251 * contentToSend).
252 * <p>
253 * Will only flush the data if there is contentToSend.
254 *
255 * @return the answer (which can be NULL)
256 *
257 * @throws IOException
258 * in case of I/O error
259 * @throws SSLException
260 * in case of crypt error
261 */
262 protected String recString() throws IOException {
263 synchronized (lock) {
264 if (server || contentToSend) {
265 if (contentToSend) {
266 out.flush();
267 contentToSend = false;
268 }
269
270 return readLine(in);
271 }
272
273 return null;
274 }
275 }
276
277 /**
278 * Read a possibly encrypted line.
279 *
280 * @param in
281 * the stream to read from
282 * @return the unencrypted line
283 *
284 *
285 * @throws IOException
286 * in case of I/O error
287 * @throws SSLException
288 * in case of crypt error
289 */
290 private String readLine(NextableInputStream in) throws IOException {
291 String line = null;
292 if (in.next()) {
293 line = IOUtils.readSmallStream(in);
294 }
295
296 if (line != null) {
297 bytesReceived += line.length();
298 if (crypt != null) {
299 line = crypt.decrypt64s(line, false);
300 }
301 }
302
303 return line;
304 }
305
306 /**
307 * Write a line, possible encrypted.
308 *
309 * @param out
310 * the stream to write to
311 * @param line
312 * the line to write
313 * @throws IOException
314 * in case of I/O error
315 * @throws SSLException
316 * in case of crypt error
317 */
318 private void writeLine(OutputStream out, String line) throws IOException {
319 if (crypt == null) {
320 out.write(line.getBytes("UTF-8"));
321 bytesSent += line.length();
322 } else {
323 // TODO: how NOT to create so many big Strings?
324 String b64 = crypt.encrypt64(line, false);
325 out.write(b64.getBytes("UTF-8"));
326 bytesSent += b64.length();
327 }
328 out.write('\b');
329 bytesSent++;
330 }
331 }