New web library (http/https)
[nikiroo-utils.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 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:
65 * <tt><b><i>xxx</i></b>(|<b><i>yyy</i></b>|<b>wl</b>)(|<b>rw</b>)</tt>
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>
80 * <li><b>wl</b>: flag to allow access to all the stories (bypassing the
81 * whitelist if it exists)</li>
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>
88 * <li><b>my_key|agzyzz|wl</b>: will ask to bypass the white list (if it
89 * exists)</li>
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
144 result[0] = Status.INVALID;
145
e6249b0f 146 try {
5db598bc 147 new RemoteConnectAction() {
e6249b0f 148 @Override
fb25273c
NR
149 public void action(Version serverVersion) throws Exception {
150 Object rep = send(new Object[] { subkey, "PING" });
ea734ab4 151
fb25273c
NR
152 if ("r/w".equals(rep)) {
153 rw = true;
0bb51c9c 154 result[0] = Status.READ_WRITE;
fb25273c
NR
155 } else if ("r/o".equals(rep)) {
156 rw = false;
0bb51c9c 157 result[0] = Status.READ_ONLY;
7e51d91c 158 } else {
99206a39 159 result[0] = Status.UNAUTHORIZED;
e6249b0f
NR
160 }
161 }
162
163 @Override
164 protected void onError(Exception e) {
210465c3
NR
165 if (e instanceof SSLException) {
166 result[0] = Status.UNAUTHORIZED;
167 } else {
168 result[0] = Status.UNAVAILABLE;
169 }
e6249b0f 170 }
ea734ab4 171 }.connect();
e6249b0f
NR
172 } catch (UnknownHostException e) {
173 result[0] = Status.INVALID;
174 } catch (IllegalArgumentException e) {
175 result[0] = Status.INVALID;
176 } catch (Exception e) {
177 result[0] = Status.UNAVAILABLE;
178 }
179
e6249b0f
NR
180 return result[0];
181 }
182
b0e88ebd 183 @Override
0bb51c9c 184 public Image getCover(final String luid) throws IOException {
16a81ef7 185 final Image[] result = new Image[1];
b0e88ebd 186
5db598bc
NR
187 connectRemoteAction(new RemoteAction() {
188 @Override
189 public void action(ConnectActionClientObject action)
190 throws Exception {
f433d153
NR
191 Object rep = action
192 .send(new Object[] { subkey, "GET_COVER", luid });
5db598bc
NR
193 result[0] = (Image) rep;
194 }
195 });
b0e88ebd 196
b9ce9cad 197 return result[0];
085a2f9a
NR
198 }
199
200 @Override
0bb51c9c 201 public Image getCustomSourceCover(final String source) throws IOException {
3989dfc5
NR
202 return getCustomCover(source, "SOURCE");
203 }
204
205 @Override
0bb51c9c 206 public Image getCustomAuthorCover(final String author) throws IOException {
3989dfc5
NR
207 return getCustomCover(author, "AUTHOR");
208 }
209
210 // type: "SOURCE" or "AUTHOR"
0bb51c9c
NR
211 private Image getCustomCover(final String source, final String type)
212 throws IOException {
16a81ef7 213 final Image[] result = new Image[1];
b9ce9cad 214
5db598bc
NR
215 connectRemoteAction(new RemoteAction() {
216 @Override
217 public void action(ConnectActionClientObject action)
218 throws Exception {
219 Object rep = action.send(new Object[] { subkey,
220 "GET_CUSTOM_COVER", type, source });
221 result[0] = (Image) rep;
222 }
223 });
b9ce9cad
NR
224
225 return result[0];
b0e88ebd 226 }
68e2c6d2
NR
227
228 @Override
0bb51c9c
NR
229 public synchronized Story getStory(final String luid, Progress pg)
230 throws IOException {
b9ce9cad
NR
231 final Progress pgF = pg;
232 final Story[] result = new Story[1];
68e2c6d2 233
5db598bc
NR
234 connectRemoteAction(new RemoteAction() {
235 @Override
236 public void action(ConnectActionClientObject action)
237 throws Exception {
238 Progress pg = pgF;
239 if (pg == null) {
240 pg = new Progress();
241 }
b9ce9cad 242
f433d153
NR
243 Object rep = action
244 .send(new Object[] { subkey, "GET_STORY", luid });
b9ce9cad 245
5db598bc
NR
246 MetaData meta = null;
247 if (rep instanceof MetaData) {
248 meta = (MetaData) rep;
249 if (meta.getWords() <= Integer.MAX_VALUE) {
250 pg.setMinMax(0, (int) meta.getWords());
b9ce9cad 251 }
b9ce9cad
NR
252 }
253
5db598bc
NR
254 List<Object> list = new ArrayList<Object>();
255 for (Object obj = action.send(null); obj != null; obj = action
256 .send(null)) {
257 list.add(obj);
258 pg.add(1);
b9ce9cad 259 }
5db598bc
NR
260
261 result[0] = RemoteLibraryServer.rebuildStory(list);
262 pg.done();
263 }
264 });
b9ce9cad
NR
265
266 return result[0];
68e2c6d2
NR
267 }
268
269 @Override
b9ce9cad
NR
270 public synchronized Story save(final Story story, final String luid,
271 Progress pg) throws IOException {
99206a39 272
0fa0fe95
NR
273 final String[] luidSaved = new String[1];
274 Progress pgSave = new Progress();
275 Progress pgRefresh = new Progress();
276 if (pg == null) {
277 pg = new Progress();
278 }
279
280 pg.setMinMax(0, 10);
281 pg.addProgress(pgSave, 9);
282 pg.addProgress(pgRefresh, 1);
283
284 final Progress pgF = pgSave;
b9ce9cad 285
5db598bc 286 connectRemoteAction(new RemoteAction() {
b9ce9cad 287 @Override
5db598bc
NR
288 public void action(ConnectActionClientObject action)
289 throws Exception {
b9ce9cad 290 Progress pg = pgF;
b9ce9cad
NR
291 if (story.getMeta().getWords() <= Integer.MAX_VALUE) {
292 pg.setMinMax(0, (int) story.getMeta().getWords());
293 }
294
5db598bc 295 action.send(new Object[] { subkey, "SAVE_STORY", luid });
b9ce9cad
NR
296
297 List<Object> list = RemoteLibraryServer.breakStory(story);
298 for (Object obj : list) {
5db598bc 299 action.send(obj);
b9ce9cad
NR
300 pg.add(1);
301 }
302
5db598bc 303 luidSaved[0] = (String) action.send(null);
0fa0fe95 304
b9ce9cad
NR
305 pg.done();
306 }
5db598bc 307 });
085a2f9a
NR
308
309 // because the meta changed:
edf79e5e 310 MetaData meta = getInfo(luidSaved[0]);
efa3c511
NR
311 if (story.getMeta().getClass() != null) {
312 // If already available locally:
313 meta.setCover(story.getMeta().getCover());
314 } else {
315 // If required:
316 meta.setCover(getCover(meta.getLuid()));
317 }
edf79e5e 318 story.setMeta(meta);
0fa0fe95
NR
319
320 pg.done();
085a2f9a
NR
321
322 return story;
68e2c6d2
NR
323 }
324
325 @Override
b9ce9cad 326 public synchronized void delete(final String luid) throws IOException {
5db598bc 327 connectRemoteAction(new RemoteAction() {
b9ce9cad 328 @Override
5db598bc
NR
329 public void action(ConnectActionClientObject action)
330 throws Exception {
331 action.send(new Object[] { subkey, "DELETE_STORY", luid });
b9ce9cad 332 }
5db598bc 333 });
68e2c6d2
NR
334 }
335
336 @Override
0bb51c9c
NR
337 public void setSourceCover(final String source, final String luid)
338 throws IOException {
3989dfc5
NR
339 setCover(source, luid, "SOURCE");
340 }
341
342 @Override
0bb51c9c
NR
343 public void setAuthorCover(final String author, final String luid)
344 throws IOException {
3989dfc5
NR
345 setCover(author, luid, "AUTHOR");
346 }
347
348 // type = "SOURCE" | "AUTHOR"
349 private void setCover(final String value, final String luid,
0bb51c9c 350 final String type) throws IOException {
5db598bc
NR
351 connectRemoteAction(new RemoteAction() {
352 @Override
353 public void action(ConnectActionClientObject action)
354 throws Exception {
355 action.send(new Object[] { subkey, "SET_COVER", type, value,
356 luid });
357 }
358 });
edf79e5e
NR
359 }
360
361 @Override
362 // Could work (more slowly) without it
b6b65795 363 public MetaData imprt(final URL url, Progress pg) throws IOException {
00f6344a 364 // Import the file locally if it is actually a file
f433d153 365
00f6344a
NR
366 if (url == null || url.getProtocol().equalsIgnoreCase("file")) {
367 return super.imprt(url, pg);
368 }
369
370 // Import it remotely if it is an URL
371
edf79e5e
NR
372 if (pg == null) {
373 pg = new Progress();
374 }
375
b6b65795 376 final Progress pgF = pg;
edf79e5e
NR
377 final String[] luid = new String[1];
378
5db598bc
NR
379 connectRemoteAction(new RemoteAction() {
380 @Override
381 public void action(ConnectActionClientObject action)
382 throws Exception {
383 Progress pg = pgF;
edf79e5e 384
f433d153
NR
385 Object rep = action.send(
386 new Object[] { subkey, "IMPORT", url.toString() });
edf79e5e 387
5db598bc
NR
388 while (true) {
389 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
390 break;
edf79e5e
NR
391 }
392
5db598bc 393 rep = action.send(null);
edf79e5e
NR
394 }
395
5db598bc
NR
396 pg.done();
397 luid[0] = (String) rep;
398 }
399 });
edf79e5e
NR
400
401 if (luid[0] == null) {
402 throw new IOException("Remote failure");
403 }
404
edf79e5e 405 pg.done();
b6b65795 406 return getInfo(luid[0]);
edf79e5e
NR
407 }
408
409 @Override
410 // Could work (more slowly) without it
c8d48938
NR
411 protected synchronized void changeSTA(final String luid,
412 final String newSource, final String newTitle,
413 final String newAuthor, Progress pg) throws IOException {
99206a39 414
edf79e5e
NR
415 final Progress pgF = pg == null ? new Progress() : pg;
416
5db598bc
NR
417 connectRemoteAction(new RemoteAction() {
418 @Override
419 public void action(ConnectActionClientObject action)
420 throws Exception {
421 Progress pg = pgF;
edf79e5e 422
5db598bc
NR
423 Object rep = action.send(new Object[] { subkey, "CHANGE_STA",
424 luid, newSource, newTitle, newAuthor });
425 while (true) {
426 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
427 break;
edf79e5e 428 }
edf79e5e 429
5db598bc 430 rep = action.send(null);
edf79e5e 431 }
5db598bc
NR
432 }
433 });
68e2c6d2
NR
434 }
435
ff05b828 436 @Override
dc919036 437 public File getFile(final String luid, Progress pg) {
ff05b828
NR
438 throw new java.lang.InternalError(
439 "Operation not supportorted on remote Libraries");
440 }
441
468b960b
NR
442 /**
443 * Stop the server.
533dc2b8
NR
444 *
445 * @throws IOException
2ef95142
NR
446 * in case of I/O errors
447 * @throws SSLException
448 * when the key was not accepted
468b960b 449 */
2ef95142 450 public void exit() throws IOException, SSLException {
5db598bc
NR
451 connectRemoteAction(new RemoteAction() {
452 @Override
453 public void action(ConnectActionClientObject action)
454 throws Exception {
455 action.send(new Object[] { subkey, "EXIT" });
159c11da 456 Thread.sleep(100);
5db598bc
NR
457 }
458 });
468b960b
NR
459 }
460
e272f05f 461 @Override
dc919036 462 public MetaData getInfo(String luid) throws IOException {
e272f05f
NR
463 List<MetaData> metas = getMetasList(luid, null);
464 if (!metas.isEmpty()) {
465 return metas.get(0);
466 }
467
468 return null;
469 }
470
14b57448 471 @Override
dc919036 472 protected List<MetaData> getMetas(Progress pg) throws IOException {
e272f05f
NR
473 return getMetasList("*", pg);
474 }
475
476 @Override
efa3c511
NR
477 protected void updateInfo(MetaData meta) {
478 // Will be taken care of directly server side
479 }
480
481 @Override
c8d48938 482 protected void invalidateInfo(String luid) {
efa3c511 483 // Will be taken care of directly server side
e272f05f
NR
484 }
485
486 // The following methods are only used by Save and Delete in BasicLibrary:
487
488 @Override
489 protected int getNextId() {
490 throw new java.lang.InternalError("Should not have been called");
491 }
492
493 @Override
494 protected void doDelete(String luid) throws IOException {
495 throw new java.lang.InternalError("Should not have been called");
496 }
497
498 @Override
499 protected Story doSave(Story story, Progress pg) throws IOException {
500 throw new java.lang.InternalError("Should not have been called");
501 }
502
503 //
504
505 /**
506 * Return the meta of the given story or a list of all known metas if the
507 * luid is "*".
9f51d8ab
NR
508 * <p>
509 * Will not get the covers.
e272f05f
NR
510 *
511 * @param luid
512 * the luid of the story or *
513 * @param pg
514 * the optional progress
515 *
e272f05f 516 * @return the metas
0bb51c9c
NR
517 *
518 * @throws IOException
519 * in case of I/O error or bad key (SSLException)
e272f05f 520 */
0bb51c9c
NR
521 private List<MetaData> getMetasList(final String luid, Progress pg)
522 throws IOException {
b9ce9cad
NR
523 final Progress pgF = pg;
524 final List<MetaData> metas = new ArrayList<MetaData>();
74a40dfb 525
5db598bc
NR
526 connectRemoteAction(new RemoteAction() {
527 @Override
528 public void action(ConnectActionClientObject action)
529 throws Exception {
530 Progress pg = pgF;
531 if (pg == null) {
532 pg = new Progress();
533 }
74a40dfb 534
f433d153
NR
535 Object rep = action
536 .send(new Object[] { subkey, "GET_METADATA", luid });
74a40dfb 537
5db598bc
NR
538 while (true) {
539 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
540 break;
ff05b828 541 }
851dd538 542
5db598bc
NR
543 rep = action.send(null);
544 }
545
546 if (rep instanceof MetaData[]) {
547 for (MetaData meta : (MetaData[]) rep) {
548 metas.add(meta);
b9ce9cad 549 }
5db598bc
NR
550 } else if (rep != null) {
551 metas.add((MetaData) rep);
552 }
553 }
554 });
555
556 return metas;
557 }
558
0bb51c9c
NR
559 private void connectRemoteAction(final RemoteAction runAction)
560 throws IOException {
561 final IOException[] err = new IOException[1];
5db598bc
NR
562 try {
563 final RemoteConnectAction[] array = new RemoteConnectAction[1];
564 RemoteConnectAction ra = new RemoteConnectAction() {
565 @Override
566 public void action(Version serverVersion) throws Exception {
567 runAction.action(array[0]);
851dd538
NR
568 }
569
570 @Override
571 protected void onError(Exception e) {
0bb51c9c 572 if (!(e instanceof IOException)) {
d66deb8d 573 Instance.getInstance().getTraceHandler().error(e);
0bb51c9c 574 return;
7e51d91c 575 }
0bb51c9c
NR
576
577 err[0] = (IOException) e;
ff05b828 578 }
5db598bc
NR
579 };
580 array[0] = ra;
581 ra.connect();
ff05b828 582 } catch (Exception e) {
0bb51c9c
NR
583 err[0] = (IOException) e;
584 }
585
586 if (err[0] != null) {
587 throw err[0];
ff05b828 588 }
b9ce9cad 589 }
b0e88ebd 590}