Merge branch 'subtree'
[fanfix.git] / src / be / nikiroo / fanfix / library / WebLibrary.java
CommitLineData
f433d153
NR
1package be.nikiroo.fanfix.library;
2
3import java.io.File;
4import java.io.IOException;
5import java.io.InputStream;
6import java.net.URL;
7import java.util.ArrayList;
8import java.util.HashMap;
9import java.util.List;
10import java.util.Map;
11
12import org.json.JSONArray;
13import org.json.JSONObject;
14
15import be.nikiroo.fanfix.Instance;
c5103223 16import be.nikiroo.fanfix.data.Chapter;
f433d153
NR
17import be.nikiroo.fanfix.data.JsonIO;
18import be.nikiroo.fanfix.data.MetaData;
c5103223
NR
19import be.nikiroo.fanfix.data.Paragraph;
20import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
f433d153
NR
21import be.nikiroo.fanfix.data.Story;
22import be.nikiroo.utils.IOUtils;
23import be.nikiroo.utils.Image;
24import be.nikiroo.utils.Progress;
5ee0fc14 25import be.nikiroo.utils.Version;
f433d153
NR
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 */
36public 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;
f433d153
NR
106 }
107
f70bcacf
NR
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 */
5ee0fc14
NR
115 public Version getVersion() {
116 try {
e4b1b70c 117 InputStream in = post(WebLibraryUrls.VERSION_URL);
5ee0fc14
NR
118 try {
119 return new Version(IOUtils.readSmallStream(in));
120 } finally {
121 in.close();
122 }
123 } catch (IOException e) {
124 }
125
a4efa44c 126 return new Version();
5ee0fc14
NR
127 }
128
f433d153
NR
129 @Override
130 public Status getStatus() {
131 try {
e4b1b70c 132 post(WebLibraryUrls.INDEX_URL).close();
f433d153
NR
133 } catch (IOException e) {
134 try {
e4b1b70c 135 post("/style.css").close();
f433d153
NR
136 return Status.UNAUTHORIZED;
137 } catch (IOException ioe) {
5ee0fc14 138 return Status.UNAVAILABLE;
f433d153
NR
139 }
140 }
141
142 return rw ? Status.READ_WRITE : Status.READ_ONLY;
143 }
144
145 @Override
146 public String getLibraryName() {
a4efa44c
NR
147 return (rw ? "[READ-ONLY] " : "") + host + ":" + port + " ("
148 + getVersion() + ")";
f433d153
NR
149 }
150
151 @Override
152 public Image getCover(String luid) throws IOException {
e4b1b70c 153 InputStream in = post(WebLibraryUrls.getStoryUrlCover(luid));
5ee0fc14 154 try {
a1226ce0
NR
155 Image img = new Image(in);
156 if (img.getSize() > 0) {
157 return img;
158 }
159
160 return null;
5ee0fc14
NR
161 } finally {
162 in.close();
f433d153 163 }
f433d153
NR
164 }
165
c5103223 166 @Override
6673ec59 167 public Image getCustomSourceCover(String source) throws IOException {
e4b1b70c 168 InputStream in = post(WebLibraryUrls.getCoverUrlSource(source));
6673ec59 169 try {
a1226ce0
NR
170 Image img = new Image(in);
171 if (img.getSize() > 0) {
172 return img;
173 }
174
175 return null;
6673ec59
NR
176 } finally {
177 in.close();
178 }
c5103223
NR
179 }
180
181 @Override
6673ec59 182 public Image getCustomAuthorCover(String author) throws IOException {
e4b1b70c 183 InputStream in = post(WebLibraryUrls.getCoverUrlAuthor(author));
6673ec59 184 try {
a1226ce0
NR
185 Image img = new Image(in);
186 if (img.getSize() > 0) {
187 return img;
188 }
189
190 return null;
6673ec59
NR
191 } finally {
192 in.close();
193 }
c5103223
NR
194 }
195
f433d153
NR
196 @Override
197 public void setSourceCover(String source, String luid) throws IOException {
e4b1b70c
NR
198 Map<String, String> post = new HashMap<String, String>();
199 post.put("luid", luid);
200 post(WebLibraryUrls.getCoverUrlSource(source), post).close();
f433d153
NR
201 }
202
203 @Override
204 public void setAuthorCover(String author, String luid) throws IOException {
e4b1b70c
NR
205 Map<String, String> post = new HashMap<String, String>();
206 post.put("luid", luid);
207 post(WebLibraryUrls.getCoverUrlAuthor(author), post).close();
f433d153
NR
208 }
209
c5103223
NR
210 @Override
211 public synchronized Story getStory(final String luid, Progress pg)
212 throws IOException {
f70bcacf
NR
213 if (pg == null) {
214 pg = new Progress();
215 }
c5103223
NR
216
217 Story story;
e4b1b70c 218 InputStream in = post(WebLibraryUrls.getStoryUrlJson(luid));
c5103223
NR
219 try {
220 JSONObject json = new JSONObject(IOUtils.readSmallStream(in));
221 story = JsonIO.toStory(json);
222 } finally {
223 in.close();
224 }
225
f70bcacf
NR
226 int max = 0;
227 for (Chapter chap : story) {
228 max += chap.getParagraphs().size();
229 }
230 pg.setMinMax(0, max);
231
c5103223
NR
232 story.getMeta().setCover(getCover(luid));
233 int chapNum = 1;
234 for (Chapter chap : story) {
235 int number = 1;
236 for (Paragraph para : chap) {
237 if (para.getType() == ParagraphType.IMAGE) {
e4b1b70c 238 InputStream subin = post(
5ee0fc14 239 WebLibraryUrls.getStoryUrl(luid, chapNum, number));
c5103223 240 try {
a1226ce0
NR
241 Image img = new Image(subin);
242 if (img.getSize() > 0) {
243 para.setContentImage(img);
244 }
c5103223
NR
245 } finally {
246 subin.close();
247 }
248 }
249
f70bcacf 250 pg.add(1);
c5103223
NR
251 number++;
252 }
253
254 chapNum++;
255 }
256
f70bcacf 257 pg.done();
c5103223
NR
258 return story;
259 }
260
f433d153
NR
261 @Override
262 protected List<MetaData> getMetas(Progress pg) throws IOException {
263 List<MetaData> metas = new ArrayList<MetaData>();
e4b1b70c 264 InputStream in = post(WebLibraryUrls.LIST_URL_METADATA);
f433d153
NR
265 JSONArray jsonArr = new JSONArray(IOUtils.readSmallStream(in));
266 for (int i = 0; i < jsonArr.length(); i++) {
267 JSONObject json = jsonArr.getJSONObject(i);
268 metas.add(JsonIO.toMetaData(json));
269 }
270
271 return metas;
272 }
273
274 @Override
275 // Could work (more slowly) without it
276 public MetaData imprt(final URL url, Progress pg) throws IOException {
f70bcacf
NR
277 if (pg == null) {
278 pg = new Progress();
279 }
f433d153
NR
280
281 // Import the file locally if it is actually a file
282
283 if (url == null || url.getProtocol().equalsIgnoreCase("file")) {
284 return super.imprt(url, pg);
285 }
286
287 // Import it remotely if it is an URL
288
f70bcacf
NR
289 try {
290 String luid = null;
291
292 Map<String, String> post = new HashMap<String, String>();
293 post.put("url", url.toString());
294 InputStream in = post(WebLibraryUrls.IMPRT_URL_IMPORT, post);
295 try {
296 luid = IOUtils.readSmallStream(in);
297 } finally {
298 in.close();
299 }
300
301 Progress subPg = null;
302 do {
303 try {
304 Thread.sleep(2000);
305 } catch (InterruptedException e) {
306 }
307
308 in = post(WebLibraryUrls.getImprtProgressUrl(luid));
309 try {
310 subPg = JsonIO.toProgress(
311 new JSONObject(IOUtils.readSmallStream(in)));
ce7d5787
NR
312 pg.setName(subPg.getName());
313 pg.setMinMax(subPg.getMin(), subPg.getMax());
314 pg.setProgress(subPg.getProgress());
a1226ce0
NR
315 } catch (Exception e) {
316 subPg = null;
f70bcacf
NR
317 } finally {
318 in.close();
319 }
320 } while (subPg != null);
321
322 in = post(WebLibraryUrls.getStoryUrlMetadata(luid));
323 try {
324 return JsonIO.toMetaData(
325 new JSONObject(IOUtils.readSmallStream(in)));
326 } finally {
327 in.close();
328 }
329 } finally {
330 pg.done();
331 }
f433d153
NR
332 }
333
334 @Override
335 // Could work (more slowly) without it
336 protected synchronized void changeSTA(final String luid,
337 final String newSource, final String newTitle,
338 final String newAuthor, Progress pg) throws IOException {
089e354e
NR
339 MetaData meta = getInfo(luid);
340 if (meta != null) {
341 if (!meta.getSource().equals(newSource)) {
342 Map<String, String> post = new HashMap<String, String>();
343 post.put("value", newSource);
344 post(WebLibraryUrls.getStoryUrlSource(luid), post).close();
345 }
346 if (!meta.getTitle().equals(newTitle)) {
347 Map<String, String> post = new HashMap<String, String>();
348 post.put("value", newTitle);
349 post(WebLibraryUrls.getStoryUrlTitle(luid), post).close();
350 }
351 if (!meta.getAuthor().equals(newAuthor)) {
352 Map<String, String> post = new HashMap<String, String>();
353 post.put("value", newAuthor);
354 post(WebLibraryUrls.getStoryUrlAuthor(luid), post).close();
355 }
356 }
f433d153
NR
357 }
358
acbec0d2
NR
359 @Override
360 public synchronized void delete(String luid) throws IOException {
361 post(WebLibraryUrls.getDeleteUrlStory(luid), null).close();
362 }
363
f433d153
NR
364 @Override
365 protected void updateInfo(MetaData meta) {
366 // Will be taken care of directly server side
367 }
368
369 @Override
370 protected void invalidateInfo(String luid) {
371 // Will be taken care of directly server side
372 }
373
374 // The following methods are only used by Save and Delete in BasicLibrary:
375
376 @Override
f70bcacf 377 protected String getNextId() {
f433d153
NR
378 throw new java.lang.InternalError("Should not have been called");
379 }
380
381 @Override
382 protected void doDelete(String luid) throws IOException {
383 throw new java.lang.InternalError("Should not have been called");
384 }
385
386 @Override
387 protected Story doSave(Story story, Progress pg) throws IOException {
388 throw new java.lang.InternalError("Should not have been called");
389 }
390
391 //
392
393 @Override
394 public File getFile(final String luid, Progress pg) {
395 throw new java.lang.InternalError(
396 "Operation not supportorted on remote Libraries");
397 }
398
5ee0fc14 399 // starts with "/", never NULL
e4b1b70c
NR
400 private InputStream post(String path) throws IOException {
401 return post(path, null);
402 }
403
404 // starts with "/", never NULL
405 private InputStream post(String path, Map<String, String> post)
406 throws IOException {
f433d153
NR
407 URL url = new URL(host + ":" + port + path);
408
e4b1b70c
NR
409 if (post == null) {
410 post = new HashMap<String, String>();
411 }
f433d153
NR
412 post.put("login", subkey);
413 post.put("password", key);
414
415 return Instance.getInstance().getCache().openNoCache(url, null, post,
416 null, null);
417 }
418}