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