1 package be
.nikiroo
.fanfix
.library
;
3 import java
.io
.ByteArrayInputStream
;
4 import java
.io
.IOException
;
5 import java
.io
.InputStream
;
7 import java
.util
.ArrayList
;
8 import java
.util
.Arrays
;
9 import java
.util
.HashMap
;
10 import java
.util
.LinkedList
;
11 import java
.util
.List
;
14 import org
.json
.JSONArray
;
15 import org
.json
.JSONObject
;
17 import be
.nikiroo
.fanfix
.Instance
;
18 import be
.nikiroo
.fanfix
.bundles
.Config
;
19 import be
.nikiroo
.fanfix
.data
.Chapter
;
20 import be
.nikiroo
.fanfix
.data
.JsonIO
;
21 import be
.nikiroo
.fanfix
.data
.MetaData
;
22 import be
.nikiroo
.fanfix
.data
.Paragraph
;
23 import be
.nikiroo
.fanfix
.data
.Paragraph
.ParagraphType
;
24 import be
.nikiroo
.fanfix
.data
.Story
;
25 import be
.nikiroo
.utils
.Image
;
26 import be
.nikiroo
.utils
.LoginResult
;
27 import be
.nikiroo
.utils
.NanoHTTPD
;
28 import be
.nikiroo
.utils
.NanoHTTPD
.Response
;
29 import be
.nikiroo
.utils
.NanoHTTPD
.Response
.Status
;
30 import be
.nikiroo
.utils
.Progress
;
32 public class WebLibraryServer
extends WebLibraryServerHtml
{
33 class WLoginResult
extends LoginResult
{
34 public WLoginResult(boolean badLogin
, boolean badCookie
) {
35 super(badLogin
, badCookie
);
38 public WLoginResult(String who
, String key
, String subkey
, boolean rw
,
39 boolean wl
, boolean bl
) {
40 super(who
, key
, subkey
, (rw ?
"|rw" : "") + (wl ?
"|wl" : "")
41 + (bl ?
"|bl" : "") + "|");
44 public WLoginResult(String cookie
, String who
, String key
,
45 List
<String
> subkeys
) {
46 super(cookie
, who
, key
, subkeys
,
47 subkeys
== null || subkeys
.isEmpty());
50 public boolean isRw() {
51 return getOption().contains("|rw|");
54 public boolean isWl() {
55 return getOption().contains("|wl|");
58 public boolean isBl() {
59 return getOption().contains("|bl|");
63 private Map
<String
, Story
> storyCache
= new HashMap
<String
, Story
>();
64 private LinkedList
<String
> storyCacheOrder
= new LinkedList
<String
>();
65 private long storyCacheSize
= 0;
66 private long maxStoryCacheSize
;
68 private List
<String
> whitelist
;
69 private List
<String
> blacklist
;
71 private Map
<String
, Progress
> imprts
= new HashMap
<String
, Progress
>();
73 private boolean exiting
;
75 public WebLibraryServer(boolean secure
) throws IOException
{
78 int cacheMb
= Instance
.getInstance().getConfig()
79 .getInteger(Config
.SERVER_MAX_CACHE_MB
, 100);
80 maxStoryCacheSize
= cacheMb
* 1024 * 1024;
82 setTraceHandler(Instance
.getInstance().getTraceHandler());
84 whitelist
= Instance
.getInstance().getConfig()
85 .getList(Config
.SERVER_WHITELIST
, new ArrayList
<String
>());
86 blacklist
= Instance
.getInstance().getConfig()
87 .getList(Config
.SERVER_BLACKLIST
, new ArrayList
<String
>());
91 * Start the server (listen on the network for new connections).
93 * Can only be called once.
95 * This call is asynchronous, and will just start a new {@link Thread} on
96 * itself (see {@link WebLibraryServer#run()}).
99 new Thread(this).start();
103 protected Response
stop(WLoginResult login
) {
105 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
106 NanoHTTPD
.MIME_PLAINTEXT
, "Exit not allowed");
110 return NanoHTTPD
.newFixedLengthResponse(Status
.SERVICE_UNAVAILABLE
,
111 NanoHTTPD
.MIME_PLAINTEXT
, "Server is already exiting...");
115 Instance
.getInstance().getTraceHandler().trace("Exiting");
119 synchronized (imprts
) {
120 ok
= imprts
.isEmpty();
125 } catch (InterruptedException e
) {
126 Instance
.getInstance().getTraceHandler()
127 .trace("Waiting to exit...");
134 new Thread(new Runnable() {
139 } catch (InterruptedException e
) {
142 Instance
.getInstance().getTraceHandler()
143 .trace("Exit timeout: force-quit");
146 }, "Exit program after timeout of 1500 ms").start();
148 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
149 NanoHTTPD
.MIME_PLAINTEXT
, "Exited");
153 protected WLoginResult
login(boolean badLogin
, boolean badCookie
) {
154 return new WLoginResult(false, false);
158 protected WLoginResult
login(String who
, String cookie
) {
159 List
<String
> subkeys
= Instance
.getInstance().getConfig()
160 .getList(Config
.SERVER_ALLOWED_SUBKEYS
);
161 String realKey
= Instance
.getInstance().getConfig()
162 .getString(Config
.SERVER_KEY
);
164 return new WLoginResult(cookie
, who
, realKey
, subkeys
);
169 protected WLoginResult
login(String who
, String key
, String subkey
) {
170 String realKey
= Instance
.getInstance().getConfig()
171 .getString(Config
.SERVER_KEY
, "");
173 // I don't like NULLs...
174 key
= key
== null ?
"" : key
;
175 subkey
= subkey
== null ?
"" : subkey
;
177 if (!realKey
.equals(key
)) {
178 return new WLoginResult(true, false);
181 // defaults are true (as previous versions without the feature)
186 rw
= Instance
.getInstance().getConfig().getBoolean(Config
.SERVER_RW
,
189 List
<String
> allowed
= Instance
.getInstance().getConfig().getList(
190 Config
.SERVER_ALLOWED_SUBKEYS
, new ArrayList
<String
>());
192 if (!allowed
.isEmpty()) {
193 if (!allowed
.contains(subkey
)) {
194 return new WLoginResult(true, false);
197 if ((subkey
+ "|").contains("|rw|")) {
200 if ((subkey
+ "|").contains("|wl|")) {
201 wl
= false; // |wl| = bypass whitelist
203 if ((subkey
+ "|").contains("|bl|")) {
204 bl
= false; // |bl| = bypass blacklist
208 return new WLoginResult(who
, key
, subkey
, rw
, wl
, bl
);
212 protected Response
getList(String uri
, WLoginResult login
)
214 if (WebLibraryUrls
.LIST_URL_METADATA
.equals(uri
)) {
215 List
<JSONObject
> jsons
= new ArrayList
<JSONObject
>();
216 for (MetaData meta
: metas(login
)) {
217 jsons
.add(JsonIO
.toJson(meta
));
220 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
221 "application/json", new JSONArray(jsons
).toString());
224 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
225 NanoHTTPD
.MIME_PLAINTEXT
, null);
228 // /story/luid/chapter/para <-- text/image
229 // /story/luid/cover <-- image
230 // /story/luid/metadata <-- json
231 // /story/luid/json <-- json, whole chapter (no images)
233 protected Response
getStoryPart(String uri
, WLoginResult login
) {
234 String
[] uriParts
= uri
.split("/");
237 if (uriParts
.length
< off
+ 2) {
238 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
239 NanoHTTPD
.MIME_PLAINTEXT
, null);
242 String luid
= uriParts
[off
+ 0];
243 String chapterStr
= uriParts
[off
+ 1];
244 String imageStr
= uriParts
.length
< off
+ 3 ?
null : uriParts
[off
+ 2];
246 // 1-based (0 = desc)
248 if (chapterStr
!= null && !"cover".equals(chapterStr
)
249 && !"metadata".equals(chapterStr
)
250 && !"json".equals(chapterStr
)) {
252 chapter
= Integer
.parseInt(chapterStr
);
254 throw new NumberFormatException();
256 } catch (NumberFormatException e
) {
257 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
258 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
264 if (imageStr
!= null) {
266 paragraph
= Integer
.parseInt(imageStr
);
268 throw new NumberFormatException();
270 } catch (NumberFormatException e
) {
271 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
272 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
276 String mimeType
= NanoHTTPD
.MIME_PLAINTEXT
;
277 InputStream in
= null;
279 if ("cover".equals(chapterStr
)) {
280 Image img
= storyCover(luid
, login
);
282 in
= img
.newInputStream();
284 // TODO: get correct image type
285 mimeType
= "image/png";
286 } else if ("metadata".equals(chapterStr
)) {
287 MetaData meta
= meta(luid
, login
);
288 JSONObject json
= JsonIO
.toJson(meta
);
289 mimeType
= "application/json";
290 in
= new ByteArrayInputStream(json
.toString().getBytes());
291 } else if ("json".equals(chapterStr
)) {
292 Story story
= story(luid
, login
);
293 JSONObject json
= JsonIO
.toJson(story
);
294 mimeType
= "application/json";
295 in
= new ByteArrayInputStream(json
.toString().getBytes());
297 Story story
= story(luid
, login
);
300 StringBuilder builder
= new StringBuilder();
301 for (Paragraph p
: story
.getMeta().getResume()) {
302 if (builder
.length() == 0) {
303 builder
.append("\n");
305 builder
.append(p
.getContent());
308 in
= new ByteArrayInputStream(
309 builder
.toString().getBytes("utf-8"));
311 Paragraph para
= story
.getChapters().get(chapter
- 1)
312 .getParagraphs().get(paragraph
- 1);
313 Image img
= para
.getContentImage();
314 if (para
.getType() == ParagraphType
.IMAGE
) {
315 // TODO: get correct image type
316 mimeType
= "image/png";
317 in
= img
.newInputStream();
319 in
= new ByteArrayInputStream(
320 para
.getContent().getBytes("utf-8"));
325 } catch (IndexOutOfBoundsException e
) {
326 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
327 NanoHTTPD
.MIME_PLAINTEXT
,
328 "Chapter or paragraph does not exist");
329 } catch (IOException e
) {
330 Instance
.getInstance().getTraceHandler()
331 .error(new IOException("Cannot get image: " + uri
, e
));
332 return NanoHTTPD
.newFixedLengthResponse(Status
.INTERNAL_ERROR
,
333 NanoHTTPD
.MIME_PLAINTEXT
, "Error when processing request");
336 return newInputStreamResponse(mimeType
, in
);
339 // /story/luid/source
341 // /story/luid/author
343 protected Response
setStoryPart(String uri
, String value
,
344 WLoginResult login
) throws IOException
{
345 String
[] uriParts
= uri
.split("/");
346 int off
= 2; // "" and "story"
348 if (uriParts
.length
< off
+ 2) {
349 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
350 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid story part request");
354 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
355 NanoHTTPD
.MIME_PLAINTEXT
, "SET story part not allowed");
359 return NanoHTTPD
.newFixedLengthResponse(Status
.SERVICE_UNAVAILABLE
,
360 NanoHTTPD
.MIME_PLAINTEXT
, "Server is exiting...");
363 String luid
= uriParts
[off
+ 0];
364 String type
= uriParts
[off
+ 1];
366 if (!Arrays
.asList("source", "title", "author").contains(type
)) {
367 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
368 NanoHTTPD
.MIME_PLAINTEXT
,
369 "Invalid SET story part: " + type
);
372 if (meta(luid
, login
) != null) {
373 BasicLibrary lib
= Instance
.getInstance().getLibrary();
374 if ("source".equals(type
)) {
375 lib
.changeSource(luid
, value
, null);
376 } else if ("title".equals(type
)) {
377 lib
.changeTitle(luid
, value
, null);
378 } else if ("author".equals(type
)) {
379 lib
.changeAuthor(luid
, value
, null);
383 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
387 protected Response
getCover(String uri
, WLoginResult login
)
389 String
[] uriParts
= uri
.split("/");
391 int off
= 2; // "" and "cover"
392 for (int i
= 0; i
< off
; i
++) {
393 startAt
+= uriParts
[i
].length() + "/".length();
396 if (uriParts
.length
< off
+ 2) {
397 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
398 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
401 String type
= uriParts
[off
+ 0];
402 String id
= uri
.substring(startAt
+ type
.length() + "/".length());
404 InputStream in
= null;
406 if ("story".equals(type
)) {
407 Image img
= storyCover(id
, login
);
409 in
= img
.newInputStream();
411 } else if ("source".equals(type
)) {
412 Image img
= sourceCover(id
, login
);
414 in
= img
.newInputStream();
416 } else if ("author".equals(type
)) {
417 Image img
= authorCover(id
, login
);
419 in
= img
.newInputStream();
422 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
423 NanoHTTPD
.MIME_PLAINTEXT
,
424 "Invalid GET cover type: " + type
);
427 // TODO: get correct image type
428 return newInputStreamResponse("image/png", in
);
432 protected Response
setCover(String uri
, String luid
, WLoginResult login
)
434 String
[] uriParts
= uri
.split("/");
435 int off
= 2; // "" and "cover"
437 for (int i
= 0; i
< off
; i
++) {
438 startAt
+= uriParts
[i
].length() + "/".length();
441 if (uriParts
.length
< off
+ 2) {
442 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
443 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
447 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
448 NanoHTTPD
.MIME_PLAINTEXT
, "Cover request not allowed");
452 return NanoHTTPD
.newFixedLengthResponse(Status
.SERVICE_UNAVAILABLE
,
453 NanoHTTPD
.MIME_PLAINTEXT
, "Server is exiting...");
456 String type
= uriParts
[off
+ 0];
457 String id
= uri
.substring(startAt
+ type
.length() + "/".length());
459 if ("source".equals(type
)) {
460 sourceCover(id
, login
, luid
);
461 } else if ("author".equals(type
)) {
462 authorCover(id
, login
, luid
);
464 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
465 NanoHTTPD
.MIME_PLAINTEXT
,
466 "Invalid SET cover type: " + type
);
469 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
473 protected Response
imprt(String uri
, String urlStr
, WLoginResult login
)
475 final BasicLibrary lib
= Instance
.getInstance().getLibrary();
478 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
479 NanoHTTPD
.MIME_PLAINTEXT
, "Import not allowed");
483 return NanoHTTPD
.newFixedLengthResponse(Status
.SERVICE_UNAVAILABLE
,
484 NanoHTTPD
.MIME_PLAINTEXT
, "Server is exiting...");
487 final URL url
= new URL(urlStr
);
488 final Progress pg
= new Progress();
489 final String luid
= lib
.getNextId();
491 synchronized (imprts
) {
492 imprts
.put(luid
, pg
);
495 new Thread(new Runnable() {
499 lib
.imprt(url
, luid
, pg
);
500 } catch (IOException e
) {
501 Instance
.getInstance().getTraceHandler().error(e
);
503 synchronized (imprts
) {
508 }, "Import story: " + urlStr
).start();
510 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
511 NanoHTTPD
.MIME_PLAINTEXT
, luid
);
515 protected Response
imprtProgress(String uri
, WLoginResult login
) {
516 String
[] uriParts
= uri
.split("/");
517 int off
= 2; // "" and "import"
519 if (uriParts
.length
< off
+ 1) {
520 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
521 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
524 String luid
= uriParts
[off
+ 0];
527 synchronized (imprts
) {
528 pg
= imprts
.get(luid
);
531 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
532 "application/json", JsonIO
.toJson(pg
).toString());
535 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
539 protected Response
delete(String uri
, WLoginResult login
)
541 String
[] uriParts
= uri
.split("/");
542 int off
= 2; // "" and "delete"
544 if (uriParts
.length
< off
+ 1) {
545 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
546 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid delete request");
550 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
551 NanoHTTPD
.MIME_PLAINTEXT
, "Delete not allowed");
555 return NanoHTTPD
.newFixedLengthResponse(Status
.SERVICE_UNAVAILABLE
,
556 NanoHTTPD
.MIME_PLAINTEXT
, "Server is exiting...");
559 String luid
= uriParts
[off
+ 0];
561 BasicLibrary lib
= Instance
.getInstance().getLibrary();
564 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
568 protected List
<MetaData
> metas(WLoginResult login
) throws IOException
{
569 BasicLibrary lib
= Instance
.getInstance().getLibrary();
570 List
<MetaData
> metas
= new ArrayList
<MetaData
>();
571 for (MetaData meta
: lib
.getList().getMetas()) {
572 if (isAllowed(meta
, login
)) {
580 // NULL if not whitelist OK or if not found
582 protected Story
story(String luid
, WLoginResult login
) throws IOException
{
583 synchronized (storyCache
) {
584 if (storyCache
.containsKey(luid
)) {
585 Story story
= storyCache
.get(luid
);
586 if (!isAllowed(story
.getMeta(), login
))
594 MetaData meta
= meta(luid
, login
);
596 BasicLibrary lib
= Instance
.getInstance().getLibrary();
597 story
= lib
.getStory(luid
, null);
598 long size
= sizeOf(story
);
600 synchronized (storyCache
) {
601 // Could have been added by another request
602 if (!storyCache
.containsKey(luid
)) {
603 while (!storyCacheOrder
.isEmpty()
604 && storyCacheSize
+ size
> maxStoryCacheSize
) {
605 String oldestLuid
= storyCacheOrder
.removeFirst();
606 Story oldestStory
= storyCache
.remove(oldestLuid
);
607 maxStoryCacheSize
-= sizeOf(oldestStory
);
610 storyCacheOrder
.add(luid
);
611 storyCache
.put(luid
, story
);
619 private MetaData
meta(String luid
, WLoginResult login
) throws IOException
{
620 BasicLibrary lib
= Instance
.getInstance().getLibrary();
621 MetaData meta
= lib
.getInfo(luid
);
622 if (!isAllowed(meta
, login
))
628 private Image
storyCover(String luid
, WLoginResult login
)
630 MetaData meta
= meta(luid
, login
);
632 BasicLibrary lib
= Instance
.getInstance().getLibrary();
633 return lib
.getCover(meta
.getLuid());
639 private Image
authorCover(String author
, WLoginResult login
)
643 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(null,
645 if (metas
.size() > 0) {
646 BasicLibrary lib
= Instance
.getInstance().getLibrary();
647 img
= lib
.getCustomAuthorCover(author
);
649 img
= lib
.getCover(metas
.get(0).getLuid());
656 private void authorCover(String author
, WLoginResult login
, String luid
)
658 if (meta(luid
, login
) != null) {
659 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(null,
661 if (metas
.size() > 0) {
662 BasicLibrary lib
= Instance
.getInstance().getLibrary();
663 lib
.setAuthorCover(author
, luid
);
668 private Image
sourceCover(String source
, WLoginResult login
)
672 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(source
,
674 if (metas
.size() > 0) {
675 BasicLibrary lib
= Instance
.getInstance().getLibrary();
676 img
= lib
.getCustomSourceCover(source
);
678 img
= lib
.getCover(metas
.get(0).getLuid());
684 private void sourceCover(String source
, WLoginResult login
, String luid
)
686 if (meta(luid
, login
) != null) {
687 List
<MetaData
> metas
= new MetaResultList(metas(login
))
688 .filter(source
, null, null);
689 if (metas
.size() > 0) {
690 BasicLibrary lib
= Instance
.getInstance().getLibrary();
691 lib
.setSourceCover(source
, luid
);
696 private boolean isAllowed(MetaData meta
, WLoginResult login
) {
697 MetaResultList one
= new MetaResultList(Arrays
.asList(meta
));
698 if (login
.isWl() && !whitelist
.isEmpty()) {
699 if (one
.filter(whitelist
, null, null).isEmpty()) {
703 if (login
.isBl() && !blacklist
.isEmpty()) {
704 if (!one
.filter(blacklist
, null, null).isEmpty()) {
712 private long sizeOf(Story story
) {
714 for (Chapter chap
: story
) {
715 for (Paragraph para
: chap
) {
716 if (para
.getType() == ParagraphType
.IMAGE
) {
717 size
+= para
.getContentImage().getSize();
719 size
+= para
.getContent().length();
727 public static void main(String
[] args
) throws IOException
{
729 WebLibraryServer web
= new WebLibraryServer(false);