manage remote and io exception in fanfix
[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 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;
44 IOException cause = remoteEx.getCause();
45 if (cause == null) {
46 cause = new IOException("IOException");
47 }
48
49 throw cause;
50 }
51
52 return rep;
53 }
54 }
55
56 private String host;
57 private int port;
58 private final String key;
59 private final String subkey;
60
61 // informative only (server will make the actual checks)
62 private boolean rw;
63
64 /**
65 * Create a {@link RemoteLibrary} linked to the given server.
66 * <p>
67 * Note that the key is structured:
68 * <tt><b><i>xxx</i></b>(|<b><i>yyy</i></b>|<b>wl</b>)(|<b>rw</b>)</tt>
69 * <p>
70 * Note that anything before the first pipe (<tt>|</tt>) character is
71 * considered to be the encryption key, anything after that character is
72 * called the subkey (including the other pipe characters and flags!).
73 * <p>
74 * This is important because the subkey (including the pipe characters and
75 * flags) must be present as-is in the server configuration file to be
76 * allowed.
77 * <ul>
78 * <li><b><i>xxx</i></b>: the encryption key used to communicate with the
79 * server</li>
80 * <li><b><i>yyy</i></b>: the secondary key</li>
81 * <li><b>rw</b>: flag to allow read and write access if it is not the
82 * default on this server</li>
83 * <li><b>wl</b>: flag to allow access to all the stories (bypassing the
84 * whitelist if it exists)</li>
85 * </ul>
86 *
87 * Some examples:
88 * <ul>
89 * <li><b>my_key</b>: normal connection, will take the default server
90 * options</li>
91 * <li><b>my_key|agzyzz|wl</b>: will ask to bypass the white list (if it
92 * exists)</li>
93 * <li><b>my_key|agzyzz|rw</b>: will ask read-write access (if the default
94 * is read-only)</li>
95 * <li><b>my_key|agzyzz|wl|rw</b>: will ask both read-write access and white
96 * list bypass</li>
97 * </ul>
98 *
99 * @param key
100 * the key that will allow us to exchange information with the
101 * server
102 * @param host
103 * the host to contact or NULL for localhost
104 * @param port
105 * the port to contact it on
106 */
107 public RemoteLibrary(String key, String host, int port) {
108 int index = -1;
109 if (key != null) {
110 index = key.indexOf('|');
111 }
112
113 if (index >= 0) {
114 this.key = key.substring(0, index);
115 this.subkey = key.substring(index + 1);
116 } else {
117 this.key = key;
118 this.subkey = "";
119 }
120
121 this.host = host;
122 this.port = port;
123 }
124
125 @Override
126 public String getLibraryName() {
127 return (rw ? "[READ-ONLY] " : "") + host + ":" + port;
128 }
129
130 @Override
131 public Status getStatus() {
132 Instance.getTraceHandler().trace("Getting remote lib status...");
133 Status status = getStatusDo();
134 Instance.getTraceHandler().trace("Remote lib status: " + status);
135 return status;
136 }
137
138 private Status getStatusDo() {
139 final Status[] result = new Status[1];
140
141 result[0] = Status.INVALID;
142
143 try {
144 new RemoteConnectAction() {
145 @Override
146 public void action(Version serverVersion) throws Exception {
147 Object rep = send(new Object[] { subkey, "PING" });
148
149 if ("r/w".equals(rep)) {
150 rw = true;
151 result[0] = Status.READ_WRITE;
152 } else if ("r/o".equals(rep)) {
153 rw = false;
154 result[0] = Status.READ_ONLY;
155 } else {
156 result[0] = Status.UNAUTHORIZED;
157 }
158 }
159
160 @Override
161 protected void onError(Exception e) {
162 if (e instanceof SSLException) {
163 result[0] = Status.UNAUTHORIZED;
164 } else {
165 result[0] = Status.UNAVAILABLE;
166 }
167 }
168 }.connect();
169 } catch (UnknownHostException e) {
170 result[0] = Status.INVALID;
171 } catch (IllegalArgumentException e) {
172 result[0] = Status.INVALID;
173 } catch (Exception e) {
174 result[0] = Status.UNAVAILABLE;
175 }
176
177 return result[0];
178 }
179
180 @Override
181 public Image getCover(final String luid) throws IOException {
182 final Image[] result = new Image[1];
183
184 connectRemoteAction(new RemoteAction() {
185 @Override
186 public void action(ConnectActionClientObject action)
187 throws Exception {
188 Object rep = action.send(new Object[] { subkey, "GET_COVER",
189 luid });
190 result[0] = (Image) rep;
191 }
192 });
193
194 return result[0];
195 }
196
197 @Override
198 public Image getCustomSourceCover(final String source) throws IOException {
199 return getCustomCover(source, "SOURCE");
200 }
201
202 @Override
203 public Image getCustomAuthorCover(final String author) throws IOException {
204 return getCustomCover(author, "AUTHOR");
205 }
206
207 // type: "SOURCE" or "AUTHOR"
208 private Image getCustomCover(final String source, final String type)
209 throws IOException {
210 final Image[] result = new Image[1];
211
212 connectRemoteAction(new RemoteAction() {
213 @Override
214 public void action(ConnectActionClientObject action)
215 throws Exception {
216 Object rep = action.send(new Object[] { subkey,
217 "GET_CUSTOM_COVER", type, source });
218 result[0] = (Image) rep;
219 }
220 });
221
222 return result[0];
223 }
224
225 @Override
226 public synchronized Story getStory(final String luid, Progress pg)
227 throws IOException {
228 final Progress pgF = pg;
229 final Story[] result = new Story[1];
230
231 connectRemoteAction(new RemoteAction() {
232 @Override
233 public void action(ConnectActionClientObject action)
234 throws Exception {
235 Progress pg = pgF;
236 if (pg == null) {
237 pg = new Progress();
238 }
239
240 Object rep = action.send(new Object[] { subkey, "GET_STORY",
241 luid });
242
243 MetaData meta = null;
244 if (rep instanceof MetaData) {
245 meta = (MetaData) rep;
246 if (meta.getWords() <= Integer.MAX_VALUE) {
247 pg.setMinMax(0, (int) meta.getWords());
248 }
249 }
250
251 List<Object> list = new ArrayList<Object>();
252 for (Object obj = action.send(null); obj != null; obj = action
253 .send(null)) {
254 list.add(obj);
255 pg.add(1);
256 }
257
258 result[0] = RemoteLibraryServer.rebuildStory(list);
259 pg.done();
260 }
261 });
262
263 return result[0];
264 }
265
266 @Override
267 public synchronized Story save(final Story story, final String luid,
268 Progress pg) throws IOException {
269
270 final String[] luidSaved = new String[1];
271 Progress pgSave = new Progress();
272 Progress pgRefresh = new Progress();
273 if (pg == null) {
274 pg = new Progress();
275 }
276
277 pg.setMinMax(0, 10);
278 pg.addProgress(pgSave, 9);
279 pg.addProgress(pgRefresh, 1);
280
281 final Progress pgF = pgSave;
282
283 connectRemoteAction(new RemoteAction() {
284 @Override
285 public void action(ConnectActionClientObject action)
286 throws Exception {
287 Progress pg = pgF;
288 if (story.getMeta().getWords() <= Integer.MAX_VALUE) {
289 pg.setMinMax(0, (int) story.getMeta().getWords());
290 }
291
292 action.send(new Object[] { subkey, "SAVE_STORY", luid });
293
294 List<Object> list = RemoteLibraryServer.breakStory(story);
295 for (Object obj : list) {
296 action.send(obj);
297 pg.add(1);
298 }
299
300 luidSaved[0] = (String) action.send(null);
301
302 pg.done();
303 }
304 });
305
306 // because the meta changed:
307 MetaData meta = getInfo(luidSaved[0]);
308 if (story.getMeta().getClass() != null) {
309 // If already available locally:
310 meta.setCover(story.getMeta().getCover());
311 } else {
312 // If required:
313 meta.setCover(getCover(meta.getLuid()));
314 }
315 story.setMeta(meta);
316
317 pg.done();
318
319 return story;
320 }
321
322 @Override
323 public synchronized void delete(final String luid) throws IOException {
324 connectRemoteAction(new RemoteAction() {
325 @Override
326 public void action(ConnectActionClientObject action)
327 throws Exception {
328 action.send(new Object[] { subkey, "DELETE_STORY", luid });
329 }
330 });
331 }
332
333 @Override
334 public void setSourceCover(final String source, final String luid)
335 throws IOException {
336 setCover(source, luid, "SOURCE");
337 }
338
339 @Override
340 public void setAuthorCover(final String author, final String luid)
341 throws IOException {
342 setCover(author, luid, "AUTHOR");
343 }
344
345 // type = "SOURCE" | "AUTHOR"
346 private void setCover(final String value, final String luid,
347 final String type) throws IOException {
348 connectRemoteAction(new RemoteAction() {
349 @Override
350 public void action(ConnectActionClientObject action)
351 throws Exception {
352 action.send(new Object[] { subkey, "SET_COVER", type, value,
353 luid });
354 }
355 });
356 }
357
358 @Override
359 // Could work (more slowly) without it
360 public Story imprt(final URL url, Progress pg) throws IOException {
361 // Import the file locally if it is actually a file
362 if (url == null || url.getProtocol().equalsIgnoreCase("file")) {
363 return super.imprt(url, pg);
364 }
365
366 // Import it remotely if it is an URL
367
368 if (pg == null) {
369 pg = new Progress();
370 }
371
372 pg.setMinMax(0, 2);
373 Progress pgImprt = new Progress();
374 Progress pgGet = new Progress();
375 pg.addProgress(pgImprt, 1);
376 pg.addProgress(pgGet, 1);
377
378 final Progress pgF = pgImprt;
379 final String[] luid = new String[1];
380
381 connectRemoteAction(new RemoteAction() {
382 @Override
383 public void action(ConnectActionClientObject action)
384 throws Exception {
385 Progress pg = pgF;
386
387 Object rep = action.send(new Object[] { subkey, "IMPORT",
388 url.toString() });
389
390 while (true) {
391 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
392 break;
393 }
394
395 rep = action.send(null);
396 }
397
398 pg.done();
399 luid[0] = (String) rep;
400 }
401 });
402
403 if (luid[0] == null) {
404 throw new IOException("Remote failure");
405 }
406
407 Story story = getStory(luid[0], pgGet);
408 pgGet.done();
409
410 pg.done();
411 return story;
412 }
413
414 @Override
415 // Could work (more slowly) without it
416 protected synchronized void changeSTA(final String luid,
417 final String newSource, final String newTitle,
418 final String newAuthor, Progress pg) throws IOException {
419
420 final Progress pgF = pg == null ? new Progress() : pg;
421
422 connectRemoteAction(new RemoteAction() {
423 @Override
424 public void action(ConnectActionClientObject action)
425 throws Exception {
426 Progress pg = pgF;
427
428 Object rep = action.send(new Object[] { subkey, "CHANGE_STA",
429 luid, newSource, newTitle, newAuthor });
430 while (true) {
431 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
432 break;
433 }
434
435 rep = action.send(null);
436 }
437 }
438 });
439 }
440
441 @Override
442 public synchronized File getFile(final String luid, Progress pg) {
443 throw new java.lang.InternalError(
444 "Operation not supportorted on remote Libraries");
445 }
446
447 /**
448 * Stop the server.
449 */
450 public void exit() throws IOException {
451 connectRemoteAction(new RemoteAction() {
452 @Override
453 public void action(ConnectActionClientObject action)
454 throws Exception {
455 action.send(new Object[] { subkey, "EXIT" });
456 }
457 });
458 }
459
460 @Override
461 public synchronized 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 int 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.send(new Object[] { subkey, "GET_METADATA",
535 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.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 }