077a857cf9a748ddbd242b1af625b853ecf6d75e
[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 // TODO: error handling is not up to par!
65
66 /**
67 * Create a {@link RemoteLibrary} linked to the given server.
68 * <p>
69 * Note that the key is structured:
70 * <tt><b><i>xxx</i></b>(|<b><i>yyy</i></b>|<b>wl</b>)(|<b>rw</b>)</tt>
71 * <p>
72 * Note that anything before the first pipe (<tt>|</tt>) character is
73 * considered to be the encryption key, anything after that character is
74 * called the subkey (including the other pipe characters and flags!).
75 * <p>
76 * This is important because the subkey (including the pipe characters and
77 * flags) must be present as-is in the server configuration file to be
78 * allowed.
79 * <ul>
80 * <li><b><i>xxx</i></b>: the encryption key used to communicate with the
81 * server</li>
82 * <li><b><i>yyy</i></b>: the secondary key</li>
83 * <li><b>rw</b>: flag to allow read and write access if it is not the
84 * default on this server</li>
85 * <li><b>wl</b>: flag to allow access to all the stories (bypassing the
86 * whitelist if it exists)</li>
87 * </ul>
88 *
89 * Some examples:
90 * <ul>
91 * <li><b>my_key</b>: normal connection, will take the default server
92 * options</li>
93 * <li><b>my_key|agzyzz|wl</b>: will ask to bypass the white list (if it
94 * exists)</li>
95 * <li><b>my_key|agzyzz|rw</b>: will ask read-write access (if the default
96 * is read-only)</li>
97 * <li><b>my_key|agzyzz|wl|rw</b>: will ask both read-write access and white
98 * list bypass</li>
99 * </ul>
100 *
101 * @param key
102 * the key that will allow us to exchange information with the
103 * server
104 * @param host
105 * the host to contact or NULL for localhost
106 * @param port
107 * the port to contact it on
108 */
109 public RemoteLibrary(String key, String host, int port) {
110 int index = -1;
111 if (key != null) {
112 index = key.indexOf('|');
113 }
114
115 if (index >= 0) {
116 this.key = key.substring(0, index);
117 this.subkey = key.substring(index + 1);
118 } else {
119 this.key = key;
120 this.subkey = "";
121 }
122
123 this.host = host;
124 this.port = port;
125 }
126
127 @Override
128 public String getLibraryName() {
129 return host + ":" + port;
130 }
131
132 @Override
133 public Status getStatus() {
134 Instance.getTraceHandler().trace("Getting remote lib status...");
135 Status status = getStatusDo();
136 Instance.getTraceHandler().trace("Remote lib status: " + status);
137 return status;
138 }
139
140 private Status getStatusDo() {
141 final Status[] result = new Status[1];
142
143 result[0] = Status.INVALID;
144
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.READY;
154 } else if ("r/o".equals(rep)) {
155 rw = false;
156 result[0] = Status.READY;
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) {
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.send(new Object[] { subkey, "GET_COVER",
191 luid });
192 result[0] = (Image) rep;
193 }
194 });
195
196 return result[0];
197 }
198
199 @Override
200 public Image getCustomSourceCover(final String source) {
201 return getCustomCover(source, "SOURCE");
202 }
203
204 @Override
205 public Image getCustomAuthorCover(final String author) {
206 return getCustomCover(author, "AUTHOR");
207 }
208
209 // type: "SOURCE" or "AUTHOR"
210 private Image getCustomCover(final String source, final String type) {
211 final Image[] result = new Image[1];
212
213 connectRemoteAction(new RemoteAction() {
214 @Override
215 public void action(ConnectActionClientObject action)
216 throws Exception {
217 Object rep = action.send(new Object[] { subkey,
218 "GET_CUSTOM_COVER", type, source });
219 result[0] = (Image) rep;
220 }
221 });
222
223 return result[0];
224 }
225
226 @Override
227 public synchronized Story getStory(final String luid, Progress pg) {
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 setCover(source, luid, "SOURCE");
336 }
337
338 @Override
339 public void setAuthorCover(final String author, final String luid) {
340 setCover(author, luid, "AUTHOR");
341 }
342
343 // type = "SOURCE" | "AUTHOR"
344 private void setCover(final String value, final String luid,
345 final String type) {
346 connectRemoteAction(new RemoteAction() {
347 @Override
348 public void action(ConnectActionClientObject action)
349 throws Exception {
350 action.send(new Object[] { subkey, "SET_COVER", type, value,
351 luid });
352 }
353 });
354 }
355
356 @Override
357 // Could work (more slowly) without it
358 public Story imprt(final URL url, Progress pg) throws IOException {
359 // Import the file locally if it is actually a file
360 if (url == null || url.getProtocol().equalsIgnoreCase("file")) {
361 return super.imprt(url, pg);
362 }
363
364 // Import it remotely if it is an URL
365
366 if (pg == null) {
367 pg = new Progress();
368 }
369
370 pg.setMinMax(0, 2);
371 Progress pgImprt = new Progress();
372 Progress pgGet = new Progress();
373 pg.addProgress(pgImprt, 1);
374 pg.addProgress(pgGet, 1);
375
376 final Progress pgF = pgImprt;
377 final String[] luid = new String[1];
378
379 connectRemoteAction(new RemoteAction() {
380 @Override
381 public void action(ConnectActionClientObject action)
382 throws Exception {
383 Progress pg = pgF;
384
385 Object rep = action.send(new Object[] { subkey, "IMPORT",
386 url.toString() });
387
388 while (true) {
389 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
390 break;
391 }
392
393 rep = action.send(null);
394 }
395
396 pg.done();
397 luid[0] = (String) rep;
398 }
399 });
400
401 if (luid[0] == null) {
402 throw new IOException("Remote failure");
403 }
404
405 Story story = getStory(luid[0], pgGet);
406 pgGet.done();
407
408 pg.done();
409 return story;
410 }
411
412 @Override
413 // Could work (more slowly) without it
414 protected synchronized void changeSTA(final String luid,
415 final String newSource, final String newTitle,
416 final String newAuthor, Progress pg) throws IOException {
417
418 final Progress pgF = pg == null ? new Progress() : pg;
419
420 connectRemoteAction(new RemoteAction() {
421 @Override
422 public void action(ConnectActionClientObject action)
423 throws Exception {
424 Progress pg = pgF;
425
426 Object rep = action.send(new Object[] { subkey, "CHANGE_STA",
427 luid, newSource, newTitle, newAuthor });
428 while (true) {
429 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
430 break;
431 }
432
433 rep = action.send(null);
434 }
435 }
436 });
437 }
438
439 @Override
440 public synchronized File getFile(final String luid, Progress pg) {
441 throw new java.lang.InternalError(
442 "Operation not supportorted on remote Libraries");
443 }
444
445 /**
446 * Stop the server.
447 */
448 public void exit() {
449 connectRemoteAction(new RemoteAction() {
450 @Override
451 public void action(ConnectActionClientObject action)
452 throws Exception {
453 action.send(new Object[] { subkey, "EXIT" });
454 }
455 });
456 }
457
458 @Override
459 public synchronized MetaData getInfo(String luid) {
460 List<MetaData> metas = getMetasList(luid, null);
461 if (!metas.isEmpty()) {
462 return metas.get(0);
463 }
464
465 return null;
466 }
467
468 @Override
469 protected List<MetaData> getMetas(Progress pg) {
470 return getMetasList("*", pg);
471 }
472
473 @Override
474 protected void updateInfo(MetaData meta) {
475 // Will be taken care of directly server side
476 }
477
478 @Override
479 protected void invalidateInfo(String luid) {
480 // Will be taken care of directly server side
481 }
482
483 // The following methods are only used by Save and Delete in BasicLibrary:
484
485 @Override
486 protected int getNextId() {
487 throw new java.lang.InternalError("Should not have been called");
488 }
489
490 @Override
491 protected void doDelete(String luid) throws IOException {
492 throw new java.lang.InternalError("Should not have been called");
493 }
494
495 @Override
496 protected Story doSave(Story story, Progress pg) throws IOException {
497 throw new java.lang.InternalError("Should not have been called");
498 }
499
500 //
501
502 /**
503 * Return the meta of the given story or a list of all known metas if the
504 * luid is "*".
505 * <p>
506 * Will not get the covers.
507 *
508 * @param luid
509 * the luid of the story or *
510 * @param pg
511 * the optional progress
512 *
513 *
514 * @return the metas
515 */
516 private List<MetaData> getMetasList(final String luid, Progress pg) {
517 final Progress pgF = pg;
518 final List<MetaData> metas = new ArrayList<MetaData>();
519
520 connectRemoteAction(new RemoteAction() {
521 @Override
522 public void action(ConnectActionClientObject action)
523 throws Exception {
524 Progress pg = pgF;
525 if (pg == null) {
526 pg = new Progress();
527 }
528
529 Object rep = action.send(new Object[] { subkey, "GET_METADATA",
530 luid });
531
532 while (true) {
533 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
534 break;
535 }
536
537 rep = action.send(null);
538 }
539
540 if (rep instanceof MetaData[]) {
541 for (MetaData meta : (MetaData[]) rep) {
542 metas.add(meta);
543 }
544 } else if (rep != null) {
545 metas.add((MetaData) rep);
546 }
547 }
548 });
549
550 return metas;
551 }
552
553 private void connectRemoteAction(final RemoteAction runAction) {
554 try {
555 final RemoteConnectAction[] array = new RemoteConnectAction[1];
556 RemoteConnectAction ra = new RemoteConnectAction() {
557 @Override
558 public void action(Version serverVersion) throws Exception {
559 runAction.action(array[0]);
560 }
561
562 @Override
563 protected void onError(Exception e) {
564 if (e instanceof SSLException) {
565 Instance.getTraceHandler().error(
566 "Connection refused (bad key)");
567 } else {
568 Instance.getTraceHandler().error(e);
569 }
570 }
571 };
572 array[0] = ra;
573 ra.connect();
574 } catch (Exception e) {
575 Instance.getTraceHandler().error(e);
576 }
577 }
578 }