Merge branch 'master' into subtree
[nikiroo-utils.git] / library / RemoteLibraryServer.java
CommitLineData
e42573a0 1package be.nikiroo.fanfix.library;
b0e88ebd 2
b0e88ebd 3import java.io.IOException;
edf79e5e 4import java.net.URL;
74a40dfb 5import java.util.ArrayList;
4536c5cf 6import java.util.Arrays;
9b863b20 7import java.util.Date;
9b558341 8import java.util.HashMap;
68e2c6d2 9import java.util.List;
9b558341 10import java.util.Map;
b0e88ebd 11
7e51d91c
NR
12import javax.net.ssl.SSLException;
13
e42573a0 14import be.nikiroo.fanfix.Instance;
fb25273c 15import be.nikiroo.fanfix.bundles.Config;
74a40dfb 16import be.nikiroo.fanfix.data.Chapter;
b0e88ebd 17import be.nikiroo.fanfix.data.MetaData;
74a40dfb 18import be.nikiroo.fanfix.data.Paragraph;
085a2f9a 19import be.nikiroo.fanfix.data.Story;
b9ce9cad
NR
20import be.nikiroo.utils.Progress;
21import be.nikiroo.utils.Progress.ProgressListener;
416c54f8 22import be.nikiroo.utils.StringUtils;
fb25273c 23import be.nikiroo.utils.Version;
62c63b07
NR
24import be.nikiroo.utils.serial.server.ConnectActionServerObject;
25import be.nikiroo.utils.serial.server.ServerObject;
b0e88ebd 26
a85e8077 27/**
ea734ab4 28 * Create a new remote server that will listen for orders on the given port.
a85e8077 29 * <p>
ea734ab4
N
30 * The available commands are given as arrays of objects (first item is the
31 * command, the rest are the arguments).
2a25f781 32 * <p>
fb25273c
NR
33 * All the commands are always prefixed by the subkey (which can be EMPTY if
34 * none).
35 * <p>
a85e8077 36 * <ul>
fb25273c
NR
37 * <li>PING: will return the mode if the key is accepted (mode can be: "r/o" or
38 * "r/w")</li>
ea734ab4
N
39 * <li>GET_METADATA *: will return the metadata of all the stories in the
40 * library (array)</li> *
c91f1830
NR
41 * <li>GET_METADATA [luid]: will return the metadata of the story of LUID
42 * luid</li>
ea734ab4
N
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
0fa0fe95 46 * command) with the given LUID, then return the LUID</li>
37abe20c
NR
47 * <li>IMPORT [url]: save the story found at the given URL, then return the LUID
48 * </li>
ea734ab4
N
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>
a85e8077
NR
58 * </ul>
59 *
60 * @author niki
61 */
62c63b07 62public class RemoteLibraryServer extends ServerObject {
9b558341
NR
63 private Map<Long, String> commands = new HashMap<Long, String>();
64 private Map<Long, Long> times = new HashMap<Long, Long>();
fb25273c 65 private Map<Long, Boolean> wls = new HashMap<Long, Boolean>();
c91f1830 66 private Map<Long, Boolean> bls = new HashMap<Long, Boolean>();
fb25273c 67 private Map<Long, Boolean> rws = new HashMap<Long, Boolean>();
9b558341 68
a85e8077
NR
69 /**
70 * Create a new remote server (will not be active until
71 * {@link RemoteLibraryServer#start()} is called).
fb25273c
NR
72 * <p>
73 * Note: the key we use here is the encryption key (it must not contain a
74 * subkey).
a85e8077 75 *
a85e8077
NR
76 * @throws IOException
77 * in case of I/O error
78 */
f433d153
NR
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));
c91f1830 85
d66deb8d 86 setTraceHandler(Instance.getInstance().getTraceHandler());
b0e88ebd
NR
87 }
88
89 @Override
fb25273c
NR
90 protected Object onRequest(ConnectActionServerObject action,
91 Version clientVersion, Object data, long id) throws Exception {
ea734ab4
N
92 long start = new Date().getTime();
93
fb25273c
NR
94 // defaults are positive (as previous versions without the feature)
95 boolean rw = true;
96 boolean wl = true;
c91f1830 97 boolean bl = true;
fb25273c
NR
98
99 String subkey = "";
085a2f9a
NR
100 String command = "";
101 Object[] args = new Object[0];
102 if (data instanceof Object[]) {
2070ced5 103 Object[] dataArray = (Object[]) data;
27eba894 104 if (dataArray.length > 0) {
fb25273c
NR
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
c91f1830
NR
117 List<String> whitelist = Instance.getInstance().getConfig()
118 .getList(Config.SERVER_WHITELIST);
fb25273c
NR
119 if (whitelist == null) {
120 whitelist = new ArrayList<String>();
121 }
c91f1830
NR
122 List<String> blacklist = Instance.getInstance().getConfig()
123 .getList(Config.SERVER_BLACKLIST);
124 if (blacklist == null) {
125 blacklist = new ArrayList<String>();
126 }
fb25273c
NR
127
128 if (whitelist.isEmpty()) {
129 wl = false;
130 }
edf79e5e 131
c91f1830
NR
132 rw = Instance.getInstance().getConfig().getBoolean(Config.SERVER_RW,
133 rw);
fb25273c 134 if (!subkey.isEmpty()) {
c91f1830
NR
135 List<String> allowed = Instance.getInstance().getConfig()
136 .getList(Config.SERVER_ALLOWED_SUBKEYS);
fb25273c
NR
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
651072f3 143 whitelist = new ArrayList<String>();
2070ced5 144 }
c91f1830
NR
145 if ((subkey + "|").contains("|bl|")) {
146 bl = false; // |bl| = bypass blacklist
147 blacklist = new ArrayList<String>();
148 }
b0e88ebd
NR
149 }
150 }
151
c91f1830 152 String mode = display(wl, bl, rw);
fb25273c
NR
153
154 String trace = mode + "[ " + command + "] ";
085a2f9a 155 for (Object arg : args) {
b9ce9cad 156 trace += arg + " ";
085a2f9a 157 }
c1bb921e
NR
158 long now = System.currentTimeMillis();
159 System.out.println(StringUtils.fromTime(now) + ": " + trace);
b0e88ebd 160
c1b31971 161 Object rep = null;
c1b31971 162 try {
c91f1830 163 rep = doRequest(action, command, args, rw, whitelist, blacklist);
5db598bc 164 } catch (IOException e) {
533dc2b8 165 rep = new RemoteLibraryException(e, true);
c1b31971 166 }
9f51d8ab 167
9b558341 168 commands.put(id, command);
fb25273c 169 wls.put(id, wl);
c91f1830 170 bls.put(id, bl);
fb25273c 171 rws.put(id, rw);
9b558341 172 times.put(id, (new Date().getTime() - start));
9f51d8ab
NR
173
174 return rep;
175 }
176
c91f1830 177 private String display(boolean whitelist, boolean blacklist, boolean rw) {
fb25273c
NR
178 String mode = "";
179 if (!rw) {
180 mode += "RO: ";
181 }
182 if (whitelist) {
183 mode += "WL: ";
184 }
c91f1830
NR
185 if (blacklist) {
186 mode += "BL: ";
187 }
fb25273c
NR
188
189 return mode;
190 }
191
9b558341
NR
192 @Override
193 protected void onRequestDone(long id, long bytesReceived, long bytesSent) {
fb25273c 194 boolean whitelist = wls.get(id);
c91f1830 195 boolean blacklist = bls.get(id);
fb25273c
NR
196 boolean rw = rws.get(id);
197 wls.remove(id);
c91f1830 198 bls.remove(id);
fb25273c
NR
199 rws.remove(id);
200
9b558341
NR
201 String rec = StringUtils.formatNumber(bytesReceived) + "b";
202 String sent = StringUtils.formatNumber(bytesSent) + "b";
c1bb921e 203 long now = System.currentTimeMillis();
c91f1830 204 System.out.println(StringUtils.fromTime(now) + ": "
c1bb921e 205 + String.format("%s[>%s]: (%s sent, %s rec) in %d ms",
c91f1830
NR
206 display(whitelist, blacklist, rw), commands.get(id),
207 sent, rec, times.get(id)));
fb25273c 208
9b558341
NR
209 commands.remove(id);
210 times.remove(id);
211 }
212
9f51d8ab 213 private Object doRequest(ConnectActionServerObject action, String command,
c91f1830
NR
214 Object[] args, boolean rw, List<String> whitelist,
215 List<String> blacklist) throws NoSuchFieldException,
216 NoSuchMethodException, ClassNotFoundException, IOException {
3bbc86a5 217 if ("PING".equals(command)) {
fb25273c 218 return rw ? "r/w" : "r/o";
3bbc86a5 219 } else if ("GET_METADATA".equals(command)) {
651072f3
NR
220 List<MetaData> metas = new ArrayList<MetaData>();
221
7efece85 222 if ("*".equals(args[0])) {
9b863b20 223 Progress pg = createPgForwarder(action);
9f51d8ab 224
c91f1830
NR
225 for (MetaData meta : Instance.getInstance().getLibrary()
226 .getMetas(pg)) {
a5d1f0e6 227 metas.add(removeCover(meta));
9f51d8ab
NR
228 }
229
9b863b20 230 forcePgDoneSent(pg);
651072f3 231 } else {
c91f1830
NR
232 MetaData meta = Instance.getInstance().getLibrary()
233 .getInfo((String) args[0]);
210465c3
NR
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);
651072f3
NR
243 }
244
c91f1830
NR
245 for (int i = 0; i < metas.size(); i++) {
246 if (!isAllowed(metas.get(i), whitelist, blacklist)) {
247 metas.remove(i);
248 i--;
651072f3 249 }
a85e8077 250 }
e272f05f 251
651072f3 252 return metas.toArray(new MetaData[0]);
c91f1830 253
a85e8077 254 } else if ("GET_STORY".equals(command)) {
c91f1830
NR
255 MetaData meta = Instance.getInstance().getLibrary()
256 .getInfo((String) args[0]);
257 if (meta == null || !isAllowed(meta, whitelist, blacklist)) {
651072f3
NR
258 return null;
259 }
260
b9ce9cad
NR
261 meta = meta.clone();
262 meta.setCover(null);
263
264 action.send(meta);
265 action.rec();
266
c91f1830
NR
267 Story story = Instance.getInstance().getLibrary()
268 .getStory((String) args[0], null);
b9ce9cad
NR
269 for (Object obj : breakStory(story)) {
270 action.send(obj);
271 action.rec();
272 }
085a2f9a 273 } else if ("SAVE_STORY".equals(command)) {
651072f3 274 if (!rw) {
c91f1830
NR
275 throw new RemoteLibraryException(
276 "Read-Only remote library: " + args[0], false);
651072f3
NR
277 }
278
b9ce9cad
NR
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);
c91f1830
NR
290 Instance.getInstance().getLibrary().save(story, (String) args[0],
291 null);
0fa0fe95 292 return story.getMeta().getLuid();
edf79e5e 293 } else if ("IMPORT".equals(command)) {
651072f3 294 if (!rw) {
c91f1830
NR
295 throw new RemoteLibraryException(
296 "Read-Only remote library: " + args[0], false);
651072f3
NR
297 }
298
9b863b20 299 Progress pg = createPgForwarder(action);
c91f1830
NR
300 MetaData meta = Instance.getInstance().getLibrary()
301 .imprt(new URL((String) args[0]), pg);
9b863b20 302 forcePgDoneSent(pg);
b6b65795 303 return meta.getLuid();
085a2f9a 304 } else if ("DELETE_STORY".equals(command)) {
651072f3 305 if (!rw) {
c91f1830
NR
306 throw new RemoteLibraryException(
307 "Read-Only remote library: " + args[0], false);
651072f3
NR
308 }
309
d66deb8d 310 Instance.getInstance().getLibrary().delete((String) args[0]);
e604986c 311 } else if ("GET_COVER".equals(command)) {
c91f1830
NR
312 return Instance.getInstance().getLibrary()
313 .getCover((String) args[0]);
3989dfc5
NR
314 } else if ("GET_CUSTOM_COVER".equals(command)) {
315 if ("SOURCE".equals(args[0])) {
c91f1830
NR
316 return Instance.getInstance().getLibrary()
317 .getCustomSourceCover((String) args[1]);
3989dfc5 318 } else if ("AUTHOR".equals(args[0])) {
c91f1830
NR
319 return Instance.getInstance().getLibrary()
320 .getCustomAuthorCover((String) args[1]);
3989dfc5
NR
321 } else {
322 return null;
323 }
324 } else if ("SET_COVER".equals(command)) {
651072f3 325 if (!rw) {
c91f1830
NR
326 throw new RemoteLibraryException(
327 "Read-Only remote library: " + args[0] + ", " + args[1],
328 false);
651072f3
NR
329 }
330
3989dfc5 331 if ("SOURCE".equals(args[0])) {
c91f1830
NR
332 Instance.getInstance().getLibrary()
333 .setSourceCover((String) args[1], (String) args[2]);
3989dfc5 334 } else if ("AUTHOR".equals(args[0])) {
c91f1830
NR
335 Instance.getInstance().getLibrary()
336 .setAuthorCover((String) args[1], (String) args[2]);
3989dfc5 337 }
c8d48938 338 } else if ("CHANGE_STA".equals(command)) {
651072f3 339 if (!rw) {
c91f1830
NR
340 throw new RemoteLibraryException(
341 "Read-Only remote library: " + args[0] + ", " + args[1],
342 false);
651072f3
NR
343 }
344
9b863b20 345 Progress pg = createPgForwarder(action);
c91f1830
NR
346 Instance.getInstance().getLibrary().changeSTA((String) args[0],
347 (String) args[1], (String) args[2], (String) args[3], pg);
9b863b20 348 forcePgDoneSent(pg);
5e848e6a 349 } else if ("EXIT".equals(command)) {
651072f3 350 if (!rw) {
533dc2b8
NR
351 throw new RemoteLibraryException(
352 "Read-Only remote library: EXIT", false);
651072f3
NR
353 }
354
c08c6ca1 355 stop(10000, false);
b0e88ebd
NR
356 }
357
358 return null;
359 }
74a40dfb 360
b9ce9cad
NR
361 @Override
362 protected void onError(Exception e) {
7e51d91c 363 if (e instanceof SSLException) {
c1bb921e
NR
364 long now = System.currentTimeMillis();
365 System.out.println(StringUtils.fromTime(now) + ": "
366 + "[Client connection refused (bad key)]");
7e51d91c
NR
367 } else {
368 getTraceHandler().error(e);
369 }
b9ce9cad 370 }
74a40dfb 371
b9ce9cad
NR
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>();
74a40dfb
NR
382
383 story = story.clone();
b9ce9cad 384 list.add(story);
74a40dfb 385
b9ce9cad
NR
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>());
74a40dfb 391 }
b9ce9cad 392 story.setChapters(new ArrayList<Chapter>());
74a40dfb 393 }
74a40dfb 394
b9ce9cad
NR
395 return list;
396 }
74a40dfb 397
b9ce9cad
NR
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) {
74a40dfb 407 Story story = null;
b9ce9cad 408 Chapter chap = null;
74a40dfb 409
b9ce9cad
NR
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);
74a40dfb
NR
418 }
419 }
420
421 return story;
422 }
423
b9ce9cad
NR
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) {
1f5a9d0c
NR
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) {
a5d1f0e6 444 Object[] a = (Object[]) rep;
b9ce9cad 445
1f5a9d0c
NR
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];
c91f1830 454
1f5a9d0c
NR
455 Object meta = null;
456 if (a.length > (3 + offset)) {
457 meta = a[3 + offset];
458 }
c91f1830 459
95c926ea
NR
460 String name = null;
461 if (a.length > (4 + offset)) {
462 name = a[4 + offset] == null ? "" : a[4 + offset].toString();
463 }
1f5a9d0c
NR
464
465 if (min >= 0 && min <= max) {
95c926ea 466 pg.setName(name);
1f5a9d0c
NR
467 pg.setMinMax(min, max);
468 pg.setProgress(progress);
469 if (meta != null) {
470 pg.put("meta", meta);
b9ce9cad 471 }
1f5a9d0c
NR
472
473 return true;
b9ce9cad 474 }
74a40dfb 475 }
b9ce9cad
NR
476
477 return false;
74a40dfb
NR
478 }
479
b9ce9cad
NR
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 */
49f3dec5 489 private Progress createPgForwarder(final ConnectActionServerObject action) {
9b863b20
NR
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
b9ce9cad 498 final Integer[] p = new Integer[] { -1, -1, -1 };
a5d1f0e6 499 final Object[] pMeta = new MetaData[1];
95c926ea 500 final String[] pName = new String[1];
9b863b20 501 final Long[] lastTime = new Long[] { new Date().getTime() };
b9ce9cad
NR
502 pg.addProgressListener(new ProgressListener() {
503 @Override
504 public void progress(Progress progress, String name) {
a5d1f0e6
NR
505 Object meta = pg.get("meta");
506 if (meta instanceof MetaData) {
c91f1830 507 meta = removeCover((MetaData) meta);
a5d1f0e6 508 }
c91f1830 509
b9ce9cad
NR
510 int min = pg.getMin();
511 int max = pg.getMax();
c91f1830
NR
512 int rel = min + (int) Math
513 .round(pg.getRelativeProgress() * (max - min));
514
a5d1f0e6 515 boolean samePg = p[0] == min && p[1] == max && p[2] == rel;
c91f1830 516
9b863b20
NR
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)
c91f1830 520 if (!samePg || !same(pMeta[0], meta) || !same(pName[0], name) //
9b863b20 521 || (new Date().getTime() - lastTime[0] > 2000)) {
b9ce9cad
NR
522 p[0] = min;
523 p[1] = max;
a5d1f0e6
NR
524 p[2] = rel;
525 pMeta[0] = meta;
95c926ea 526 pName[0] = name;
b9ce9cad
NR
527
528 try {
95c926ea
NR
529 action.send(new Object[] { "UPDATE", min, max, rel,
530 meta, name });
b9ce9cad
NR
531 action.rec();
532 } catch (Exception e) {
49f3dec5 533 getTraceHandler().error(e);
b9ce9cad 534 }
9b863b20 535
9b863b20 536 lastTime[0] = new Date().getTime();
b9ce9cad 537 }
652fd9b0
NR
538
539 isDoneForwarded[0] = (pg.getProgress() >= pg.getMax());
b9ce9cad
NR
540 }
541 });
542
543 return pg;
74a40dfb 544 }
c91f1830 545
a5d1f0e6
NR
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 }
9b863b20
NR
552
553 // with 30 seconds timeout
49f3dec5 554 private void forcePgDoneSent(Progress pg) {
9b863b20
NR
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) {
49f3dec5 561 getTraceHandler().error(e);
9b863b20
NR
562 }
563 }
564 }
c91f1830 565
a5d1f0e6 566 private MetaData removeCover(MetaData meta) {
eccb0cbc
NR
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 }
a5d1f0e6 575 }
c91f1830 576
a5d1f0e6
NR
577 return light;
578 }
c91f1830
NR
579
580 private boolean isAllowed(MetaData meta, List<String> whitelist,
581 List<String> blacklist) {
4536c5cf
NR
582 MetaResultList one = new MetaResultList(Arrays.asList(meta));
583 if (!whitelist.isEmpty()) {
584 if (one.filter(whitelist, null, null).isEmpty()) {
585 return false;
586 }
c91f1830 587 }
4536c5cf
NR
588 if (!blacklist.isEmpty()) {
589 if (!one.filter(blacklist, null, null).isEmpty()) {
590 return false;
591 }
c91f1830
NR
592 }
593
594 return true;
595 }
b0e88ebd 596}