Merge branch 'master' into subtree
[nikiroo-utils.git] / library / RemoteLibraryServer.java
1 package be.nikiroo.fanfix.library;
2
3 import java.io.IOException;
4 import java.net.URL;
5 import java.util.ArrayList;
6 import java.util.Arrays;
7 import java.util.Date;
8 import java.util.HashMap;
9 import java.util.List;
10 import java.util.Map;
11
12 import javax.net.ssl.SSLException;
13
14 import be.nikiroo.fanfix.Instance;
15 import be.nikiroo.fanfix.bundles.Config;
16 import be.nikiroo.fanfix.data.Chapter;
17 import be.nikiroo.fanfix.data.MetaData;
18 import be.nikiroo.fanfix.data.Paragraph;
19 import be.nikiroo.fanfix.data.Story;
20 import be.nikiroo.utils.Progress;
21 import be.nikiroo.utils.Progress.ProgressListener;
22 import be.nikiroo.utils.StringUtils;
23 import be.nikiroo.utils.Version;
24 import be.nikiroo.utils.serial.server.ConnectActionServerObject;
25 import be.nikiroo.utils.serial.server.ServerObject;
26
27 /**
28 * Create a new remote server that will listen for orders on the given port.
29 * <p>
30 * The available commands are given as arrays of objects (first item is the
31 * command, the rest are the arguments).
32 * <p>
33 * All the commands are always prefixed by the subkey (which can be EMPTY if
34 * none).
35 * <p>
36 * <ul>
37 * <li>PING: will return the mode if the key is accepted (mode can be: "r/o" or
38 * "r/w")</li>
39 * <li>GET_METADATA *: will return the metadata of all the stories in the
40 * library (array)</li> *
41 * <li>GET_METADATA [luid]: will return the metadata of the story of LUID
42 * luid</li>
43 * <li>GET_STORY [luid]: will return the given story if it exists (or NULL if
44 * not)</li>
45 * <li>SAVE_STORY [luid]: save the story (that must be sent just after the
46 * command) with the given LUID, then return the LUID</li>
47 * <li>IMPORT [url]: save the story found at the given URL, then return the LUID
48 * </li>
49 * <li>DELETE_STORY [luid]: delete the story of LUID luid</li>
50 * <li>GET_COVER [luid]: return the cover of the story</li>
51 * <li>GET_CUSTOM_COVER ["SOURCE"|"AUTHOR"] [source]: return the cover for this
52 * source/author</li>
53 * <li>SET_COVER ["SOURCE"|"AUTHOR"] [value] [luid]: set the default cover for
54 * the given source/author to the cover of the story denoted by luid</li>
55 * <li>CHANGE_SOURCE [luid] [new source]: change the source of the story of LUID
56 * luid</li>
57 * <li>EXIT: stop the server</li>
58 * </ul>
59 *
60 * @author niki
61 */
62 public class RemoteLibraryServer extends ServerObject {
63 private Map<Long, String> commands = new HashMap<Long, String>();
64 private Map<Long, Long> times = new HashMap<Long, Long>();
65 private Map<Long, Boolean> wls = new HashMap<Long, Boolean>();
66 private Map<Long, Boolean> bls = new HashMap<Long, Boolean>();
67 private Map<Long, Boolean> rws = new HashMap<Long, Boolean>();
68
69 /**
70 * Create a new remote server (will not be active until
71 * {@link RemoteLibraryServer#start()} is called).
72 * <p>
73 * Note: the key we use here is the encryption key (it must not contain a
74 * subkey).
75 *
76 * @throws IOException
77 * in case of I/O error
78 */
79 public RemoteLibraryServer() throws IOException {
80 super("Fanfix remote library",
81 Instance.getInstance().getConfig()
82 .getInteger(Config.SERVER_PORT),
83 Instance.getInstance().getConfig()
84 .getString(Config.SERVER_KEY));
85
86 setTraceHandler(Instance.getInstance().getTraceHandler());
87 }
88
89 @Override
90 protected Object onRequest(ConnectActionServerObject action,
91 Version clientVersion, Object data, long id) throws Exception {
92 long start = new Date().getTime();
93
94 // defaults are positive (as previous versions without the feature)
95 boolean rw = true;
96 boolean wl = true;
97 boolean bl = true;
98
99 String subkey = "";
100 String command = "";
101 Object[] args = new Object[0];
102 if (data instanceof Object[]) {
103 Object[] dataArray = (Object[]) data;
104 if (dataArray.length > 0) {
105 subkey = "" + dataArray[0];
106 }
107 if (dataArray.length > 1) {
108 command = "" + dataArray[1];
109
110 args = new Object[dataArray.length - 2];
111 for (int i = 2; i < dataArray.length; i++) {
112 args[i - 2] = dataArray[i];
113 }
114 }
115 }
116
117 List<String> whitelist = Instance.getInstance().getConfig()
118 .getList(Config.SERVER_WHITELIST);
119 if (whitelist == null) {
120 whitelist = new ArrayList<String>();
121 }
122 List<String> blacklist = Instance.getInstance().getConfig()
123 .getList(Config.SERVER_BLACKLIST);
124 if (blacklist == null) {
125 blacklist = new ArrayList<String>();
126 }
127
128 if (whitelist.isEmpty()) {
129 wl = false;
130 }
131
132 rw = Instance.getInstance().getConfig().getBoolean(Config.SERVER_RW,
133 rw);
134 if (!subkey.isEmpty()) {
135 List<String> allowed = Instance.getInstance().getConfig()
136 .getList(Config.SERVER_ALLOWED_SUBKEYS);
137 if (allowed.contains(subkey)) {
138 if ((subkey + "|").contains("|rw|")) {
139 rw = true;
140 }
141 if ((subkey + "|").contains("|wl|")) {
142 wl = false; // |wl| = bypass whitelist
143 whitelist = new ArrayList<String>();
144 }
145 if ((subkey + "|").contains("|bl|")) {
146 bl = false; // |bl| = bypass blacklist
147 blacklist = new ArrayList<String>();
148 }
149 }
150 }
151
152 String mode = display(wl, bl, rw);
153
154 String trace = mode + "[ " + command + "] ";
155 for (Object arg : args) {
156 trace += arg + " ";
157 }
158 long now = System.currentTimeMillis();
159 System.out.println(StringUtils.fromTime(now) + ": " + trace);
160
161 Object rep = null;
162 try {
163 rep = doRequest(action, command, args, rw, whitelist, blacklist);
164 } catch (IOException e) {
165 rep = new RemoteLibraryException(e, true);
166 }
167
168 commands.put(id, command);
169 wls.put(id, wl);
170 bls.put(id, bl);
171 rws.put(id, rw);
172 times.put(id, (new Date().getTime() - start));
173
174 return rep;
175 }
176
177 private String display(boolean whitelist, boolean blacklist, boolean rw) {
178 String mode = "";
179 if (!rw) {
180 mode += "RO: ";
181 }
182 if (whitelist) {
183 mode += "WL: ";
184 }
185 if (blacklist) {
186 mode += "BL: ";
187 }
188
189 return mode;
190 }
191
192 @Override
193 protected void onRequestDone(long id, long bytesReceived, long bytesSent) {
194 boolean whitelist = wls.get(id);
195 boolean blacklist = bls.get(id);
196 boolean rw = rws.get(id);
197 wls.remove(id);
198 bls.remove(id);
199 rws.remove(id);
200
201 String rec = StringUtils.formatNumber(bytesReceived) + "b";
202 String sent = StringUtils.formatNumber(bytesSent) + "b";
203 long now = System.currentTimeMillis();
204 System.out.println(StringUtils.fromTime(now) + ": "
205 + String.format("%s[>%s]: (%s sent, %s rec) in %d ms",
206 display(whitelist, blacklist, rw), commands.get(id),
207 sent, rec, times.get(id)));
208
209 commands.remove(id);
210 times.remove(id);
211 }
212
213 private Object doRequest(ConnectActionServerObject action, String command,
214 Object[] args, boolean rw, List<String> whitelist,
215 List<String> blacklist) throws NoSuchFieldException,
216 NoSuchMethodException, ClassNotFoundException, IOException {
217 if ("PING".equals(command)) {
218 return rw ? "r/w" : "r/o";
219 } else if ("GET_METADATA".equals(command)) {
220 List<MetaData> metas = new ArrayList<MetaData>();
221
222 if ("*".equals(args[0])) {
223 Progress pg = createPgForwarder(action);
224
225 for (MetaData meta : Instance.getInstance().getLibrary()
226 .getMetas(pg)) {
227 metas.add(removeCover(meta));
228 }
229
230 forcePgDoneSent(pg);
231 } else {
232 MetaData meta = Instance.getInstance().getLibrary()
233 .getInfo((String) args[0]);
234 MetaData light;
235 if (meta.getCover() == null) {
236 light = meta;
237 } else {
238 light = meta.clone();
239 light.setCover(null);
240 }
241
242 metas.add(light);
243 }
244
245 for (int i = 0; i < metas.size(); i++) {
246 if (!isAllowed(metas.get(i), whitelist, blacklist)) {
247 metas.remove(i);
248 i--;
249 }
250 }
251
252 return metas.toArray(new MetaData[0]);
253
254 } else if ("GET_STORY".equals(command)) {
255 MetaData meta = Instance.getInstance().getLibrary()
256 .getInfo((String) args[0]);
257 if (meta == null || !isAllowed(meta, whitelist, blacklist)) {
258 return null;
259 }
260
261 meta = meta.clone();
262 meta.setCover(null);
263
264 action.send(meta);
265 action.rec();
266
267 Story story = Instance.getInstance().getLibrary()
268 .getStory((String) args[0], null);
269 for (Object obj : breakStory(story)) {
270 action.send(obj);
271 action.rec();
272 }
273 } else if ("SAVE_STORY".equals(command)) {
274 if (!rw) {
275 throw new RemoteLibraryException(
276 "Read-Only remote library: " + args[0], false);
277 }
278
279 List<Object> list = new ArrayList<Object>();
280
281 action.send(null);
282 Object obj = action.rec();
283 while (obj != null) {
284 list.add(obj);
285 action.send(null);
286 obj = action.rec();
287 }
288
289 Story story = rebuildStory(list);
290 Instance.getInstance().getLibrary().save(story, (String) args[0],
291 null);
292 return story.getMeta().getLuid();
293 } else if ("IMPORT".equals(command)) {
294 if (!rw) {
295 throw new RemoteLibraryException(
296 "Read-Only remote library: " + args[0], false);
297 }
298
299 Progress pg = createPgForwarder(action);
300 MetaData meta = Instance.getInstance().getLibrary()
301 .imprt(new URL((String) args[0]), pg);
302 forcePgDoneSent(pg);
303 return meta.getLuid();
304 } else if ("DELETE_STORY".equals(command)) {
305 if (!rw) {
306 throw new RemoteLibraryException(
307 "Read-Only remote library: " + args[0], false);
308 }
309
310 Instance.getInstance().getLibrary().delete((String) args[0]);
311 } else if ("GET_COVER".equals(command)) {
312 return Instance.getInstance().getLibrary()
313 .getCover((String) args[0]);
314 } else if ("GET_CUSTOM_COVER".equals(command)) {
315 if ("SOURCE".equals(args[0])) {
316 return Instance.getInstance().getLibrary()
317 .getCustomSourceCover((String) args[1]);
318 } else if ("AUTHOR".equals(args[0])) {
319 return Instance.getInstance().getLibrary()
320 .getCustomAuthorCover((String) args[1]);
321 } else {
322 return null;
323 }
324 } else if ("SET_COVER".equals(command)) {
325 if (!rw) {
326 throw new RemoteLibraryException(
327 "Read-Only remote library: " + args[0] + ", " + args[1],
328 false);
329 }
330
331 if ("SOURCE".equals(args[0])) {
332 Instance.getInstance().getLibrary()
333 .setSourceCover((String) args[1], (String) args[2]);
334 } else if ("AUTHOR".equals(args[0])) {
335 Instance.getInstance().getLibrary()
336 .setAuthorCover((String) args[1], (String) args[2]);
337 }
338 } else if ("CHANGE_STA".equals(command)) {
339 if (!rw) {
340 throw new RemoteLibraryException(
341 "Read-Only remote library: " + args[0] + ", " + args[1],
342 false);
343 }
344
345 Progress pg = createPgForwarder(action);
346 Instance.getInstance().getLibrary().changeSTA((String) args[0],
347 (String) args[1], (String) args[2], (String) args[3], pg);
348 forcePgDoneSent(pg);
349 } else if ("EXIT".equals(command)) {
350 if (!rw) {
351 throw new RemoteLibraryException(
352 "Read-Only remote library: EXIT", false);
353 }
354
355 stop(10000, false);
356 }
357
358 return null;
359 }
360
361 @Override
362 protected void onError(Exception e) {
363 if (e instanceof SSLException) {
364 long now = System.currentTimeMillis();
365 System.out.println(StringUtils.fromTime(now) + ": "
366 + "[Client connection refused (bad key)]");
367 } else {
368 getTraceHandler().error(e);
369 }
370 }
371
372 /**
373 * Break a story in multiple {@link Object}s for easier serialisation.
374 *
375 * @param story
376 * the {@link Story} to break
377 *
378 * @return the list of {@link Object}s
379 */
380 static List<Object> breakStory(Story story) {
381 List<Object> list = new ArrayList<Object>();
382
383 story = story.clone();
384 list.add(story);
385
386 if (story.getMeta().isImageDocument()) {
387 for (Chapter chap : story) {
388 list.add(chap);
389 list.addAll(chap.getParagraphs());
390 chap.setParagraphs(new ArrayList<Paragraph>());
391 }
392 story.setChapters(new ArrayList<Chapter>());
393 }
394
395 return list;
396 }
397
398 /**
399 * Rebuild a story from a list of broke up {@link Story} parts.
400 *
401 * @param list
402 * the list of {@link Story} parts
403 *
404 * @return the reconstructed {@link Story}
405 */
406 static Story rebuildStory(List<Object> list) {
407 Story story = null;
408 Chapter chap = null;
409
410 for (Object obj : list) {
411 if (obj instanceof Story) {
412 story = (Story) obj;
413 } else if (obj instanceof Chapter) {
414 chap = (Chapter) obj;
415 story.getChapters().add(chap);
416 } else if (obj instanceof Paragraph) {
417 chap.getParagraphs().add((Paragraph) obj);
418 }
419 }
420
421 return story;
422 }
423
424 /**
425 * Update the {@link Progress} with the adequate {@link Object} received
426 * from the network via {@link RemoteLibraryServer}.
427 *
428 * @param pg
429 * the {@link Progress} to update
430 * @param rep
431 * the object received from the network
432 *
433 * @return TRUE if it was a progress event, FALSE if not
434 */
435 static boolean updateProgress(Progress pg, Object rep) {
436 boolean updateProgress = false;
437 if (rep instanceof Integer[] && ((Integer[]) rep).length == 3)
438 updateProgress = true;
439 if (rep instanceof Object[] && ((Object[]) rep).length >= 5
440 && "UPDATE".equals(((Object[]) rep)[0]))
441 updateProgress = true;
442
443 if (updateProgress) {
444 Object[] a = (Object[]) rep;
445
446 int offset = 0;
447 if (a[0] instanceof String) {
448 offset = 1;
449 }
450
451 int min = (Integer) a[0 + offset];
452 int max = (Integer) a[1 + offset];
453 int progress = (Integer) a[2 + offset];
454
455 Object meta = null;
456 if (a.length > (3 + offset)) {
457 meta = a[3 + offset];
458 }
459
460 String name = null;
461 if (a.length > (4 + offset)) {
462 name = a[4 + offset] == null ? "" : a[4 + offset].toString();
463 }
464
465 if (min >= 0 && min <= max) {
466 pg.setName(name);
467 pg.setMinMax(min, max);
468 pg.setProgress(progress);
469 if (meta != null) {
470 pg.put("meta", meta);
471 }
472
473 return true;
474 }
475 }
476
477 return false;
478 }
479
480 /**
481 * Create a {@link Progress} that will forward its progress over the
482 * network.
483 *
484 * @param action
485 * the {@link ConnectActionServerObject} to use to forward it
486 *
487 * @return the {@link Progress}
488 */
489 private Progress createPgForwarder(final ConnectActionServerObject action) {
490 final Boolean[] isDoneForwarded = new Boolean[] { false };
491 final Progress pg = new Progress() {
492 @Override
493 public boolean isDone() {
494 return isDoneForwarded[0];
495 }
496 };
497
498 final Integer[] p = new Integer[] { -1, -1, -1 };
499 final Object[] pMeta = new MetaData[1];
500 final String[] pName = new String[1];
501 final Long[] lastTime = new Long[] { new Date().getTime() };
502 pg.addProgressListener(new ProgressListener() {
503 @Override
504 public void progress(Progress progress, String name) {
505 Object meta = pg.get("meta");
506 if (meta instanceof MetaData) {
507 meta = removeCover((MetaData) meta);
508 }
509
510 int min = pg.getMin();
511 int max = pg.getMax();
512 int rel = min + (int) Math
513 .round(pg.getRelativeProgress() * (max - min));
514
515 boolean samePg = p[0] == min && p[1] == max && p[2] == rel;
516
517 // Do not re-send the same value twice over the wire,
518 // unless more than 2 seconds have elapsed (to maintain the
519 // connection)
520 if (!samePg || !same(pMeta[0], meta) || !same(pName[0], name) //
521 || (new Date().getTime() - lastTime[0] > 2000)) {
522 p[0] = min;
523 p[1] = max;
524 p[2] = rel;
525 pMeta[0] = meta;
526 pName[0] = name;
527
528 try {
529 action.send(new Object[] { "UPDATE", min, max, rel,
530 meta, name });
531 action.rec();
532 } catch (Exception e) {
533 getTraceHandler().error(e);
534 }
535
536 lastTime[0] = new Date().getTime();
537 }
538
539 isDoneForwarded[0] = (pg.getProgress() >= pg.getMax());
540 }
541 });
542
543 return pg;
544 }
545
546 private boolean same(Object obj1, Object obj2) {
547 if (obj1 == null || obj2 == null)
548 return obj1 == null && obj2 == null;
549
550 return obj1.equals(obj2);
551 }
552
553 // with 30 seconds timeout
554 private void forcePgDoneSent(Progress pg) {
555 long start = new Date().getTime();
556 pg.done();
557 while (!pg.isDone() && new Date().getTime() - start < 30000) {
558 try {
559 Thread.sleep(100);
560 } catch (InterruptedException e) {
561 getTraceHandler().error(e);
562 }
563 }
564 }
565
566 private MetaData removeCover(MetaData meta) {
567 MetaData light = null;
568 if (meta != null) {
569 if (meta.getCover() == null) {
570 light = meta;
571 } else {
572 light = meta.clone();
573 light.setCover(null);
574 }
575 }
576
577 return light;
578 }
579
580 private boolean isAllowed(MetaData meta, List<String> whitelist,
581 List<String> blacklist) {
582 MetaResultList one = new MetaResultList(Arrays.asList(meta));
583 if (!whitelist.isEmpty()) {
584 if (one.filter(whitelist, null, null).isEmpty()) {
585 return false;
586 }
587 }
588 if (!blacklist.isEmpty()) {
589 if (!one.filter(blacklist, null, null).isEmpty()) {
590 return false;
591 }
592 }
593
594 return true;
595 }
596 }