fix lib
[fanfix.git] / src / be / nikiroo / fanfix / library / RemoteLibrary.java
CommitLineData
e42573a0 1package be.nikiroo.fanfix.library;
b0e88ebd
NR
2
3import java.io.File;
4import java.io.IOException;
edf79e5e 5import java.net.URL;
e6249b0f 6import java.net.UnknownHostException;
68e2c6d2
NR
7import java.util.ArrayList;
8import java.util.List;
b0e88ebd 9
7e51d91c
NR
10import javax.net.ssl.SSLException;
11
e42573a0 12import be.nikiroo.fanfix.Instance;
b0e88ebd
NR
13import be.nikiroo.fanfix.data.MetaData;
14import be.nikiroo.fanfix.data.Story;
16a81ef7 15import be.nikiroo.utils.Image;
b0e88ebd 16import be.nikiroo.utils.Progress;
fb25273c 17import be.nikiroo.utils.Version;
62c63b07 18import be.nikiroo.utils.serial.server.ConnectActionClientObject;
b0e88ebd 19
68e2c6d2
NR
20/**
21 * This {@link BasicLibrary} will access a remote server to list the available
a85e8077 22 * stories, and download the ones you try to load to the local directory
68e2c6d2
NR
23 * specified in the configuration.
24 *
25 * @author niki
26 */
27public class RemoteLibrary extends BasicLibrary {
b0e88ebd
NR
28 private String host;
29 private int port;
ea734ab4 30 private final String key;
fb25273c
NR
31 private final String subkey;
32
33 // informative only (server will make the actual checks)
34 private boolean rw;
68e2c6d2 35
99206a39
NR
36 // TODO: error handling is not up to par!
37
68e2c6d2
NR
38 /**
39 * Create a {@link RemoteLibrary} linked to the given server.
fb25273c
NR
40 * <p>
41 * Note that the key is structured:
42 * <tt><b><i>xxx</i></b>(|<b><i>yyy</i></b>|<b>wl</b>)(|<b>rw</b>)</tt>
43 * <p>
44 * Note that anything before the first pipe (<tt>|</tt>) character is
45 * considered to be the encryption key, anything after that character is
46 * called the subkey (including the other pipe characters and flags!).
47 * <p>
48 * This is important because the subkey (including the pipe characters and
49 * flags) must be present as-is in the server configuration file to be
50 * allowed.
51 * <ul>
52 * <li><b><i>xxx</i></b>: the encryption key used to communicate with the
53 * server</li>
54 * <li><b><i>yyy</i></b>: the secondary key</li>
55 * <li><b>rw</b>: flag to allow read and write access if it is not the
56 * default on this server</li>
57 * <li><b>wl</b>: flag to allow access to all the stories (bypassing the
58 * whitelist if it exists)</li>
59 * </ul>
60 *
61 * Some examples:
62 * <ul>
63 * <li><b>my_key</b>: normal connection, will take the default server
64 * options</li>
65 * <li><b>my_key|agzyzz|wl</b>: will ask to bypass the white list (if it
66 * exists)</li>
67 * <li><b>my_key|agzyzz|rw</b>: will ask read-write access (if the default
68 * is read-only)</li>
69 * <li><b>my_key|agzyzz|wl|rw</b>: will ask both read-write access and white
70 * list bypass</li>
71 * </ul>
68e2c6d2 72 *
2070ced5
NR
73 * @param key
74 * the key that will allow us to exchange information with the
75 * server
68e2c6d2
NR
76 * @param host
77 * the host to contact or NULL for localhost
78 * @param port
79 * the port to contact it on
80 */
2070ced5 81 public RemoteLibrary(String key, String host, int port) {
fb25273c
NR
82 int index = -1;
83 if (key != null) {
651072f3 84 index = key.indexOf('|');
fb25273c
NR
85 }
86
87 if (index >= 0) {
651072f3
NR
88 this.key = key.substring(0, index);
89 this.subkey = key.substring(index + 1);
fb25273c
NR
90 } else {
91 this.key = key;
92 this.subkey = "";
93 }
94
b0e88ebd
NR
95 this.host = host;
96 this.port = port;
b0e88ebd
NR
97 }
98
99ccbdf6
NR
99 @Override
100 public String getLibraryName() {
101 return host + ":" + port;
102 }
103
e6249b0f
NR
104 @Override
105 public Status getStatus() {
99206a39
NR
106 Instance.getTraceHandler().trace("Getting remote lib status...");
107 Status status = getStatusDo();
108 Instance.getTraceHandler().trace("Remote lib status: " + status);
109 return status;
110 }
111
112 private boolean check() {
113 Status status = getStatusDo();
114 if (status != Status.READY) {
115 Instance.getTraceHandler().error("Remote lib not ready: " + status);
116 return false;
117 }
118
119 return true;
120 }
121
122 private Status getStatusDo() {
e6249b0f
NR
123 final Status[] result = new Status[1];
124
125 result[0] = Status.INVALID;
126
e6249b0f 127 try {
3040c4f0 128 new ConnectActionClientObject(host, port, key) {
e6249b0f 129 @Override
fb25273c
NR
130 public void action(Version serverVersion) throws Exception {
131 Object rep = send(new Object[] { subkey, "PING" });
ea734ab4 132
fb25273c
NR
133 if ("r/w".equals(rep)) {
134 rw = true;
135 result[0] = Status.READY;
136 } else if ("r/o".equals(rep)) {
137 rw = false;
7e51d91c
NR
138 result[0] = Status.READY;
139 } else {
99206a39 140 result[0] = Status.UNAUTHORIZED;
e6249b0f
NR
141 }
142 }
143
144 @Override
145 protected void onError(Exception e) {
210465c3
NR
146 if (e instanceof SSLException) {
147 result[0] = Status.UNAUTHORIZED;
148 } else {
149 result[0] = Status.UNAVAILABLE;
150 }
e6249b0f 151 }
ea734ab4 152 }.connect();
e6249b0f
NR
153 } catch (UnknownHostException e) {
154 result[0] = Status.INVALID;
155 } catch (IllegalArgumentException e) {
156 result[0] = Status.INVALID;
157 } catch (Exception e) {
158 result[0] = Status.UNAVAILABLE;
159 }
160
e6249b0f
NR
161 return result[0];
162 }
163
b0e88ebd 164 @Override
16a81ef7 165 public Image getCover(final String luid) {
99206a39
NR
166 if (!check()) {
167 return null;
168 }
169
16a81ef7 170 final Image[] result = new Image[1];
b0e88ebd 171
b9ce9cad 172 try {
3040c4f0 173 new ConnectActionClientObject(host, port, key) {
b9ce9cad 174 @Override
fb25273c
NR
175 public void action(Version serverVersion) throws Exception {
176 Object rep = send(new Object[] { subkey, "GET_COVER", luid });
16a81ef7 177 result[0] = (Image) rep;
b9ce9cad 178 }
b0e88ebd 179
b9ce9cad
NR
180 @Override
181 protected void onError(Exception e) {
7e51d91c
NR
182 if (e instanceof SSLException) {
183 Instance.getTraceHandler().error(
184 "Connection refused (bad key)");
185 } else {
186 Instance.getTraceHandler().error(e);
187 }
b9ce9cad
NR
188 }
189 }.connect();
190 } catch (Exception e) {
191 Instance.getTraceHandler().error(e);
192 }
b0e88ebd 193
b9ce9cad 194 return result[0];
085a2f9a
NR
195 }
196
197 @Override
e1de8087 198 public Image getCustomSourceCover(final String source) {
99206a39
NR
199 if (!check()) {
200 return null;
201 }
202
3989dfc5
NR
203 return getCustomCover(source, "SOURCE");
204 }
205
206 @Override
207 public Image getCustomAuthorCover(final String author) {
99206a39
NR
208 if (!check()) {
209 return null;
210 }
211
3989dfc5
NR
212 return getCustomCover(author, "AUTHOR");
213 }
214
215 // type: "SOURCE" or "AUTHOR"
216 private Image getCustomCover(final String source, final String type) {
99206a39
NR
217 if (!check()) {
218 return null;
219 }
220
16a81ef7 221 final Image[] result = new Image[1];
b9ce9cad
NR
222
223 try {
3040c4f0 224 new ConnectActionClientObject(host, port, key) {
b9ce9cad 225 @Override
fb25273c
NR
226 public void action(Version serverVersion) throws Exception {
227 Object rep = send(new Object[] { subkey,
228 "GET_CUSTOM_COVER", type, source });
16a81ef7 229 result[0] = (Image) rep;
b9ce9cad
NR
230 }
231
232 @Override
233 protected void onError(Exception e) {
7e51d91c
NR
234 if (e instanceof SSLException) {
235 Instance.getTraceHandler().error(
236 "Connection refused (bad key)");
237 } else {
238 Instance.getTraceHandler().error(e);
239 }
b9ce9cad
NR
240 }
241 }.connect();
242 } catch (Exception e) {
243 Instance.getTraceHandler().error(e);
244 }
245
246 return result[0];
b0e88ebd 247 }
68e2c6d2
NR
248
249 @Override
ff05b828 250 public synchronized Story getStory(final String luid, Progress pg) {
99206a39
NR
251 if (!check()) {
252 return null;
253 }
254
b9ce9cad
NR
255 final Progress pgF = pg;
256 final Story[] result = new Story[1];
68e2c6d2 257
b9ce9cad 258 try {
3040c4f0 259 new ConnectActionClientObject(host, port, key) {
b9ce9cad 260 @Override
fb25273c 261 public void action(Version serverVersion) throws Exception {
b9ce9cad
NR
262 Progress pg = pgF;
263 if (pg == null) {
264 pg = new Progress();
265 }
266
fb25273c 267 Object rep = send(new Object[] { subkey, "GET_STORY", luid });
b9ce9cad
NR
268
269 MetaData meta = null;
270 if (rep instanceof MetaData) {
271 meta = (MetaData) rep;
272 if (meta.getWords() <= Integer.MAX_VALUE) {
273 pg.setMinMax(0, (int) meta.getWords());
274 }
275 }
276
277 List<Object> list = new ArrayList<Object>();
27eba894 278 for (Object obj = send(null); obj != null; obj = send(null)) {
b9ce9cad
NR
279 list.add(obj);
280 pg.add(1);
281 }
282
283 result[0] = RemoteLibraryServer.rebuildStory(list);
284 pg.done();
285 }
286
287 @Override
288 protected void onError(Exception e) {
7e51d91c
NR
289 if (e instanceof SSLException) {
290 Instance.getTraceHandler().error(
291 "Connection refused (bad key)");
292 } else {
293 Instance.getTraceHandler().error(e);
294 }
b9ce9cad
NR
295 }
296 }.connect();
297 } catch (Exception e) {
298 Instance.getTraceHandler().error(e);
299 }
300
301 return result[0];
68e2c6d2
NR
302 }
303
304 @Override
b9ce9cad
NR
305 public synchronized Story save(final Story story, final String luid,
306 Progress pg) throws IOException {
99206a39
NR
307 if (!check()) {
308 return null;
309 }
310
0fa0fe95
NR
311 final String[] luidSaved = new String[1];
312 Progress pgSave = new Progress();
313 Progress pgRefresh = new Progress();
314 if (pg == null) {
315 pg = new Progress();
316 }
317
318 pg.setMinMax(0, 10);
319 pg.addProgress(pgSave, 9);
320 pg.addProgress(pgRefresh, 1);
321
322 final Progress pgF = pgSave;
b9ce9cad 323
3040c4f0 324 new ConnectActionClientObject(host, port, key) {
b9ce9cad 325 @Override
fb25273c 326 public void action(Version serverVersion) throws Exception {
b9ce9cad 327 Progress pg = pgF;
b9ce9cad
NR
328 if (story.getMeta().getWords() <= Integer.MAX_VALUE) {
329 pg.setMinMax(0, (int) story.getMeta().getWords());
330 }
331
fb25273c 332 send(new Object[] { subkey, "SAVE_STORY", luid });
b9ce9cad
NR
333
334 List<Object> list = RemoteLibraryServer.breakStory(story);
335 for (Object obj : list) {
336 send(obj);
337 pg.add(1);
338 }
339
edf79e5e 340 luidSaved[0] = (String) send(null);
0fa0fe95 341
b9ce9cad
NR
342 pg.done();
343 }
344
345 @Override
346 protected void onError(Exception e) {
7e51d91c
NR
347 if (e instanceof SSLException) {
348 Instance.getTraceHandler().error(
349 "Connection refused (bad key)");
350 } else {
351 Instance.getTraceHandler().error(e);
352 }
b9ce9cad
NR
353 }
354 }.connect();
085a2f9a
NR
355
356 // because the meta changed:
edf79e5e 357 MetaData meta = getInfo(luidSaved[0]);
efa3c511
NR
358 if (story.getMeta().getClass() != null) {
359 // If already available locally:
360 meta.setCover(story.getMeta().getCover());
361 } else {
362 // If required:
363 meta.setCover(getCover(meta.getLuid()));
364 }
edf79e5e 365 story.setMeta(meta);
0fa0fe95
NR
366
367 pg.done();
085a2f9a
NR
368
369 return story;
68e2c6d2
NR
370 }
371
372 @Override
b9ce9cad 373 public synchronized void delete(final String luid) throws IOException {
99206a39
NR
374 if (!check()) {
375 throw new IOException("Library not ready");
376 }
377
3040c4f0 378 new ConnectActionClientObject(host, port, key) {
b9ce9cad 379 @Override
fb25273c
NR
380 public void action(Version serverVersion) throws Exception {
381 send(new Object[] { subkey, "DELETE_STORY", luid });
b9ce9cad
NR
382 }
383
384 @Override
385 protected void onError(Exception e) {
7e51d91c
NR
386 if (e instanceof SSLException) {
387 Instance.getTraceHandler().error(
388 "Connection refused (bad key)");
389 } else {
390 Instance.getTraceHandler().error(e);
391 }
b9ce9cad
NR
392 }
393 }.connect();
68e2c6d2
NR
394 }
395
396 @Override
b9ce9cad 397 public void setSourceCover(final String source, final String luid) {
99206a39
NR
398 if (!check()) {
399 return;
400 }
401
3989dfc5
NR
402 setCover(source, luid, "SOURCE");
403 }
404
405 @Override
406 public void setAuthorCover(final String author, final String luid) {
99206a39
NR
407 if (!check()) {
408 return;
409 }
410
3989dfc5
NR
411 setCover(author, luid, "AUTHOR");
412 }
413
414 // type = "SOURCE" | "AUTHOR"
415 private void setCover(final String value, final String luid,
416 final String type) {
99206a39
NR
417 if (!check()) {
418 return;
419 }
420
b9ce9cad 421 try {
3040c4f0 422 new ConnectActionClientObject(host, port, key) {
b9ce9cad 423 @Override
fb25273c
NR
424 public void action(Version serverVersion) throws Exception {
425 send(new Object[] { subkey, "SET_COVER", type, value, luid });
b9ce9cad
NR
426 }
427
428 @Override
429 protected void onError(Exception e) {
7e51d91c
NR
430 if (e instanceof SSLException) {
431 Instance.getTraceHandler().error(
432 "Connection refused (bad key)");
433 } else {
434 Instance.getTraceHandler().error(e);
435 }
b9ce9cad
NR
436 }
437 }.connect();
438 } catch (IOException e) {
439 Instance.getTraceHandler().error(e);
edf79e5e
NR
440 }
441 }
442
443 @Override
444 // Could work (more slowly) without it
445 public Story imprt(final URL url, Progress pg) throws IOException {
99206a39
NR
446 if (!check()) {
447 return null;
448 }
449
00f6344a
NR
450 // Import the file locally if it is actually a file
451 if (url == null || url.getProtocol().equalsIgnoreCase("file")) {
452 return super.imprt(url, pg);
453 }
454
455 // Import it remotely if it is an URL
456
edf79e5e
NR
457 if (pg == null) {
458 pg = new Progress();
459 }
460
461 pg.setMinMax(0, 2);
462 Progress pgImprt = new Progress();
463 Progress pgGet = new Progress();
464 pg.addProgress(pgImprt, 1);
465 pg.addProgress(pgGet, 1);
466
467 final Progress pgF = pgImprt;
468 final String[] luid = new String[1];
469
470 try {
3040c4f0 471 new ConnectActionClientObject(host, port, key) {
edf79e5e 472 @Override
fb25273c 473 public void action(Version serverVersion) throws Exception {
edf79e5e
NR
474 Progress pg = pgF;
475
fb25273c
NR
476 Object rep = send(new Object[] { subkey, "IMPORT",
477 url.toString() });
edf79e5e
NR
478
479 while (true) {
480 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
481 break;
482 }
483
484 rep = send(null);
485 }
486
487 pg.done();
488 luid[0] = (String) rep;
489 }
490
491 @Override
492 protected void onError(Exception e) {
7e51d91c
NR
493 if (e instanceof SSLException) {
494 Instance.getTraceHandler().error(
495 "Connection refused (bad key)");
496 } else {
497 Instance.getTraceHandler().error(e);
498 }
edf79e5e
NR
499 }
500 }.connect();
501 } catch (IOException e) {
502 Instance.getTraceHandler().error(e);
503 }
504
505 if (luid[0] == null) {
506 throw new IOException("Remote failure");
507 }
508
509 Story story = getStory(luid[0], pgGet);
510 pgGet.done();
511
512 pg.done();
513 return story;
514 }
515
516 @Override
517 // Could work (more slowly) without it
c8d48938
NR
518 protected synchronized void changeSTA(final String luid,
519 final String newSource, final String newTitle,
520 final String newAuthor, Progress pg) throws IOException {
99206a39
NR
521 if (!check()) {
522 return;
523 }
524
edf79e5e
NR
525 final Progress pgF = pg == null ? new Progress() : pg;
526
527 try {
3040c4f0 528 new ConnectActionClientObject(host, port, key) {
edf79e5e 529 @Override
fb25273c 530 public void action(Version serverVersion) throws Exception {
edf79e5e
NR
531 Progress pg = pgF;
532
fb25273c
NR
533 Object rep = send(new Object[] { subkey, "CHANGE_STA",
534 luid, newSource, newTitle, newAuthor });
edf79e5e
NR
535 while (true) {
536 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
537 break;
538 }
539
540 rep = send(null);
541 }
542 }
543
544 @Override
545 protected void onError(Exception e) {
7e51d91c
NR
546 if (e instanceof SSLException) {
547 Instance.getTraceHandler().error(
548 "Connection refused (bad key)");
549 } else {
550 Instance.getTraceHandler().error(e);
551 }
edf79e5e
NR
552 }
553 }.connect();
554 } catch (IOException e) {
555 Instance.getTraceHandler().error(e);
b9ce9cad 556 }
68e2c6d2
NR
557 }
558
ff05b828 559 @Override
2249988a 560 public synchronized File getFile(final String luid, Progress pg) {
ff05b828
NR
561 throw new java.lang.InternalError(
562 "Operation not supportorted on remote Libraries");
563 }
564
468b960b
NR
565 /**
566 * Stop the server.
567 */
568 public void exit() {
99206a39
NR
569 if (!check()) {
570 return;
571 }
572
468b960b 573 try {
3040c4f0 574 new ConnectActionClientObject(host, port, key) {
468b960b 575 @Override
fb25273c
NR
576 public void action(Version serverVersion) throws Exception {
577 send(new Object[] { subkey, "EXIT" });
468b960b
NR
578 }
579
580 @Override
581 protected void onError(Exception e) {
7e51d91c
NR
582 if (e instanceof SSLException) {
583 Instance.getTraceHandler().error(
584 "Connection refused (bad key)");
585 } else {
586 Instance.getTraceHandler().error(e);
587 }
468b960b
NR
588 }
589 }.connect();
590 } catch (IOException e) {
591 Instance.getTraceHandler().error(e);
592 }
593 }
594
e272f05f
NR
595 @Override
596 public synchronized MetaData getInfo(String luid) {
99206a39
NR
597 if (!check()) {
598 return null;
599 }
600
e272f05f
NR
601 List<MetaData> metas = getMetasList(luid, null);
602 if (!metas.isEmpty()) {
603 return metas.get(0);
604 }
605
606 return null;
607 }
608
14b57448 609 @Override
b9ce9cad 610 protected List<MetaData> getMetas(Progress pg) {
e272f05f
NR
611 return getMetasList("*", pg);
612 }
613
614 @Override
efa3c511
NR
615 protected void updateInfo(MetaData meta) {
616 // Will be taken care of directly server side
617 }
618
619 @Override
c8d48938 620 protected void invalidateInfo(String luid) {
efa3c511 621 // Will be taken care of directly server side
e272f05f
NR
622 }
623
624 // The following methods are only used by Save and Delete in BasicLibrary:
625
626 @Override
627 protected int getNextId() {
628 throw new java.lang.InternalError("Should not have been called");
629 }
630
631 @Override
632 protected void doDelete(String luid) throws IOException {
633 throw new java.lang.InternalError("Should not have been called");
634 }
635
636 @Override
637 protected Story doSave(Story story, Progress pg) throws IOException {
638 throw new java.lang.InternalError("Should not have been called");
639 }
640
641 //
642
643 /**
644 * Return the meta of the given story or a list of all known metas if the
645 * luid is "*".
9f51d8ab
NR
646 * <p>
647 * Will not get the covers.
e272f05f
NR
648 *
649 * @param luid
650 * the luid of the story or *
651 * @param pg
652 * the optional progress
653 *
654 *
655 * @return the metas
656 */
657 private List<MetaData> getMetasList(final String luid, Progress pg) {
99206a39
NR
658 if (!check()) {
659 return null;
660 }
661
b9ce9cad
NR
662 final Progress pgF = pg;
663 final List<MetaData> metas = new ArrayList<MetaData>();
74a40dfb 664
ff05b828 665 try {
3040c4f0 666 new ConnectActionClientObject(host, port, key) {
ff05b828 667 @Override
fb25273c 668 public void action(Version serverVersion) throws Exception {
b9ce9cad
NR
669 Progress pg = pgF;
670 if (pg == null) {
671 pg = new Progress();
851dd538 672 }
74a40dfb 673
fb25273c
NR
674 Object rep = send(new Object[] { subkey, "GET_METADATA",
675 luid });
74a40dfb 676
b9ce9cad
NR
677 while (true) {
678 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
679 break;
680 }
74a40dfb 681
b9ce9cad 682 rep = send(null);
ff05b828 683 }
851dd538 684
e272f05f
NR
685 if (rep instanceof MetaData[]) {
686 for (MetaData meta : (MetaData[]) rep) {
687 metas.add(meta);
688 }
689 } else if (rep != null) {
690 metas.add((MetaData) rep);
b9ce9cad 691 }
851dd538
NR
692 }
693
694 @Override
695 protected void onError(Exception e) {
7e51d91c
NR
696 if (e instanceof SSLException) {
697 Instance.getTraceHandler().error(
698 "Connection refused (bad key)");
699 } else {
700 Instance.getTraceHandler().error(e);
701 }
ff05b828
NR
702 }
703 }.connect();
ff05b828 704 } catch (Exception e) {
62c63b07 705 Instance.getTraceHandler().error(e);
ff05b828 706 }
b9ce9cad
NR
707
708 return metas;
709 }
b0e88ebd 710}