Merge branch 'subtree'
[fanfix.git] / src / be / nikiroo / fanfix / library / WebLibrary.java
1 package be.nikiroo.fanfix.library;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.net.URL;
7 import java.util.ArrayList;
8 import java.util.HashMap;
9 import java.util.List;
10 import java.util.Map;
11
12 import org.json.JSONArray;
13 import org.json.JSONObject;
14
15 import be.nikiroo.fanfix.Instance;
16 import be.nikiroo.fanfix.data.Chapter;
17 import be.nikiroo.fanfix.data.JsonIO;
18 import be.nikiroo.fanfix.data.MetaData;
19 import be.nikiroo.fanfix.data.Paragraph;
20 import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
21 import be.nikiroo.fanfix.data.Story;
22 import be.nikiroo.utils.IOUtils;
23 import be.nikiroo.utils.Image;
24 import be.nikiroo.utils.Progress;
25 import be.nikiroo.utils.Version;
26
27 /**
28 * This {@link BasicLibrary} will access a remote server to list the available
29 * stories, and download the ones you try to load to the local directory
30 * specified in the configuration.
31 * <p>
32 * This remote library uses http:// or https://.
33 *
34 * @author niki
35 */
36 public class WebLibrary extends BasicLibrary {
37 private String host;
38 private int port;
39 private final String key;
40 private final String subkey;
41
42 // informative only (server will make the actual checks)
43 private boolean rw;
44
45 /**
46 * Create a {@link RemoteLibrary} linked to the given server.
47 * <p>
48 * Note that the key is structured:
49 * <tt><b><i>xxx</i></b>(|<b><i>yyy</i></b>|<b>wl</b>)(|<b>rw</b>)</tt>
50 * <p>
51 * Note that anything before the first pipe (<tt>|</tt>) character is
52 * considered to be the encryption key, anything after that character is
53 * called the subkey (including the other pipe characters and flags!).
54 * <p>
55 * This is important because the subkey (including the pipe characters and
56 * flags) must be present as-is in the server configuration file to be
57 * allowed.
58 * <ul>
59 * <li><b><i>xxx</i></b>: the encryption key used to communicate with the
60 * server</li>
61 * <li><b><i>yyy</i></b>: the secondary key</li>
62 * <li><b>rw</b>: flag to allow read and write access if it is not the
63 * default on this server</li>
64 * <li><b>wl</b>: flag to allow access to all the stories (bypassing the
65 * whitelist if it exists)</li>
66 * </ul>
67 * <p>
68 * Some examples:
69 * <ul>
70 * <li><b>my_key</b>: normal connection, will take the default server
71 * options</li>
72 * <li><b>my_key|agzyzz|wl</b>: will ask to bypass the white list (if it
73 * exists)</li>
74 * <li><b>my_key|agzyzz|rw</b>: will ask read-write access (if the default
75 * is read-only)</li>
76 * <li><b>my_key|agzyzz|wl|rw</b>: will ask both read-write access and white
77 * list bypass</li>
78 * </ul>
79 *
80 * @param key
81 * the key that will allow us to exchange information with the
82 * server
83 * @param host
84 * the host to contact or NULL for localhost
85 * @param port
86 * the port to contact it on
87 */
88 public WebLibrary(String key, String host, int port) {
89 int index = -1;
90 if (key != null) {
91 index = key.indexOf('|');
92 }
93
94 if (index >= 0) {
95 this.key = key.substring(0, index);
96 this.subkey = key.substring(index + 1);
97 } else {
98 this.key = key;
99 this.subkey = "";
100 }
101
102 this.rw = subkey.contains("|rw");
103
104 this.host = host;
105 this.port = port;
106 }
107
108 /**
109 * Return the version of the program running server-side.
110 * <p>
111 * Never returns NULL.
112 *
113 * @return the version or an empty {@link Version} if not known
114 */
115 public Version getVersion() {
116 try {
117 InputStream in = post(WebLibraryUrls.VERSION_URL);
118 try {
119 return new Version(IOUtils.readSmallStream(in));
120 } finally {
121 in.close();
122 }
123 } catch (IOException e) {
124 }
125
126 return new Version();
127 }
128
129 /**
130 * Stop the server.
131 *
132 * @throws IOException
133 * in case of I/O errors
134 */
135 public void stop() throws IOException {
136 try {
137 post(WebLibraryUrls.EXIT_URL, null).close();
138 } catch (Exception e) {
139 try {
140 Thread.sleep(200);
141 } catch (InterruptedException e1) {
142 }
143 if (getStatus() != Status.UNAVAILABLE) {
144 throw new IOException("Cannot exit the library", e);
145 }
146 }
147 }
148
149 @Override
150 public Status getStatus() {
151 try {
152 post(WebLibraryUrls.INDEX_URL).close();
153 } catch (IOException e) {
154 try {
155 post("/style.css").close();
156 return Status.UNAUTHORIZED;
157 } catch (IOException ioe) {
158 return Status.UNAVAILABLE;
159 }
160 }
161
162 return rw ? Status.READ_WRITE : Status.READ_ONLY;
163 }
164
165 @Override
166 public String getLibraryName() {
167 return (rw ? "[READ-ONLY] " : "") + host + ":" + port + " ("
168 + getVersion() + ")";
169 }
170
171 @Override
172 public Image getCover(String luid) throws IOException {
173 InputStream in = post(WebLibraryUrls.getStoryUrlCover(luid));
174 try {
175 Image img = new Image(in);
176 if (img.getSize() > 0) {
177 img.close();
178 return img;
179 }
180
181 return null;
182 } finally {
183 in.close();
184 }
185 }
186
187 @Override
188 public Image getCustomSourceCover(String source) throws IOException {
189 InputStream in = post(WebLibraryUrls.getCoverUrlSource(source));
190 try {
191 Image img = new Image(in);
192 if (img.getSize() > 0) {
193 img.close();
194 return img;
195 }
196
197 return null;
198 } finally {
199 in.close();
200 }
201 }
202
203 @Override
204 public Image getCustomAuthorCover(String author) throws IOException {
205 InputStream in = post(WebLibraryUrls.getCoverUrlAuthor(author));
206 try {
207 Image img = new Image(in);
208 if (img.getSize() > 0) {
209 img.close();
210 return img;
211 }
212
213 return null;
214 } finally {
215 in.close();
216 }
217 }
218
219 @Override
220 public void setSourceCover(String source, String luid) throws IOException {
221 Map<String, String> post = new HashMap<String, String>();
222 post.put("luid", luid);
223 post(WebLibraryUrls.getCoverUrlSource(source), post).close();
224 }
225
226 @Override
227 public void setAuthorCover(String author, String luid) throws IOException {
228 Map<String, String> post = new HashMap<String, String>();
229 post.put("luid", luid);
230 post(WebLibraryUrls.getCoverUrlAuthor(author), post).close();
231 }
232
233 @Override
234 public synchronized Story getStory(final String luid, Progress pg)
235 throws IOException {
236 if (pg == null) {
237 pg = new Progress();
238 }
239
240 Story story;
241 InputStream in = post(WebLibraryUrls.getStoryUrlJson(luid));
242 try {
243 JSONObject json = new JSONObject(IOUtils.readSmallStream(in));
244 story = JsonIO.toStory(json);
245 } finally {
246 in.close();
247 }
248
249 int max = 0;
250 for (Chapter chap : story) {
251 max += chap.getParagraphs().size();
252 }
253 pg.setMinMax(0, max);
254
255 story.getMeta().setCover(getCover(luid));
256 int chapNum = 1;
257 for (Chapter chap : story) {
258 int number = 1;
259 for (Paragraph para : chap) {
260 if (para.getType() == ParagraphType.IMAGE) {
261 InputStream subin = post(
262 WebLibraryUrls.getStoryUrl(luid, chapNum, number));
263 try {
264 Image img = new Image(subin);
265 if (img.getSize() > 0) {
266 para.setContentImage(img);
267 }
268 } finally {
269 subin.close();
270 }
271 }
272
273 pg.add(1);
274 number++;
275 }
276
277 chapNum++;
278 }
279
280 pg.done();
281 return story;
282 }
283
284 @Override
285 protected List<MetaData> getMetas(Progress pg) throws IOException {
286 List<MetaData> metas = new ArrayList<MetaData>();
287 InputStream in = post(WebLibraryUrls.LIST_URL_METADATA);
288 JSONArray jsonArr = new JSONArray(IOUtils.readSmallStream(in));
289 for (int i = 0; i < jsonArr.length(); i++) {
290 JSONObject json = jsonArr.getJSONObject(i);
291 metas.add(JsonIO.toMetaData(json));
292 }
293
294 return metas;
295 }
296
297 @Override
298 // Could work (more slowly) without it
299 public MetaData imprt(final URL url, Progress pg) throws IOException {
300 if (pg == null) {
301 pg = new Progress();
302 }
303
304 // Import the file locally if it is actually a file
305
306 if (url == null || url.getProtocol().equalsIgnoreCase("file")) {
307 return super.imprt(url, pg);
308 }
309
310 // Import it remotely if it is an URL
311
312 try {
313 String luid = null;
314
315 Map<String, String> post = new HashMap<String, String>();
316 post.put("url", url.toString());
317 InputStream in = post(WebLibraryUrls.IMPRT_URL_IMPORT, post);
318 try {
319 luid = IOUtils.readSmallStream(in);
320 } finally {
321 in.close();
322 }
323
324 Progress subPg = null;
325 do {
326 try {
327 Thread.sleep(2000);
328 } catch (InterruptedException e) {
329 }
330
331 in = post(WebLibraryUrls.getImprtProgressUrl(luid));
332 try {
333 subPg = JsonIO.toProgress(
334 new JSONObject(IOUtils.readSmallStream(in)));
335 pg.setName(subPg.getName());
336 pg.setMinMax(subPg.getMin(), subPg.getMax());
337 pg.setProgress(subPg.getProgress());
338 } catch (Exception e) {
339 subPg = null;
340 } finally {
341 in.close();
342 }
343 } while (subPg != null);
344
345 in = post(WebLibraryUrls.getStoryUrlMetadata(luid));
346 try {
347 return JsonIO.toMetaData(
348 new JSONObject(IOUtils.readSmallStream(in)));
349 } finally {
350 in.close();
351 }
352 } finally {
353 pg.done();
354 }
355 }
356
357 @Override
358 // Could work (more slowly) without it
359 protected synchronized void changeSTA(final String luid,
360 final String newSource, final String newTitle,
361 final String newAuthor, Progress pg) throws IOException {
362 MetaData meta = getInfo(luid);
363 if (meta != null) {
364 if (!meta.getSource().equals(newSource)) {
365 Map<String, String> post = new HashMap<String, String>();
366 post.put("value", newSource);
367 post(WebLibraryUrls.getStoryUrlSource(luid), post).close();
368 }
369 if (!meta.getTitle().equals(newTitle)) {
370 Map<String, String> post = new HashMap<String, String>();
371 post.put("value", newTitle);
372 post(WebLibraryUrls.getStoryUrlTitle(luid), post).close();
373 }
374 if (!meta.getAuthor().equals(newAuthor)) {
375 Map<String, String> post = new HashMap<String, String>();
376 post.put("value", newAuthor);
377 post(WebLibraryUrls.getStoryUrlAuthor(luid), post).close();
378 }
379 }
380 }
381
382 @Override
383 public synchronized void delete(String luid) throws IOException {
384 post(WebLibraryUrls.getDeleteUrlStory(luid), null).close();
385 }
386
387 @Override
388 protected void updateInfo(MetaData meta) {
389 // Will be taken care of directly server side
390 }
391
392 @Override
393 protected void invalidateInfo(String luid) {
394 // Will be taken care of directly server side
395 }
396
397 // The following methods are only used by Save and Delete in BasicLibrary:
398
399 @Override
400 protected String getNextId() {
401 throw new java.lang.InternalError("Should not have been called");
402 }
403
404 @Override
405 protected void doDelete(String luid) throws IOException {
406 throw new java.lang.InternalError("Should not have been called");
407 }
408
409 @Override
410 protected Story doSave(Story story, Progress pg) throws IOException {
411 throw new java.lang.InternalError("Should not have been called");
412 }
413
414 //
415
416 @Override
417 public File getFile(final String luid, Progress pg) {
418 throw new java.lang.InternalError(
419 "Operation not supportorted on remote Libraries");
420 }
421
422 // starts with "/", never NULL
423 private InputStream post(String path) throws IOException {
424 return post(path, null);
425 }
426
427 // starts with "/", never NULL
428 private InputStream post(String path, Map<String, String> post)
429 throws IOException {
430 URL url = new URL(host + ":" + port + path);
431
432 if (post == null) {
433 post = new HashMap<String, String>();
434 }
435 post.put("login", subkey);
436 post.put("password", key);
437
438 return Instance.getInstance().getCache().openNoCache(url, null, post,
439 null, null);
440 }
441 }