Merge branch 'master' into subtree
[nikiroo-utils.git] / library / RemoteLibrary.java
1 package be.nikiroo.fanfix.library;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.net.URL;
6 import java.net.UnknownHostException;
7 import java.util.ArrayList;
8 import java.util.List;
9
10 import javax.net.ssl.SSLException;
11
12 import be.nikiroo.fanfix.Instance;
13 import be.nikiroo.fanfix.data.MetaData;
14 import be.nikiroo.fanfix.data.Story;
15 import be.nikiroo.utils.Image;
16 import be.nikiroo.utils.Progress;
17 import be.nikiroo.utils.Version;
18 import be.nikiroo.utils.serial.server.ConnectActionClientObject;
19
20 /**
21 * This {@link BasicLibrary} will access a remote server to list the available
22 * stories, and download the ones you try to load to the local directory
23 * specified in the configuration.
24 * <p>
25 * This remote library uses a custom fanfix:// protocol.
26 *
27 * @author niki
28 */
29 public class RemoteLibrary extends BasicLibrary {
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
40 public Object send(Object data)
41 throws IOException, NoSuchFieldException, NoSuchMethodException,
42 ClassNotFoundException {
43 Object rep = super.send(data);
44 if (rep instanceof RemoteLibraryException) {
45 RemoteLibraryException remoteEx = (RemoteLibraryException) rep;
46 throw remoteEx.unwrapException();
47 }
48
49 return rep;
50 }
51 }
52
53 private String host;
54 private int port;
55 private final String key;
56 private final String subkey;
57
58 // informative only (server will make the actual checks)
59 private boolean rw;
60
61 /**
62 * Create a {@link RemoteLibrary} linked to the given server.
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>bl</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>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>
82 * </ul>
83 * <p>
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|bl</b>: will ask to bypass the black list and the
89 * white list (if it 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>
95 *
96 * @param key
97 * the key that will allow us to exchange information with the
98 * server
99 * @param host
100 * the host to contact or NULL for localhost
101 * @param port
102 * the port to contact it on
103 */
104 public RemoteLibrary(String key, String host, int port) {
105 int index = -1;
106 if (key != null) {
107 index = key.indexOf('|');
108 }
109
110 if (index >= 0) {
111 this.key = key.substring(0, index);
112 this.subkey = key.substring(index + 1);
113 } else {
114 this.key = key;
115 this.subkey = "";
116 }
117
118 if (host.startsWith("fanfix://")) {
119 host = host.substring("fanfix://".length());
120 }
121
122 this.host = host;
123 this.port = port;
124 }
125
126 @Override
127 public String getLibraryName() {
128 return (rw ? "[READ-ONLY] " : "") + "fanfix://" + host + ":" + port;
129 }
130
131 @Override
132 public Status getStatus() {
133 Instance.getInstance().getTraceHandler()
134 .trace("Getting remote lib status...");
135 Status status = getStatusDo();
136 Instance.getInstance().getTraceHandler()
137 .trace("Remote lib status: " + status);
138 return status;
139 }
140
141 private Status getStatusDo() {
142 final Status[] result = new Status[1];
143
144 result[0] = null;
145 try {
146 new RemoteConnectAction() {
147 @Override
148 public void action(Version serverVersion) throws Exception {
149 Object rep = send(new Object[] { subkey, "PING" });
150
151 if ("r/w".equals(rep)) {
152 rw = true;
153 result[0] = Status.READ_WRITE;
154 } else if ("r/o".equals(rep)) {
155 rw = false;
156 result[0] = Status.READ_ONLY;
157 } else {
158 result[0] = Status.UNAUTHORIZED;
159 }
160 }
161
162 @Override
163 protected void onError(Exception e) {
164 if (e instanceof SSLException) {
165 result[0] = Status.UNAUTHORIZED;
166 } else {
167 result[0] = Status.UNAVAILABLE;
168 }
169 }
170 }.connect();
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
179 return result[0];
180 }
181
182 @Override
183 public Image getCover(final String luid) throws IOException {
184 final Image[] result = new Image[1];
185
186 connectRemoteAction(new RemoteAction() {
187 @Override
188 public void action(ConnectActionClientObject action)
189 throws Exception {
190 Object rep = action
191 .send(new Object[] { subkey, "GET_COVER", luid });
192 result[0] = (Image) rep;
193 }
194 });
195
196 return result[0];
197 }
198
199 @Override
200 public Image getCustomSourceCover(final String source) throws IOException {
201 return getCustomCover(source, "SOURCE");
202 }
203
204 @Override
205 public Image getCustomAuthorCover(final String author) throws IOException {
206 return getCustomCover(author, "AUTHOR");
207 }
208
209 // type: "SOURCE" or "AUTHOR"
210 private Image getCustomCover(final String source, final String type)
211 throws IOException {
212 final Image[] result = new Image[1];
213
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 });
223
224 return result[0];
225 }
226
227 @Override
228 public synchronized Story getStory(final String luid, Progress pg)
229 throws IOException {
230 final Progress pgF = pg;
231 final Story[] result = new Story[1];
232
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 }
241
242 Object rep = action
243 .send(new Object[] { subkey, "GET_STORY", luid });
244
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());
250 }
251 }
252
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);
258 }
259
260 result[0] = RemoteLibraryServer.rebuildStory(list);
261 pg.done();
262 }
263 });
264
265 return result[0];
266 }
267
268 @Override
269 public synchronized Story save(final Story story, final String luid,
270 Progress pg) throws IOException {
271
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;
284
285 connectRemoteAction(new RemoteAction() {
286 @Override
287 public void action(ConnectActionClientObject action)
288 throws Exception {
289 Progress pg = pgF;
290 if (story.getMeta().getWords() <= Integer.MAX_VALUE) {
291 pg.setMinMax(0, (int) story.getMeta().getWords());
292 }
293
294 action.send(new Object[] { subkey, "SAVE_STORY", luid });
295
296 List<Object> list = RemoteLibraryServer.breakStory(story);
297 for (Object obj : list) {
298 action.send(obj);
299 pg.add(1);
300 }
301
302 luidSaved[0] = (String) action.send(null);
303
304 pg.done();
305 }
306 });
307
308 // because the meta changed:
309 MetaData meta = getInfo(luidSaved[0]);
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 }
317 story.setMeta(meta);
318
319 pg.done();
320
321 return story;
322 }
323
324 @Override
325 public synchronized void delete(final String luid) throws IOException {
326 connectRemoteAction(new RemoteAction() {
327 @Override
328 public void action(ConnectActionClientObject action)
329 throws Exception {
330 action.send(new Object[] { subkey, "DELETE_STORY", luid });
331 }
332 });
333 }
334
335 @Override
336 public void setSourceCover(final String source, final String luid)
337 throws IOException {
338 setCover(source, luid, "SOURCE");
339 }
340
341 @Override
342 public void setAuthorCover(final String author, final String luid)
343 throws IOException {
344 setCover(author, luid, "AUTHOR");
345 }
346
347 // type = "SOURCE" | "AUTHOR"
348 private void setCover(final String value, final String luid,
349 final String type) throws IOException {
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 });
358 }
359
360 @Override
361 // Could work (more slowly) without it
362 public MetaData imprt(final URL url, Progress pg) throws IOException {
363 // Import the file locally if it is actually a file
364
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
371 if (pg == null) {
372 pg = new Progress();
373 }
374
375 final Progress pgF = pg;
376 final String[] luid = new String[1];
377
378 connectRemoteAction(new RemoteAction() {
379 @Override
380 public void action(ConnectActionClientObject action)
381 throws Exception {
382 Progress pg = pgF;
383
384 Object rep = action.send(
385 new Object[] { subkey, "IMPORT", url.toString() });
386
387 while (true) {
388 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
389 break;
390 }
391
392 rep = action.send(null);
393 }
394
395 pg.done();
396 luid[0] = (String) rep;
397 }
398 });
399
400 if (luid[0] == null) {
401 throw new IOException("Remote failure");
402 }
403
404 pg.done();
405 return getInfo(luid[0]);
406 }
407
408 @Override
409 // Could work (more slowly) without it
410 protected synchronized void changeSTA(final String luid,
411 final String newSource, final String newTitle,
412 final String newAuthor, Progress pg) throws IOException {
413
414 final Progress pgF = pg == null ? new Progress() : pg;
415
416 connectRemoteAction(new RemoteAction() {
417 @Override
418 public void action(ConnectActionClientObject action)
419 throws Exception {
420 Progress pg = pgF;
421
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;
427 }
428
429 rep = action.send(null);
430 }
431 }
432 });
433 }
434
435 @Override
436 public File getFile(final String luid, Progress pg) {
437 throw new java.lang.InternalError(
438 "Operation not supportorted on remote Libraries");
439 }
440
441 /**
442 * Stop the server.
443 *
444 * @throws IOException
445 * in case of I/O errors
446 * @throws SSLException
447 * when the key was not accepted
448 */
449 public void stop() throws IOException, SSLException {
450 connectRemoteAction(new RemoteAction() {
451 @Override
452 public void action(ConnectActionClientObject action)
453 throws Exception {
454 action.send(new Object[] { subkey, "EXIT" });
455 Thread.sleep(100);
456 }
457 });
458 }
459
460 @Override
461 public MetaData getInfo(String luid) throws IOException {
462 List<MetaData> metas = getMetasList(luid, null);
463 if (!metas.isEmpty()) {
464 return metas.get(0);
465 }
466
467 return null;
468 }
469
470 @Override
471 protected List<MetaData> getMetas(Progress pg) throws IOException {
472 return getMetasList("*", pg);
473 }
474
475 @Override
476 protected void updateInfo(MetaData meta) {
477 // Will be taken care of directly server side
478 }
479
480 @Override
481 protected void invalidateInfo(String luid) {
482 // Will be taken care of directly server side
483 }
484
485 // The following methods are only used by Save and Delete in BasicLibrary:
486
487 @Override
488 protected String getNextId() {
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 "*".
507 * <p>
508 * Will not get the covers.
509 *
510 * @param luid
511 * the luid of the story or *
512 * @param pg
513 * the optional progress
514 *
515 * @return the metas
516 *
517 * @throws IOException
518 * in case of I/O error or bad key (SSLException)
519 */
520 private List<MetaData> getMetasList(final String luid, Progress pg)
521 throws IOException {
522 final Progress pgF = pg;
523 final List<MetaData> metas = new ArrayList<MetaData>();
524
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 }
533
534 Object rep = action
535 .send(new Object[] { subkey, "GET_METADATA", luid });
536
537 while (true) {
538 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
539 break;
540 }
541
542 rep = action.send(null);
543 }
544
545 if (rep instanceof MetaData[]) {
546 for (MetaData meta : (MetaData[]) rep) {
547 metas.add(meta);
548 }
549 } else if (rep != null) {
550 metas.add((MetaData) rep);
551 }
552 }
553 });
554
555 return metas;
556 }
557
558 private void connectRemoteAction(final RemoteAction runAction)
559 throws IOException {
560 final IOException[] err = new IOException[1];
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]);
567 }
568
569 @Override
570 protected void onError(Exception e) {
571 if (!(e instanceof IOException)) {
572 Instance.getInstance().getTraceHandler().error(e);
573 return;
574 }
575
576 err[0] = (IOException) e;
577 }
578 };
579 array[0] = ra;
580 ra.connect();
581 } catch (Exception e) {
582 err[0] = (IOException) e;
583 }
584
585 if (err[0] != null) {
586 throw err[0];
587 }
588 }
589 }