ssl -> cryptutils
[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, key) {
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, key) {
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, key) {
135 @Override
136 public void action(Version serverVersion) throws Exception {
137 Object rep = sendCmd(this, new Object[] {
138 "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, key) {
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(null)) {
181 list.add(obj);
182 pg.add(1);
183 }
184
185 result[0] = RemoteLibraryServer.rebuildStory(list);
186 pg.done();
187 }
188
189 @Override
190 protected void onError(Exception e) {
191 Instance.getTraceHandler().error(e);
192 }
193 }.connect();
194 } catch (Exception e) {
195 Instance.getTraceHandler().error(e);
196 }
197
198 return result[0];
199 }
200
201 @Override
202 public synchronized Story save(final Story story, final String luid,
203 Progress pg) throws IOException {
204 final String[] luidSaved = new String[1];
205 Progress pgSave = new Progress();
206 Progress pgRefresh = new Progress();
207 if (pg == null) {
208 pg = new Progress();
209 }
210
211 pg.setMinMax(0, 10);
212 pg.addProgress(pgSave, 9);
213 pg.addProgress(pgRefresh, 1);
214
215 final Progress pgF = pgSave;
216
217 new ConnectActionClientObject(host, port, key) {
218 @Override
219 public void action(Version serverVersion) throws Exception {
220 Progress pg = pgF;
221 if (story.getMeta().getWords() <= Integer.MAX_VALUE) {
222 pg.setMinMax(0, (int) story.getMeta().getWords());
223 }
224
225 sendCmd(this, new Object[] { "SAVE_STORY", luid });
226
227 List<Object> list = RemoteLibraryServer.breakStory(story);
228 for (Object obj : list) {
229 send(obj);
230 pg.add(1);
231 }
232
233 luidSaved[0] = (String) send(null);
234
235 pg.done();
236 }
237
238 @Override
239 protected void onError(Exception e) {
240 Instance.getTraceHandler().error(e);
241 }
242 }.connect();
243
244 // because the meta changed:
245 MetaData meta = getInfo(luidSaved[0]);
246 if (story.getMeta().getClass() != null) {
247 // If already available locally:
248 meta.setCover(story.getMeta().getCover());
249 } else {
250 // If required:
251 meta.setCover(getCover(meta.getLuid()));
252 }
253 story.setMeta(meta);
254
255 pg.done();
256
257 return story;
258 }
259
260 @Override
261 public synchronized void delete(final String luid) throws IOException {
262 new ConnectActionClientObject(host, port, key) {
263 @Override
264 public void action(Version serverVersion) throws Exception {
265 sendCmd(this, new Object[] { "DELETE_STORY", luid });
266 }
267
268 @Override
269 protected void onError(Exception e) {
270 Instance.getTraceHandler().error(e);
271 }
272 }.connect();
273 }
274
275 @Override
276 public void setSourceCover(final String source, final String luid) {
277 setCover(source, luid, "SOURCE");
278 }
279
280 @Override
281 public void setAuthorCover(final String author, final String luid) {
282 setCover(author, luid, "AUTHOR");
283 }
284
285 // type = "SOURCE" | "AUTHOR"
286 private void setCover(final String value, final String luid,
287 final String type) {
288 try {
289 new ConnectActionClientObject(host, port, key) {
290 @Override
291 public void action(Version serverVersion) throws Exception {
292 sendCmd(this,
293 new Object[] { "SET_COVER", type, value, luid });
294 }
295
296 @Override
297 protected void onError(Exception e) {
298 Instance.getTraceHandler().error(e);
299 }
300 }.connect();
301 } catch (IOException e) {
302 Instance.getTraceHandler().error(e);
303 }
304 }
305
306 @Override
307 // Could work (more slowly) without it
308 public Story imprt(final URL url, Progress pg) throws IOException {
309 // Import the file locally if it is actually a file
310 if (url == null || url.getProtocol().equalsIgnoreCase("file")) {
311 return super.imprt(url, pg);
312 }
313
314 // Import it remotely if it is an URL
315
316 if (pg == null) {
317 pg = new Progress();
318 }
319
320 pg.setMinMax(0, 2);
321 Progress pgImprt = new Progress();
322 Progress pgGet = new Progress();
323 pg.addProgress(pgImprt, 1);
324 pg.addProgress(pgGet, 1);
325
326 final Progress pgF = pgImprt;
327 final String[] luid = new String[1];
328
329 try {
330 new ConnectActionClientObject(host, port, key) {
331 @Override
332 public void action(Version serverVersion) throws Exception {
333 Progress pg = pgF;
334
335 Object rep = sendCmd(this,
336 new Object[] { "IMPORT", url.toString() });
337
338 while (true) {
339 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
340 break;
341 }
342
343 rep = send(null);
344 }
345
346 pg.done();
347 luid[0] = (String) rep;
348 }
349
350 @Override
351 protected void onError(Exception e) {
352 Instance.getTraceHandler().error(e);
353 }
354 }.connect();
355 } catch (IOException e) {
356 Instance.getTraceHandler().error(e);
357 }
358
359 if (luid[0] == null) {
360 throw new IOException("Remote failure");
361 }
362
363 Story story = getStory(luid[0], pgGet);
364 pgGet.done();
365
366 pg.done();
367 return story;
368 }
369
370 @Override
371 // Could work (more slowly) without it
372 protected synchronized void changeSTA(final String luid,
373 final String newSource, final String newTitle,
374 final String newAuthor, Progress pg) throws IOException {
375 final Progress pgF = pg == null ? new Progress() : pg;
376
377 try {
378 new ConnectActionClientObject(host, port, key) {
379 @Override
380 public void action(Version serverVersion) throws Exception {
381 Progress pg = pgF;
382
383 Object rep = sendCmd(this, new Object[] { "CHANGE_STA",
384 luid, newSource, newTitle, newAuthor });
385 while (true) {
386 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
387 break;
388 }
389
390 rep = send(null);
391 }
392 }
393
394 @Override
395 protected void onError(Exception e) {
396 Instance.getTraceHandler().error(e);
397 }
398 }.connect();
399 } catch (IOException e) {
400 Instance.getTraceHandler().error(e);
401 }
402 }
403
404 @Override
405 public synchronized File getFile(final String luid, Progress pg) {
406 throw new java.lang.InternalError(
407 "Operation not supportorted on remote Libraries");
408 }
409
410 /**
411 * Stop the server.
412 */
413 public void exit() {
414 try {
415 new ConnectActionClientObject(host, port, key) {
416 @Override
417 public void action(Version serverVersion) throws Exception {
418 sendCmd(this, new Object[] { "EXIT" });
419 }
420
421 @Override
422 protected void onError(Exception e) {
423 Instance.getTraceHandler().error(e);
424 }
425 }.connect();
426 } catch (IOException e) {
427 Instance.getTraceHandler().error(e);
428 }
429 }
430
431 @Override
432 public synchronized MetaData getInfo(String luid) {
433 List<MetaData> metas = getMetasList(luid, null);
434 if (!metas.isEmpty()) {
435 return metas.get(0);
436 }
437
438 return null;
439 }
440
441 @Override
442 protected List<MetaData> getMetas(Progress pg) {
443 return getMetasList("*", pg);
444 }
445
446 @Override
447 protected void updateInfo(MetaData meta) {
448 // Will be taken care of directly server side
449 }
450
451 @Override
452 protected void invalidateInfo(String luid) {
453 // Will be taken care of directly server side
454 }
455
456 // The following methods are only used by Save and Delete in BasicLibrary:
457
458 @Override
459 protected int getNextId() {
460 throw new java.lang.InternalError("Should not have been called");
461 }
462
463 @Override
464 protected void doDelete(String luid) throws IOException {
465 throw new java.lang.InternalError("Should not have been called");
466 }
467
468 @Override
469 protected Story doSave(Story story, Progress pg) throws IOException {
470 throw new java.lang.InternalError("Should not have been called");
471 }
472
473 //
474
475 /**
476 * Return the meta of the given story or a list of all known metas if the
477 * luid is "*".
478 * <p>
479 * Will not get the covers.
480 *
481 * @param luid
482 * the luid of the story or *
483 * @param pg
484 * the optional progress
485 *
486 *
487 * @return the metas
488 */
489 private List<MetaData> getMetasList(final String luid, Progress pg) {
490 final Progress pgF = pg;
491 final List<MetaData> metas = new ArrayList<MetaData>();
492
493 try {
494 new ConnectActionClientObject(host, port, key) {
495 @Override
496 public void action(Version serverVersion) throws Exception {
497 Progress pg = pgF;
498 if (pg == null) {
499 pg = new Progress();
500 }
501
502 Object rep = sendCmd(this, new Object[] { "GET_METADATA",
503 luid });
504
505 while (true) {
506 if (!RemoteLibraryServer.updateProgress(pg, rep)) {
507 break;
508 }
509
510 rep = send(null);
511 }
512
513 if (rep instanceof MetaData[]) {
514 for (MetaData meta : (MetaData[]) rep) {
515 metas.add(meta);
516 }
517 } else if (rep != null) {
518 metas.add((MetaData) rep);
519 }
520 }
521
522 @Override
523 protected void onError(Exception e) {
524 Instance.getTraceHandler().error(e);
525 }
526 }.connect();
527 } catch (Exception e) {
528 Instance.getTraceHandler().error(e);
529 }
530
531 return metas;
532 }
533
534 // IllegalArgumentException if key is bad
535 private Object sendCmd(ConnectActionClientObject action, Object[] params)
536 throws IOException, NoSuchFieldException, NoSuchMethodException,
537 ClassNotFoundException {
538 Object rep = action.send(params);
539
540 String hash = hashKey(key, "" + rep);
541 return action.send(hash);
542 }
543
544 /**
545 * Return a hash that corresponds to the given key and the given random
546 * value.
547 *
548 * @param key
549 * the key (the secret)
550 *
551 * @param random
552 * the random value
553 *
554 * @return a hash that was computed using both
555 */
556 static String hashKey(String key, String random) {
557 return StringUtils.getMd5Hash(key + " <==> " + random);
558 }
559 }