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