weblib: change STA
[fanfix.git] / src / be / nikiroo / fanfix / library / WebLibraryServer.java
CommitLineData
f433d153
NR
1package be.nikiroo.fanfix.library;
2
3import java.io.ByteArrayInputStream;
f433d153
NR
4import java.io.IOException;
5import java.io.InputStream;
f433d153 6import java.util.ArrayList;
5c4ce687 7import java.util.Arrays;
f433d153
NR
8import java.util.HashMap;
9import java.util.LinkedList;
10import java.util.List;
11import java.util.Map;
12
f433d153
NR
13import org.json.JSONArray;
14import org.json.JSONObject;
15
16import be.nikiroo.fanfix.Instance;
17import be.nikiroo.fanfix.bundles.Config;
f433d153
NR
18import be.nikiroo.fanfix.data.Chapter;
19import be.nikiroo.fanfix.data.JsonIO;
20import be.nikiroo.fanfix.data.MetaData;
21import be.nikiroo.fanfix.data.Paragraph;
22import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
23import be.nikiroo.fanfix.data.Story;
f433d153 24import be.nikiroo.utils.Image;
fce0a73f 25import be.nikiroo.utils.LoginResult;
f433d153 26import be.nikiroo.utils.NanoHTTPD;
f433d153
NR
27import be.nikiroo.utils.NanoHTTPD.Response;
28import be.nikiroo.utils.NanoHTTPD.Response.Status;
f433d153 29
33d40b9f
NR
30public class WebLibraryServer extends WebLibraryServerHtml {
31 class WLoginResult extends LoginResult {
f433d153
NR
32 private boolean rw;
33 private boolean wl;
d11fb35b 34 private boolean bl;
fce0a73f
NR
35
36 public WLoginResult(boolean badLogin, boolean badCookie) {
37 super(badLogin, badCookie);
38 }
39
40 public WLoginResult(String who, String key, String subkey, boolean rw,
41 boolean wl, boolean bl) {
42 super(who, key, subkey, (rw ? "|rw" : "") + (wl ? "|wl" : "")
43 + (bl ? "|bl" : "") + "|");
f433d153
NR
44 this.rw = rw;
45 this.wl = wl;
d11fb35b 46 this.bl = bl;
f433d153
NR
47 }
48
fce0a73f 49 public WLoginResult(String cookie, String who, String key,
f433d153 50 List<String> subkeys) {
fce0a73f
NR
51 super(cookie, who, key, subkeys,
52 subkeys == null || subkeys.isEmpty());
f433d153
NR
53 }
54
55 public boolean isRw() {
fce0a73f 56 return getOption().contains("|rw|");
f433d153
NR
57 }
58
59 public boolean isWl() {
fce0a73f 60 return getOption().contains("|wl|");
f433d153
NR
61 }
62
d11fb35b 63 public boolean isBl() {
fce0a73f 64 return getOption().contains("|bl|");
f433d153
NR
65 }
66 }
67
f433d153
NR
68 private Map<String, Story> storyCache = new HashMap<String, Story>();
69 private LinkedList<String> storyCacheOrder = new LinkedList<String>();
70 private long storyCacheSize = 0;
71 private long maxStoryCacheSize;
f433d153 72
d11fb35b
NR
73 private List<String> whitelist;
74 private List<String> blacklist;
75
f433d153 76 public WebLibraryServer(boolean secure) throws IOException {
33d40b9f 77 super(secure);
f433d153
NR
78
79 int cacheMb = Instance.getInstance().getConfig()
80 .getInteger(Config.SERVER_MAX_CACHE_MB, 100);
81 maxStoryCacheSize = cacheMb * 1024 * 1024;
82
83 setTraceHandler(Instance.getInstance().getTraceHandler());
84
d11fb35b
NR
85 whitelist = Instance.getInstance().getConfig()
86 .getList(Config.SERVER_WHITELIST, new ArrayList<String>());
87 blacklist = Instance.getInstance().getConfig()
88 .getList(Config.SERVER_BLACKLIST, new ArrayList<String>());
f433d153
NR
89 }
90
91 /**
92 * Start the server (listen on the network for new connections).
93 * <p>
94 * Can only be called once.
95 * <p>
96 * This call is asynchronous, and will just start a new {@link Thread} on
97 * itself (see {@link WebLibraryServer#run()}).
98 */
99 public void start() {
100 new Thread(this).start();
101 }
102
33d40b9f
NR
103 @Override
104 protected WLoginResult login(boolean badLogin, boolean badCookie) {
105 return new WLoginResult(false, false);
f433d153
NR
106 }
107
33d40b9f
NR
108 @Override
109 protected WLoginResult login(String who, String cookie) {
fce0a73f
NR
110 List<String> subkeys = Instance.getInstance().getConfig()
111 .getList(Config.SERVER_ALLOWED_SUBKEYS);
f433d153 112 String realKey = Instance.getInstance().getConfig()
fce0a73f 113 .getString(Config.SERVER_KEY);
d11fb35b 114
fce0a73f 115 return new WLoginResult(cookie, who, realKey, subkeys);
f433d153
NR
116 }
117
118 // allow rw/wl
33d40b9f
NR
119 @Override
120 protected WLoginResult login(String who, String key, String subkey) {
f433d153 121 String realKey = Instance.getInstance().getConfig()
d11fb35b 122 .getString(Config.SERVER_KEY, "");
f433d153
NR
123
124 // I don't like NULLs...
f433d153
NR
125 key = key == null ? "" : key;
126 subkey = subkey == null ? "" : subkey;
127
128 if (!realKey.equals(key)) {
fce0a73f 129 return new WLoginResult(true, false);
f433d153
NR
130 }
131
d11fb35b 132 // defaults are true (as previous versions without the feature)
f433d153
NR
133 boolean rw = true;
134 boolean wl = true;
d11fb35b 135 boolean bl = true;
f433d153 136
599b05c7
NR
137 rw = Instance.getInstance().getConfig().getBoolean(Config.SERVER_RW,
138 rw);
fce0a73f
NR
139
140 List<String> allowed = Instance.getInstance().getConfig().getList(
141 Config.SERVER_ALLOWED_SUBKEYS, new ArrayList<String>());
142
143 if (!allowed.isEmpty()) {
144 if (!allowed.contains(subkey)) {
145 return new WLoginResult(true, false);
146 }
147
148 if ((subkey + "|").contains("|rw|")) {
149 rw = true;
150 }
151 if ((subkey + "|").contains("|wl|")) {
152 wl = false; // |wl| = bypass whitelist
153 }
154 if ((subkey + "|").contains("|bl|")) {
155 bl = false; // |bl| = bypass blacklist
f433d153
NR
156 }
157 }
158
fce0a73f 159 return new WLoginResult(who, key, subkey, rw, wl, bl);
f433d153
NR
160 }
161
33d40b9f 162 @Override
fce0a73f 163 protected Response getList(String uri, WLoginResult login)
f433d153 164 throws IOException {
5ee0fc14 165 if (WebLibraryUrls.LIST_URL_METADATA.equals(uri)) {
f433d153 166 List<JSONObject> jsons = new ArrayList<JSONObject>();
d11fb35b 167 for (MetaData meta : metas(login)) {
f433d153
NR
168 jsons.add(JsonIO.toJson(meta));
169 }
170
171 return newInputStreamResponse("application/json",
599b05c7
NR
172 new ByteArrayInputStream(
173 new JSONArray(jsons).toString().getBytes()));
f433d153
NR
174 }
175
176 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
177 NanoHTTPD.MIME_PLAINTEXT, null);
178 }
179
f433d153
NR
180 // /story/luid/chapter/para <-- text/image
181 // /story/luid/cover <-- image
182 // /story/luid/metadata <-- json
c5103223 183 // /story/luid/json <-- json, whole chapter (no images)
33d40b9f
NR
184 @Override
185 protected Response getStoryPart(String uri, WLoginResult login) {
6673ec59 186 String[] uriParts = uri.split("/");
f433d153
NR
187 int off = 2;
188
6673ec59 189 if (uriParts.length < off + 2) {
f433d153
NR
190 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
191 NanoHTTPD.MIME_PLAINTEXT, null);
192 }
193
6673ec59
NR
194 String luid = uriParts[off + 0];
195 String chapterStr = uriParts[off + 1];
196 String imageStr = uriParts.length < off + 3 ? null : uriParts[off + 2];
f433d153
NR
197
198 // 1-based (0 = desc)
199 int chapter = 0;
200 if (chapterStr != null && !"cover".equals(chapterStr)
599b05c7
NR
201 && !"metadata".equals(chapterStr)
202 && !"json".equals(chapterStr)) {
f433d153
NR
203 try {
204 chapter = Integer.parseInt(chapterStr);
205 if (chapter < 0) {
206 throw new NumberFormatException();
207 }
208 } catch (NumberFormatException e) {
209 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
210 NanoHTTPD.MIME_PLAINTEXT, "Chapter is not valid");
211 }
212 }
213
214 // 1-based
215 int paragraph = 1;
216 if (imageStr != null) {
217 try {
218 paragraph = Integer.parseInt(imageStr);
219 if (paragraph < 0) {
220 throw new NumberFormatException();
221 }
222 } catch (NumberFormatException e) {
223 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
224 NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid");
225 }
226 }
227
228 String mimeType = NanoHTTPD.MIME_PLAINTEXT;
229 InputStream in = null;
230 try {
231 if ("cover".equals(chapterStr)) {
6673ec59 232 Image img = storyCover(luid, login);
f433d153
NR
233 if (img != null) {
234 in = img.newInputStream();
235 }
3f468ac7
NR
236 // TODO: get correct image type
237 mimeType = "image/png";
f433d153 238 } else if ("metadata".equals(chapterStr)) {
d11fb35b 239 MetaData meta = meta(luid, login);
f433d153
NR
240 JSONObject json = JsonIO.toJson(meta);
241 mimeType = "application/json";
242 in = new ByteArrayInputStream(json.toString().getBytes());
3fbc084c 243 } else if ("json".equals(chapterStr)) {
d11fb35b 244 Story story = story(luid, login);
c5103223
NR
245 JSONObject json = JsonIO.toJson(story);
246 mimeType = "application/json";
247 in = new ByteArrayInputStream(json.toString().getBytes());
f433d153 248 } else {
d11fb35b 249 Story story = story(luid, login);
f433d153
NR
250 if (story != null) {
251 if (chapter == 0) {
252 StringBuilder builder = new StringBuilder();
253 for (Paragraph p : story.getMeta().getResume()) {
254 if (builder.length() == 0) {
255 builder.append("\n");
256 }
257 builder.append(p.getContent());
258 }
259
599b05c7
NR
260 in = new ByteArrayInputStream(
261 builder.toString().getBytes("utf-8"));
f433d153
NR
262 } else {
263 Paragraph para = story.getChapters().get(chapter - 1)
264 .getParagraphs().get(paragraph - 1);
265 Image img = para.getContentImage();
266 if (para.getType() == ParagraphType.IMAGE) {
267 // TODO: get correct image type
268 mimeType = "image/png";
269 in = img.newInputStream();
270 } else {
599b05c7
NR
271 in = new ByteArrayInputStream(
272 para.getContent().getBytes("utf-8"));
f433d153
NR
273 }
274 }
275 }
276 }
277 } catch (IndexOutOfBoundsException e) {
278 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
279 NanoHTTPD.MIME_PLAINTEXT,
280 "Chapter or paragraph does not exist");
281 } catch (IOException e) {
282 Instance.getInstance().getTraceHandler()
283 .error(new IOException("Cannot get image: " + uri, e));
284 return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR,
285 NanoHTTPD.MIME_PLAINTEXT, "Error when processing request");
286 }
287
288 return newInputStreamResponse(mimeType, in);
289 }
290
089e354e
NR
291 // /story/luid/source
292 // /story/luid/title
293 // /story/luid/author
294 @Override
295 protected Response setStoryPart(String uri, String value,
296 WLoginResult login) throws IOException {
297 String[] uriParts = uri.split("/");
298 int off = 2; // "" and "story"
299
300 if (uriParts.length < off + 2) {
301 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
302 NanoHTTPD.MIME_PLAINTEXT, "Invalid story part request");
303 }
304
305 String luid = uriParts[off + 0];
306 String type = uriParts[off + 1];
307
308 if (!Arrays.asList("source", "title", "author").contains(type)) {
309 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
310 NanoHTTPD.MIME_PLAINTEXT,
311 "Invalid SET story part: " + type);
312 }
313
314 if (meta(luid, login) != null) {
315 BasicLibrary lib = Instance.getInstance().getLibrary();
316 if ("source".equals(type)) {
317 lib.changeSource(luid, value, null);
318 } else if ("title".equals(type)) {
319 lib.changeTitle(luid, value, null);
320 } else if ("author".equals(type)) {
321 lib.changeAuthor(luid, value, null);
322 }
323 }
324
325 return newInputStreamResponse(NanoHTTPD.MIME_PLAINTEXT, null);
326 }
327
6673ec59
NR
328 @Override
329 protected Response getCover(String uri, WLoginResult login)
330 throws IOException {
331 String[] uriParts = uri.split("/");
332 int off = 2; // "" and "cover"
333
334 if (uriParts.length < off + 2) {
335 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
336 NanoHTTPD.MIME_PLAINTEXT, "Invalid cover request");
337 }
338
339 String type = uriParts[off + 0];
340 String id = uriParts[off + 1];
341
342 InputStream in = null;
343
e4b1b70c 344 if ("story".equals(type)) {
6673ec59
NR
345 Image img = storyCover(id, login);
346 if (img != null) {
347 in = img.newInputStream();
348 }
e4b1b70c
NR
349 } else if ("source".equals(type)) {
350 Image img = sourceCover(id, login);
6673ec59
NR
351 if (img != null) {
352 in = img.newInputStream();
353 }
e4b1b70c
NR
354 } else if ("author".equals(type)) {
355 Image img = authorCover(id, login);
6673ec59
NR
356 if (img != null) {
357 in = img.newInputStream();
358 }
359 } else {
360 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
e4b1b70c
NR
361 NanoHTTPD.MIME_PLAINTEXT,
362 "Invalid GET cover type: " + type);
6673ec59
NR
363 }
364
365 // TODO: get correct image type
366 return newInputStreamResponse("image/png", in);
367 }
368
e4b1b70c
NR
369 @Override
370 protected Response setCover(String uri, String luid, WLoginResult login)
371 throws IOException {
372 String[] uriParts = uri.split("/");
373 int off = 2; // "" and "cover"
374
375 if (uriParts.length < off + 2) {
376 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
377 NanoHTTPD.MIME_PLAINTEXT, "Invalid cover request");
378 }
379
380 String type = uriParts[off + 0];
381 String id = uriParts[off + 1];
382
383 if ("source".equals(type)) {
384 sourceCover(id, login, luid);
385 } else if ("author".equals(type)) {
386 authorCover(id, login, luid);
387 } else {
388 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
389 NanoHTTPD.MIME_PLAINTEXT,
390 "Invalid SET cover type: " + type);
391 }
392
393 return newInputStreamResponse(NanoHTTPD.MIME_PLAINTEXT, null);
394 }
395
33d40b9f
NR
396 @Override
397 protected List<MetaData> metas(WLoginResult login) throws IOException {
d11fb35b
NR
398 BasicLibrary lib = Instance.getInstance().getLibrary();
399 List<MetaData> metas = new ArrayList<MetaData>();
400 for (MetaData meta : lib.getList().getMetas()) {
401 if (isAllowed(meta, login)) {
402 metas.add(meta);
403 }
404 }
405
406 return metas;
407 }
408
f433d153 409 // NULL if not whitelist OK or if not found
33d40b9f
NR
410 @Override
411 protected Story story(String luid, WLoginResult login) throws IOException {
f433d153
NR
412 synchronized (storyCache) {
413 if (storyCache.containsKey(luid)) {
414 Story story = storyCache.get(luid);
d11fb35b 415 if (!isAllowed(story.getMeta(), login))
f433d153 416 return null;
f433d153
NR
417
418 return story;
419 }
420 }
421
422 Story story = null;
d11fb35b 423 MetaData meta = meta(luid, login);
f433d153
NR
424 if (meta != null) {
425 BasicLibrary lib = Instance.getInstance().getLibrary();
426 story = lib.getStory(luid, null);
427 long size = sizeOf(story);
428
429 synchronized (storyCache) {
430 // Could have been added by another request
431 if (!storyCache.containsKey(luid)) {
432 while (!storyCacheOrder.isEmpty()
433 && storyCacheSize + size > maxStoryCacheSize) {
434 String oldestLuid = storyCacheOrder.removeFirst();
435 Story oldestStory = storyCache.remove(oldestLuid);
436 maxStoryCacheSize -= sizeOf(oldestStory);
437 }
438
439 storyCacheOrder.add(luid);
440 storyCache.put(luid, story);
441 }
442 }
443 }
444
445 return story;
446 }
447
33d40b9f
NR
448 private MetaData meta(String luid, WLoginResult login) throws IOException {
449 BasicLibrary lib = Instance.getInstance().getLibrary();
450 MetaData meta = lib.getInfo(luid);
451 if (!isAllowed(meta, login))
452 return null;
f433d153 453
33d40b9f 454 return meta;
f433d153
NR
455 }
456
6673ec59
NR
457 private Image storyCover(String luid, WLoginResult login)
458 throws IOException {
33d40b9f
NR
459 MetaData meta = meta(luid, login);
460 if (meta != null) {
461 BasicLibrary lib = Instance.getInstance().getLibrary();
462 return lib.getCover(meta.getLuid());
f433d153 463 }
f433d153 464
33d40b9f 465 return null;
f433d153
NR
466 }
467
6673ec59
NR
468 private Image authorCover(String author, WLoginResult login)
469 throws IOException {
470 Image img = null;
471
472 List<MetaData> metas = new MetaResultList(metas(login)).filter(null,
473 author, null);
474 if (metas.size() > 0) {
475 BasicLibrary lib = Instance.getInstance().getLibrary();
476 img = lib.getCustomAuthorCover(author);
477 if (img == null)
478 img = lib.getCover(metas.get(0).getLuid());
479 }
480
481 return img;
482
483 }
484
e4b1b70c
NR
485 private void authorCover(String author, WLoginResult login, String luid)
486 throws IOException {
487 if (meta(luid, login) != null) {
488 List<MetaData> metas = new MetaResultList(metas(login)).filter(null,
489 author, null);
490 if (metas.size() > 0) {
491 BasicLibrary lib = Instance.getInstance().getLibrary();
492 lib.setAuthorCover(author, luid);
493 }
494 }
495 }
496
6673ec59
NR
497 private Image sourceCover(String source, WLoginResult login)
498 throws IOException {
499 Image img = null;
500
501 List<MetaData> metas = new MetaResultList(metas(login)).filter(source,
502 null, null);
503 if (metas.size() > 0) {
504 BasicLibrary lib = Instance.getInstance().getLibrary();
505 img = lib.getCustomSourceCover(source);
506 if (img == null)
507 img = lib.getCover(metas.get(0).getLuid());
508 }
509
510 return img;
511 }
512
e4b1b70c
NR
513 private void sourceCover(String source, WLoginResult login, String luid)
514 throws IOException {
515 if (meta(luid, login) != null) {
516 List<MetaData> metas = new MetaResultList(metas(login))
517 .filter(source, null, null);
518 if (metas.size() > 0) {
519 BasicLibrary lib = Instance.getInstance().getLibrary();
520 lib.setSourceCover(source, luid);
521 }
522 }
523 }
524
33d40b9f 525 private boolean isAllowed(MetaData meta, WLoginResult login) {
5c4ce687
NR
526 MetaResultList one = new MetaResultList(Arrays.asList(meta));
527 if (login.isWl() && !whitelist.isEmpty()) {
528 if (one.filter(whitelist, null, null).isEmpty()) {
529 return false;
530 }
f433d153 531 }
5c4ce687
NR
532 if (login.isBl() && !blacklist.isEmpty()) {
533 if (!one.filter(blacklist, null, null).isEmpty()) {
534 return false;
535 }
599b05c7
NR
536 }
537
33d40b9f 538 return true;
599b05c7
NR
539 }
540
33d40b9f
NR
541 private long sizeOf(Story story) {
542 long size = 0;
543 for (Chapter chap : story) {
544 for (Paragraph para : chap) {
545 if (para.getType() == ParagraphType.IMAGE) {
546 size += para.getContentImage().getSize();
547 } else {
548 size += para.getContent().length();
549 }
550 }
6b89e45c
NR
551 }
552
33d40b9f 553 return size;
6b89e45c
NR
554 }
555
3fbc084c
NR
556 public static void main(String[] args) throws IOException {
557 Instance.init();
558 WebLibraryServer web = new WebLibraryServer(false);
559 web.run();
560 }
f433d153 561}