remote server -> plain text
[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 be.nikiroo.fanfix.Instance;
11 import be.nikiroo.fanfix.data.MetaData;
12 import be.nikiroo.fanfix.data.Story;
13 import be.nikiroo.utils.Image;
14 import be.nikiroo.utils.Progress;
15 import be.nikiroo.utils.StringUtils;
16 import be.nikiroo.utils.Version;
17 import be.nikiroo.utils.serial.server.ConnectActionClientObject;
18
19 /**
20 * This {@link BasicLibrary} will access a remote server to list the available
21 * stories, and download the ones you try to load to the local directory
22 * specified in the configuration.
23 *
24 * @author niki
25 */
26 public class RemoteLibrary extends BasicLibrary {
27 private String host;
28 private int port;
29 private final String key;
30
31 /**
32 * Create a {@link RemoteLibrary} linked to the given server.
33 *
34 * @param key
35 * the key that will allow us to exchange information with the
36 * server
37 * @param host
38 * the host to contact or NULL for localhost
39 * @param port
40 * the port to contact it on
41 */
42 public RemoteLibrary(String key, String host, int port) {
43 this.key = key;
44 this.host = host;
45 this.port = port;
46 }
47
48 @Override
49 public String getLibraryName() {
50 return host + ":" + port;
51 }
52
53 @Override
54 public Status getStatus() {
55 final Status[] result = new Status[1];
56
57 result[0] = Status.INVALID;
58
59 try {
60 Instance.getTraceHandler().trace("Getting remote lib status...");
61 new ConnectActionClientObject(host, port, false) {
62 @Override
63 public void action(Version serverVersion) throws Exception {
64 try {
65 Object rep = sendCmd(this, new Object[] { "PING" });
66
67 if ("PONG".equals(rep)) {
68 result[0] = Status.READY;
69 } else {
70 result[0] = Status.UNAUTORIZED;
71 }
72 } catch (IllegalArgumentException e) {
73 result[0] = Status.UNAUTORIZED;
74 }
75 }
76
77 @Override
78 protected void onError(Exception e) {
79 result[0] = Status.UNAVAILABLE;
80 }
81 }.connect();
82 } catch (UnknownHostException e) {
83 result[0] = Status.INVALID;
84 } catch (IllegalArgumentException e) {
85 result[0] = Status.INVALID;
86 } catch (Exception e) {
87 result[0] = Status.UNAVAILABLE;
88 }
89
90 Instance.getTraceHandler().trace("Remote lib status: " + result[0]);
91 return result[0];
92 }
93
94 @Override
95 public Image getCover(final String luid) {
96 final Image[] result = new Image[1];
97
98 try {
99 new ConnectActionClientObject(host, port, false) {
100 @Override
101 public void action(Version serverVersion) throws Exception {
102 Object rep = sendCmd(this,
103 new Object[] { "GET_COVER", luid });
104 result[0] = (Image) rep;
105 }
106
107 @Override
108 protected void onError(Exception e) {
109 Instance.getTraceHandler().error(e);
110 }
111 }.connect();
112 } catch (Exception e) {
113 Instance.getTraceHandler().error(e);
114 }
115
116 return result[0];
117 }
118
119 @Override
120 public Image getCustomSourceCover(final String source) {
121 return getCustomCover(source, "SOURCE");
122 }
123
124 @Override
125 public Image getCustomAuthorCover(final String author) {
126 return getCustomCover(author, "AUTHOR");
127 }
128
129 // type: "SOURCE" or "AUTHOR"
130 private Image getCustomCover(final String source, final String type) {
131 final Image[] result = new Image[1];
132
133 try {
134 new ConnectActionClientObject(host, port, false) {
135 @Override
136 public void action(Version serverVersion) throws Exception {
137 Object rep = sendCmd(this,
138 new Object[] { "GET_CUSTOM_COVER", type, source });
139 result[0] = (Image) rep;
140 }
141
142 @Override
143 protected void onError(Exception e) {
144 Instance.getTraceHandler().error(e);
145 }
146 }.connect();
147 } catch (Exception e) {
148 Instance.getTraceHandler().error(e);
149 }
150
151 return result[0];
152 }
153
154 @Override
155 public synchronized Story getStory(final String luid, Progress pg) {
156 final Progress pgF = pg;
157 final Story[] result = new Story[1];
158
159 try {
160 new ConnectActionClientObject(host, port, false) {
161 @Override
162 public void action(Version serverVersion) throws Exception {
163 Progress pg = pgF;
164 if (pg == null) {
165 pg = new Progress();
166 }
167
168 Object rep = sendCmd(this,
169 new Object[] { "GET_STORY", luid });
170
171 MetaData meta = null;
172 if (rep instanceof MetaData) {
173 meta = (MetaData) rep;
174 if (meta.getWords() <= Integer.MAX_VALUE) {
175 pg.setMinMax(0, (int) meta.getWords());
176 }
177 }
178
179 List<Object> list = new ArrayList<Object>();
180 for (Object obj = send(null); obj != null; obj = send(
181 null)) {
182 list.add(obj);
183 pg.add(1);
184 }
185
186 result[0] = RemoteLibraryServer.rebuildStory(list);
187 pg.done();
188 }
189
190 @Override
191 protected void onError(Exception e) {
192 Instance.getTraceHandler().error(e);
193 }
194 }.connect();
195 } catch (Exception e) {
196 Instance.getTraceHandler().error(e);
197 }
198
199 return result[0];
200 }
201
202 @Override
203 public synchronized Story save(final Story story, final String luid,
204 Progress pg) throws IOException {
205 final String[] luidSaved = new String[1];
206 Progress pgSave = new Progress();
207 Progress pgRefresh = new Progress();
208 if (pg == null) {
209 pg = new Progress();
210 }
211
212 pg.setMinMax(0, 10);
213 pg.addProgress(pgSave, 9);
214 pg.addProgress(pgRefresh, 1);
215
216 final Progress pgF = pgSave;
217
218 new ConnectActionClientObject(host, port, false) {
219 @Override
220 public void action(Version serverVersion) throws Exception {
221 Progress pg = pgF;
222 if (story.getMeta().getWords() <= Integer.MAX_VALUE) {
223 pg.setMinMax(0, (int) story.getMeta().getWords());
224 }
225
226 sendCmd(this, new Object[] { "SAVE_STORY", luid });
227
228 List<Object> list = RemoteLibraryServer.breakStory(story);
229 for (Object obj : list) {
230 send(obj);
231 pg.add(1);
232 }
233
234 luidSaved[0] = (String) send(null);
235
236 pg.done();
237 }
238
239 @Override
240 protected void onError(Exception e) {
241 Instance.getTraceHandler().error(e);
242 }
243 }.connect();
244
245 // because the meta changed:
246 MetaData meta = getInfo(luidSaved[0]);
247 if (story.getMeta().getClass() != null) {
248 // If already available locally:
249 meta.setCover(story.getMeta().getCover());
250 } else {
251 // If required:
252 meta.setCover(getCover(meta.getLuid()));
253 }
254 story.setMeta(meta);
255
256 pg.done();
257
258 return story;
259 }
260
261 @Override
262 public synchronized void delete(final String luid) throws IOException {
263 new ConnectActionClientObject(host, port, false) {
264 @Override
265 public void action(Version serverVersion) throws Exception {
266 sendCmd(this, new Object[] { "DELETE_STORY", luid });
267 }
268
269 @Override
270 protected void onError(Exception e) {
271 Instance.getTraceHandler().error(e);
272 }
273 }.connect();
274 }
275
276 @Override
277 public void setSourceCover(final String source, final String luid) {
278 setCover(source, luid, "SOURCE");
279 }
280
281 @Override
282 public void setAuthorCover(final String author, final String luid) {
283 setCover(author, luid, "AUTHOR");
284 }
285
286 // type = "SOURCE" | "AUTHOR"
287 private void setCover(final String value, final String luid,
288 final String type) {
289 try {
290 new ConnectActionClientObject(host, port, false) {
291 @Override
292 public void action(Version serverVersion) throws Exception {
293 sendCmd(this,
294 new Object[] { "SET_COVER", type, value, luid });
295 }
296
297 @Override
298 protected void onError(Exception e) {
299 Instance.getTraceHandler().error(e);
300 }
301 }.connect();
302 } catch (IOException e) {
303 Instance.getTraceHandler().error(e);
304 }
305 }
306
307 @Override
308 // Could work (more slowly) without it
309 public Story imprt(final URL url, Progress pg) throws IOException {
310 // Import the file locally if it is actually a file
311 if (url == null || url.getProtocol().equalsIgnoreCase("file")) {
312 return super.imprt(url, pg);
313 }
314
315 // Import it remotely if it is an URL
316
317 if (pg == null) {
318 pg = new Progress();
319 }
320
321 pg.setMinMax(0, 2);
322 Progress pgImprt = new Progress();
323 Progress pgGet = new Progress();
324 pg.addProgress(pgImprt, 1);
325 pg.addProgress(pgGet, 1);
326
327 final Progress pgF = pgImprt;
328 final String[] luid = new String[1];
329
330 try {
331 new ConnectActionClientObject(host, port, false) {
332 @Override
333 public void action(Version serverVersion) throws Exception {
334 Progress pg = pgF;
335
336 Object rep = sendCmd(this,
337 new Object[] { "IMPORT", url.toString() });
338
339 while (true) {
340 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
341 break;
342 }
343
344 rep = send(null);
345 }
346
347 pg.done();
348 luid[0] = (String) rep;
349 }
350
351 @Override
352 protected void onError(Exception e) {
353 Instance.getTraceHandler().error(e);
354 }
355 }.connect();
356 } catch (IOException e) {
357 Instance.getTraceHandler().error(e);
358 }
359
360 if (luid[0] == null) {
361 throw new IOException("Remote failure");
362 }
363
364 Story story = getStory(luid[0], pgGet);
365 pgGet.done();
366
367 pg.done();
368 return story;
369 }
370
371 @Override
372 // Could work (more slowly) without it
373 protected synchronized void changeSTA(final String luid,
374 final String newSource, final String newTitle,
375 final String newAuthor, Progress pg) throws IOException {
376 final Progress pgF = pg == null ? new Progress() : pg;
377
378 try {
379 new ConnectActionClientObject(host, port, false) {
380 @Override
381 public void action(Version serverVersion) throws Exception {
382 Progress pg = pgF;
383
384 Object rep = sendCmd(this, new Object[] { "CHANGE_STA",
385 luid, newSource, newTitle, newAuthor });
386 while (true) {
387 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
388 break;
389 }
390
391 rep = send(null);
392 }
393 }
394
395 @Override
396 protected void onError(Exception e) {
397 Instance.getTraceHandler().error(e);
398 }
399 }.connect();
400 } catch (IOException e) {
401 Instance.getTraceHandler().error(e);
402 }
403 }
404
405 @Override
406 public synchronized File getFile(final String luid, Progress pg) {
407 throw new java.lang.InternalError(
408 "Operation not supportorted on remote Libraries");
409 }
410
411 /**
412 * Stop the server.
413 */
414 public void exit() {
415 try {
416 new ConnectActionClientObject(host, port, false) {
417 @Override
418 public void action(Version serverVersion) throws Exception {
419 sendCmd(this, new Object[] { "EXIT" });
420 }
421
422 @Override
423 protected void onError(Exception e) {
424 Instance.getTraceHandler().error(e);
425 }
426 }.connect();
427 } catch (IOException e) {
428 Instance.getTraceHandler().error(e);
429 }
430 }
431
432 @Override
433 public synchronized MetaData getInfo(String luid) {
434 List<MetaData> metas = getMetasList(luid, null);
435 if (!metas.isEmpty()) {
436 return metas.get(0);
437 }
438
439 return null;
440 }
441
442 @Override
443 protected List<MetaData> getMetas(Progress pg) {
444 return getMetasList("*", pg);
445 }
446
447 @Override
448 protected void updateInfo(MetaData meta) {
449 // Will be taken care of directly server side
450 }
451
452 @Override
453 protected void invalidateInfo(String luid) {
454 // Will be taken care of directly server side
455 }
456
457 // The following methods are only used by Save and Delete in BasicLibrary:
458
459 @Override
460 protected int getNextId() {
461 throw new java.lang.InternalError("Should not have been called");
462 }
463
464 @Override
465 protected void doDelete(String luid) throws IOException {
466 throw new java.lang.InternalError("Should not have been called");
467 }
468
469 @Override
470 protected Story doSave(Story story, Progress pg) throws IOException {
471 throw new java.lang.InternalError("Should not have been called");
472 }
473
474 //
475
476 /**
477 * Return the meta of the given story or a list of all known metas if the
478 * luid is "*".
479 * <p>
480 * Will not get the covers.
481 *
482 * @param luid
483 * the luid of the story or *
484 * @param pg
485 * the optional progress
486 *
487 *
488 * @return the metas
489 */
490 private List<MetaData> getMetasList(final String luid, Progress pg) {
491 final Progress pgF = pg;
492 final List<MetaData> metas = new ArrayList<MetaData>();
493
494 try {
495 new ConnectActionClientObject(host, port, false) {
496 @Override
497 public void action(Version serverVersion) throws Exception {
498 Progress pg = pgF;
499 if (pg == null) {
500 pg = new Progress();
501 }
502
503
504 Object rep = sendCmd(this,
505 new Object[] { "GET_METADATA", luid });
506
507 while (true) {
508 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
509 break;
510 }
511
512 rep = send(null);
513 }
514
515 if (rep instanceof MetaData[]) {
516 for (MetaData meta : (MetaData[]) rep) {
517 metas.add(meta);
518 }
519 } else if (rep != null) {
520 metas.add((MetaData) rep);
521 }
522 }
523
524 @Override
525 protected void onError(Exception e) {
526 Instance.getTraceHandler().error(e);
527 }
528 }.connect();
529 } catch (Exception e) {
530 Instance.getTraceHandler().error(e);
531 }
532
533 return metas;
534 }
535
536 // IllegalArgumentException if key is bad
537 private Object sendCmd(ConnectActionClientObject action, Object[] params)
538 throws IOException, NoSuchFieldException, NoSuchMethodException,
539 ClassNotFoundException {
540 Object rep = action.send(params);
541
542 String hash = hashKey(key, "" + rep);
543 rep = action.send(hash);
544 if (rep == null) {
545 throw new java.lang.IllegalArgumentException();
546 }
547
548 return action.send(hash);
549 }
550
551 /**
552 * Return a hash that corresponds to the given key and the given random
553 * value.
554 *
555 * @param key
556 * the key (the secret)
557 *
558 * @param random
559 * the random value
560 *
561 * @return a hash that was computed using both
562 */
563 static String hashKey(String key, String random) {
564 return StringUtils.getMd5Hash(key + " <==> " + random);
565 }
566 }