remote: do not ping all the time
[fanfix.git] / src / be / nikiroo / fanfix / 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 *
25 * @author niki
26 */
27 public class RemoteLibrary extends BasicLibrary {
28 private String host;
29 private int port;
30 private final String key;
31 private final String subkey;
32
33 // informative only (server will make the actual checks)
34 private boolean rw;
35
36 // TODO: error handling is not up to par!
37
38 /**
39 * Create a {@link RemoteLibrary} linked to the given server.
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>
72 *
73 * @param key
74 * the key that will allow us to exchange information with the
75 * server
76 * @param host
77 * the host to contact or NULL for localhost
78 * @param port
79 * the port to contact it on
80 */
81 public RemoteLibrary(String key, String host, int port) {
82 int index = -1;
83 if (key != null) {
84 index = key.indexOf('|');
85 }
86
87 if (index >= 0) {
88 this.key = key.substring(0, index);
89 this.subkey = key.substring(index + 1);
90 } else {
91 this.key = key;
92 this.subkey = "";
93 }
94
95 this.host = host;
96 this.port = port;
97 }
98
99 @Override
100 public String getLibraryName() {
101 return host + ":" + port;
102 }
103
104 @Override
105 public Status getStatus() {
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 Status getStatusDo() {
113 final Status[] result = new Status[1];
114
115 result[0] = Status.INVALID;
116
117 try {
118 new ConnectActionClientObject(host, port, key) {
119 @Override
120 public void action(Version serverVersion) throws Exception {
121 Object rep = send(new Object[] { subkey, "PING" });
122
123 if ("r/w".equals(rep)) {
124 rw = true;
125 result[0] = Status.READY;
126 } else if ("r/o".equals(rep)) {
127 rw = false;
128 result[0] = Status.READY;
129 } else {
130 result[0] = Status.UNAUTHORIZED;
131 }
132 }
133
134 @Override
135 protected void onError(Exception e) {
136 if (e instanceof SSLException) {
137 result[0] = Status.UNAUTHORIZED;
138 } else {
139 result[0] = Status.UNAVAILABLE;
140 }
141 }
142 }.connect();
143 } catch (UnknownHostException e) {
144 result[0] = Status.INVALID;
145 } catch (IllegalArgumentException e) {
146 result[0] = Status.INVALID;
147 } catch (Exception e) {
148 result[0] = Status.UNAVAILABLE;
149 }
150
151 return result[0];
152 }
153
154 @Override
155 public Image getCover(final String luid) {
156 final Image[] result = new Image[1];
157
158 try {
159 new ConnectActionClientObject(host, port, key) {
160 @Override
161 public void action(Version serverVersion) throws Exception {
162 Object rep = send(new Object[] { subkey, "GET_COVER", luid });
163 result[0] = (Image) rep;
164 }
165
166 @Override
167 protected void onError(Exception e) {
168 if (e instanceof SSLException) {
169 Instance.getTraceHandler().error(
170 "Connection refused (bad key)");
171 } else {
172 Instance.getTraceHandler().error(e);
173 }
174 }
175 }.connect();
176 } catch (Exception e) {
177 Instance.getTraceHandler().error(e);
178 }
179
180 return result[0];
181 }
182
183 @Override
184 public Image getCustomSourceCover(final String source) {
185 return getCustomCover(source, "SOURCE");
186 }
187
188 @Override
189 public Image getCustomAuthorCover(final String author) {
190 return getCustomCover(author, "AUTHOR");
191 }
192
193 // type: "SOURCE" or "AUTHOR"
194 private Image getCustomCover(final String source, final String type) {
195 final Image[] result = new Image[1];
196
197 try {
198 new ConnectActionClientObject(host, port, key) {
199 @Override
200 public void action(Version serverVersion) throws Exception {
201 Object rep = send(new Object[] { subkey,
202 "GET_CUSTOM_COVER", type, source });
203 result[0] = (Image) rep;
204 }
205
206 @Override
207 protected void onError(Exception e) {
208 if (e instanceof SSLException) {
209 Instance.getTraceHandler().error(
210 "Connection refused (bad key)");
211 } else {
212 Instance.getTraceHandler().error(e);
213 }
214 }
215 }.connect();
216 } catch (Exception e) {
217 Instance.getTraceHandler().error(e);
218 }
219
220 return result[0];
221 }
222
223 @Override
224 public synchronized Story getStory(final String luid, Progress pg) {
225 final Progress pgF = pg;
226 final Story[] result = new Story[1];
227
228 try {
229 new ConnectActionClientObject(host, port, key) {
230 @Override
231 public void action(Version serverVersion) throws Exception {
232 Progress pg = pgF;
233 if (pg == null) {
234 pg = new Progress();
235 }
236
237 Object rep = send(new Object[] { subkey, "GET_STORY", luid });
238
239 MetaData meta = null;
240 if (rep instanceof MetaData) {
241 meta = (MetaData) rep;
242 if (meta.getWords() <= Integer.MAX_VALUE) {
243 pg.setMinMax(0, (int) meta.getWords());
244 }
245 }
246
247 List<Object> list = new ArrayList<Object>();
248 for (Object obj = send(null); obj != null; obj = send(null)) {
249 list.add(obj);
250 pg.add(1);
251 }
252
253 result[0] = RemoteLibraryServer.rebuildStory(list);
254 pg.done();
255 }
256
257 @Override
258 protected void onError(Exception e) {
259 if (e instanceof SSLException) {
260 Instance.getTraceHandler().error(
261 "Connection refused (bad key)");
262 } else {
263 Instance.getTraceHandler().error(e);
264 }
265 }
266 }.connect();
267 } catch (Exception e) {
268 Instance.getTraceHandler().error(e);
269 }
270
271 return result[0];
272 }
273
274 @Override
275 public synchronized Story save(final Story story, final String luid,
276 Progress pg) throws IOException {
277
278 final String[] luidSaved = new String[1];
279 Progress pgSave = new Progress();
280 Progress pgRefresh = new Progress();
281 if (pg == null) {
282 pg = new Progress();
283 }
284
285 pg.setMinMax(0, 10);
286 pg.addProgress(pgSave, 9);
287 pg.addProgress(pgRefresh, 1);
288
289 final Progress pgF = pgSave;
290
291 new ConnectActionClientObject(host, port, key) {
292 @Override
293 public void action(Version serverVersion) throws Exception {
294 Progress pg = pgF;
295 if (story.getMeta().getWords() <= Integer.MAX_VALUE) {
296 pg.setMinMax(0, (int) story.getMeta().getWords());
297 }
298
299 send(new Object[] { subkey, "SAVE_STORY", luid });
300
301 List<Object> list = RemoteLibraryServer.breakStory(story);
302 for (Object obj : list) {
303 send(obj);
304 pg.add(1);
305 }
306
307 luidSaved[0] = (String) send(null);
308
309 pg.done();
310 }
311
312 @Override
313 protected void onError(Exception e) {
314 if (e instanceof SSLException) {
315 Instance.getTraceHandler().error(
316 "Connection refused (bad key)");
317 } else {
318 Instance.getTraceHandler().error(e);
319 }
320 }
321 }.connect();
322
323 // because the meta changed:
324 MetaData meta = getInfo(luidSaved[0]);
325 if (story.getMeta().getClass() != null) {
326 // If already available locally:
327 meta.setCover(story.getMeta().getCover());
328 } else {
329 // If required:
330 meta.setCover(getCover(meta.getLuid()));
331 }
332 story.setMeta(meta);
333
334 pg.done();
335
336 return story;
337 }
338
339 @Override
340 public synchronized void delete(final String luid) throws IOException {
341 new ConnectActionClientObject(host, port, key) {
342 @Override
343 public void action(Version serverVersion) throws Exception {
344 send(new Object[] { subkey, "DELETE_STORY", luid });
345 }
346
347 @Override
348 protected void onError(Exception e) {
349 if (e instanceof SSLException) {
350 Instance.getTraceHandler().error(
351 "Connection refused (bad key)");
352 } else {
353 Instance.getTraceHandler().error(e);
354 }
355 }
356 }.connect();
357 }
358
359 @Override
360 public void setSourceCover(final String source, final String luid) {
361 setCover(source, luid, "SOURCE");
362 }
363
364 @Override
365 public void setAuthorCover(final String author, final String luid) {
366 setCover(author, luid, "AUTHOR");
367 }
368
369 // type = "SOURCE" | "AUTHOR"
370 private void setCover(final String value, final String luid,
371 final String type) {
372 try {
373 new ConnectActionClientObject(host, port, key) {
374 @Override
375 public void action(Version serverVersion) throws Exception {
376 send(new Object[] { subkey, "SET_COVER", type, value, luid });
377 }
378
379 @Override
380 protected void onError(Exception e) {
381 if (e instanceof SSLException) {
382 Instance.getTraceHandler().error(
383 "Connection refused (bad key)");
384 } else {
385 Instance.getTraceHandler().error(e);
386 }
387 }
388 }.connect();
389 } catch (IOException e) {
390 Instance.getTraceHandler().error(e);
391 }
392 }
393
394 @Override
395 // Could work (more slowly) without it
396 public Story imprt(final URL url, Progress pg) throws IOException {
397 // Import the file locally if it is actually a file
398 if (url == null || url.getProtocol().equalsIgnoreCase("file")) {
399 return super.imprt(url, pg);
400 }
401
402 // Import it remotely if it is an URL
403
404 if (pg == null) {
405 pg = new Progress();
406 }
407
408 pg.setMinMax(0, 2);
409 Progress pgImprt = new Progress();
410 Progress pgGet = new Progress();
411 pg.addProgress(pgImprt, 1);
412 pg.addProgress(pgGet, 1);
413
414 final Progress pgF = pgImprt;
415 final String[] luid = new String[1];
416
417 try {
418 new ConnectActionClientObject(host, port, key) {
419 @Override
420 public void action(Version serverVersion) throws Exception {
421 Progress pg = pgF;
422
423 Object rep = send(new Object[] { subkey, "IMPORT",
424 url.toString() });
425
426 while (true) {
427 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
428 break;
429 }
430
431 rep = send(null);
432 }
433
434 pg.done();
435 luid[0] = (String) rep;
436 }
437
438 @Override
439 protected void onError(Exception e) {
440 if (e instanceof SSLException) {
441 Instance.getTraceHandler().error(
442 "Connection refused (bad key)");
443 } else {
444 Instance.getTraceHandler().error(e);
445 }
446 }
447 }.connect();
448 } catch (IOException e) {
449 Instance.getTraceHandler().error(e);
450 }
451
452 if (luid[0] == null) {
453 throw new IOException("Remote failure");
454 }
455
456 Story story = getStory(luid[0], pgGet);
457 pgGet.done();
458
459 pg.done();
460 return story;
461 }
462
463 @Override
464 // Could work (more slowly) without it
465 protected synchronized void changeSTA(final String luid,
466 final String newSource, final String newTitle,
467 final String newAuthor, Progress pg) throws IOException {
468
469 final Progress pgF = pg == null ? new Progress() : pg;
470
471 try {
472 new ConnectActionClientObject(host, port, key) {
473 @Override
474 public void action(Version serverVersion) throws Exception {
475 Progress pg = pgF;
476
477 Object rep = send(new Object[] { subkey, "CHANGE_STA",
478 luid, newSource, newTitle, newAuthor });
479 while (true) {
480 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
481 break;
482 }
483
484 rep = send(null);
485 }
486 }
487
488 @Override
489 protected void onError(Exception e) {
490 if (e instanceof SSLException) {
491 Instance.getTraceHandler().error(
492 "Connection refused (bad key)");
493 } else {
494 Instance.getTraceHandler().error(e);
495 }
496 }
497 }.connect();
498 } catch (IOException e) {
499 Instance.getTraceHandler().error(e);
500 }
501 }
502
503 @Override
504 public synchronized File getFile(final String luid, Progress pg) {
505 throw new java.lang.InternalError(
506 "Operation not supportorted on remote Libraries");
507 }
508
509 /**
510 * Stop the server.
511 */
512 public void exit() {
513 try {
514 new ConnectActionClientObject(host, port, key) {
515 @Override
516 public void action(Version serverVersion) throws Exception {
517 send(new Object[] { subkey, "EXIT" });
518 }
519
520 @Override
521 protected void onError(Exception e) {
522 if (e instanceof SSLException) {
523 Instance.getTraceHandler().error(
524 "Connection refused (bad key)");
525 } else {
526 Instance.getTraceHandler().error(e);
527 }
528 }
529 }.connect();
530 } catch (IOException e) {
531 Instance.getTraceHandler().error(e);
532 }
533 }
534
535 @Override
536 public synchronized MetaData getInfo(String luid) {
537 List<MetaData> metas = getMetasList(luid, null);
538 if (!metas.isEmpty()) {
539 return metas.get(0);
540 }
541
542 return null;
543 }
544
545 @Override
546 protected List<MetaData> getMetas(Progress pg) {
547 return getMetasList("*", pg);
548 }
549
550 @Override
551 protected void updateInfo(MetaData meta) {
552 // Will be taken care of directly server side
553 }
554
555 @Override
556 protected void invalidateInfo(String luid) {
557 // Will be taken care of directly server side
558 }
559
560 // The following methods are only used by Save and Delete in BasicLibrary:
561
562 @Override
563 protected int getNextId() {
564 throw new java.lang.InternalError("Should not have been called");
565 }
566
567 @Override
568 protected void doDelete(String luid) throws IOException {
569 throw new java.lang.InternalError("Should not have been called");
570 }
571
572 @Override
573 protected Story doSave(Story story, Progress pg) throws IOException {
574 throw new java.lang.InternalError("Should not have been called");
575 }
576
577 //
578
579 /**
580 * Return the meta of the given story or a list of all known metas if the
581 * luid is "*".
582 * <p>
583 * Will not get the covers.
584 *
585 * @param luid
586 * the luid of the story or *
587 * @param pg
588 * the optional progress
589 *
590 *
591 * @return the metas
592 */
593 private List<MetaData> getMetasList(final String luid, Progress pg) {
594 final Progress pgF = pg;
595 final List<MetaData> metas = new ArrayList<MetaData>();
596
597 try {
598 new ConnectActionClientObject(host, port, key) {
599 @Override
600 public void action(Version serverVersion) throws Exception {
601 Progress pg = pgF;
602 if (pg == null) {
603 pg = new Progress();
604 }
605
606 Object rep = send(new Object[] { subkey, "GET_METADATA",
607 luid });
608
609 while (true) {
610 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
611 break;
612 }
613
614 rep = send(null);
615 }
616
617 if (rep instanceof MetaData[]) {
618 for (MetaData meta : (MetaData[]) rep) {
619 metas.add(meta);
620 }
621 } else if (rep != null) {
622 metas.add((MetaData) rep);
623 }
624 }
625
626 @Override
627 protected void onError(Exception e) {
628 if (e instanceof SSLException) {
629 Instance.getTraceHandler().error(
630 "Connection refused (bad key)");
631 } else {
632 Instance.getTraceHandler().error(e);
633 }
634 }
635 }.connect();
636 } catch (Exception e) {
637 Instance.getTraceHandler().error(e);
638 }
639
640 return metas;
641 }
642 }