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
;
31 import be
.nikiroo
.utils
.Progress
.ProgressListener
;
33 public class WebLibraryServer
extends WebLibraryServerHtml
{
34 class WLoginResult
extends LoginResult
{
35 public WLoginResult(boolean badLogin
, boolean badCookie
) {
36 super(badLogin
, badCookie
);
39 public WLoginResult(String who
, String key
, String subkey
, boolean rw
,
40 boolean wl
, boolean bl
) {
41 super(who
, key
, subkey
, (rw ?
"|rw" : "") + (wl ?
"|wl" : "")
42 + (bl ?
"|bl" : "") + "|");
45 public WLoginResult(String cookie
, String who
, String key
,
46 List
<String
> subkeys
) {
47 super(cookie
, who
, key
, subkeys
,
48 subkeys
== null || subkeys
.isEmpty());
51 public boolean isRw() {
52 return getOption().contains("|rw|");
55 public boolean isWl() {
56 return getOption().contains("|wl|");
59 public boolean isBl() {
60 return getOption().contains("|bl|");
64 private Map
<String
, Story
> storyCache
= new HashMap
<String
, Story
>();
65 private LinkedList
<String
> storyCacheOrder
= new LinkedList
<String
>();
66 private long storyCacheSize
= 0;
67 private long maxStoryCacheSize
;
69 private List
<String
> whitelist
;
70 private List
<String
> blacklist
;
72 private Map
<String
, Progress
> imprts
= new HashMap
<String
, Progress
>();
74 public WebLibraryServer(boolean secure
) throws IOException
{
77 int cacheMb
= Instance
.getInstance().getConfig()
78 .getInteger(Config
.SERVER_MAX_CACHE_MB
, 100);
79 maxStoryCacheSize
= cacheMb
* 1024 * 1024;
81 setTraceHandler(Instance
.getInstance().getTraceHandler());
83 whitelist
= Instance
.getInstance().getConfig()
84 .getList(Config
.SERVER_WHITELIST
, new ArrayList
<String
>());
85 blacklist
= Instance
.getInstance().getConfig()
86 .getList(Config
.SERVER_BLACKLIST
, new ArrayList
<String
>());
90 * Start the server (listen on the network for new connections).
92 * Can only be called once.
94 * This call is asynchronous, and will just start a new {@link Thread} on
95 * itself (see {@link WebLibraryServer#run()}).
98 new Thread(this).start();
102 protected WLoginResult
login(boolean badLogin
, boolean badCookie
) {
103 return new WLoginResult(false, false);
107 protected WLoginResult
login(String who
, String cookie
) {
108 List
<String
> subkeys
= Instance
.getInstance().getConfig()
109 .getList(Config
.SERVER_ALLOWED_SUBKEYS
);
110 String realKey
= Instance
.getInstance().getConfig()
111 .getString(Config
.SERVER_KEY
);
113 return new WLoginResult(cookie
, who
, realKey
, subkeys
);
118 protected WLoginResult
login(String who
, String key
, String subkey
) {
119 String realKey
= Instance
.getInstance().getConfig()
120 .getString(Config
.SERVER_KEY
, "");
122 // I don't like NULLs...
123 key
= key
== null ?
"" : key
;
124 subkey
= subkey
== null ?
"" : subkey
;
126 if (!realKey
.equals(key
)) {
127 return new WLoginResult(true, false);
130 // defaults are true (as previous versions without the feature)
135 rw
= Instance
.getInstance().getConfig().getBoolean(Config
.SERVER_RW
,
138 List
<String
> allowed
= Instance
.getInstance().getConfig().getList(
139 Config
.SERVER_ALLOWED_SUBKEYS
, new ArrayList
<String
>());
141 if (!allowed
.isEmpty()) {
142 if (!allowed
.contains(subkey
)) {
143 return new WLoginResult(true, false);
146 if ((subkey
+ "|").contains("|rw|")) {
149 if ((subkey
+ "|").contains("|wl|")) {
150 wl
= false; // |wl| = bypass whitelist
152 if ((subkey
+ "|").contains("|bl|")) {
153 bl
= false; // |bl| = bypass blacklist
157 return new WLoginResult(who
, key
, subkey
, rw
, wl
, bl
);
161 protected Response
getList(String uri
, WLoginResult login
)
163 if (WebLibraryUrls
.LIST_URL_METADATA
.equals(uri
)) {
164 List
<JSONObject
> jsons
= new ArrayList
<JSONObject
>();
165 for (MetaData meta
: metas(login
)) {
166 jsons
.add(JsonIO
.toJson(meta
));
169 return newInputStreamResponse("application/json",
170 new ByteArrayInputStream(
171 new JSONArray(jsons
).toString().getBytes()));
174 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
175 NanoHTTPD
.MIME_PLAINTEXT
, null);
178 // /story/luid/chapter/para <-- text/image
179 // /story/luid/cover <-- image
180 // /story/luid/metadata <-- json
181 // /story/luid/json <-- json, whole chapter (no images)
183 protected Response
getStoryPart(String uri
, WLoginResult login
) {
184 String
[] uriParts
= uri
.split("/");
187 if (uriParts
.length
< off
+ 2) {
188 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
189 NanoHTTPD
.MIME_PLAINTEXT
, null);
192 String luid
= uriParts
[off
+ 0];
193 String chapterStr
= uriParts
[off
+ 1];
194 String imageStr
= uriParts
.length
< off
+ 3 ?
null : uriParts
[off
+ 2];
196 // 1-based (0 = desc)
198 if (chapterStr
!= null && !"cover".equals(chapterStr
)
199 && !"metadata".equals(chapterStr
)
200 && !"json".equals(chapterStr
)) {
202 chapter
= Integer
.parseInt(chapterStr
);
204 throw new NumberFormatException();
206 } catch (NumberFormatException e
) {
207 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
208 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
214 if (imageStr
!= null) {
216 paragraph
= Integer
.parseInt(imageStr
);
218 throw new NumberFormatException();
220 } catch (NumberFormatException e
) {
221 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
222 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
226 String mimeType
= NanoHTTPD
.MIME_PLAINTEXT
;
227 InputStream in
= null;
229 if ("cover".equals(chapterStr
)) {
230 Image img
= storyCover(luid
, login
);
232 in
= img
.newInputStream();
234 // TODO: get correct image type
235 mimeType
= "image/png";
236 } else if ("metadata".equals(chapterStr
)) {
237 MetaData meta
= meta(luid
, login
);
238 JSONObject json
= JsonIO
.toJson(meta
);
239 mimeType
= "application/json";
240 in
= new ByteArrayInputStream(json
.toString().getBytes());
241 } else if ("json".equals(chapterStr
)) {
242 Story story
= story(luid
, login
);
243 JSONObject json
= JsonIO
.toJson(story
);
244 mimeType
= "application/json";
245 in
= new ByteArrayInputStream(json
.toString().getBytes());
247 Story story
= story(luid
, login
);
250 StringBuilder builder
= new StringBuilder();
251 for (Paragraph p
: story
.getMeta().getResume()) {
252 if (builder
.length() == 0) {
253 builder
.append("\n");
255 builder
.append(p
.getContent());
258 in
= new ByteArrayInputStream(
259 builder
.toString().getBytes("utf-8"));
261 Paragraph para
= story
.getChapters().get(chapter
- 1)
262 .getParagraphs().get(paragraph
- 1);
263 Image img
= para
.getContentImage();
264 if (para
.getType() == ParagraphType
.IMAGE
) {
265 // TODO: get correct image type
266 mimeType
= "image/png";
267 in
= img
.newInputStream();
269 in
= new ByteArrayInputStream(
270 para
.getContent().getBytes("utf-8"));
275 } catch (IndexOutOfBoundsException e
) {
276 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
277 NanoHTTPD
.MIME_PLAINTEXT
,
278 "Chapter or paragraph does not exist");
279 } catch (IOException e
) {
280 Instance
.getInstance().getTraceHandler()
281 .error(new IOException("Cannot get image: " + uri
, e
));
282 return NanoHTTPD
.newFixedLengthResponse(Status
.INTERNAL_ERROR
,
283 NanoHTTPD
.MIME_PLAINTEXT
, "Error when processing request");
286 return newInputStreamResponse(mimeType
, in
);
289 // /story/luid/source
291 // /story/luid/author
293 protected Response
setStoryPart(String uri
, String value
,
294 WLoginResult login
) throws IOException
{
295 String
[] uriParts
= uri
.split("/");
296 int off
= 2; // "" and "story"
298 if (uriParts
.length
< off
+ 2) {
299 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
300 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid story part request");
304 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
305 NanoHTTPD
.MIME_PLAINTEXT
, "SET story part not allowed");
308 String luid
= uriParts
[off
+ 0];
309 String type
= uriParts
[off
+ 1];
311 if (!Arrays
.asList("source", "title", "author").contains(type
)) {
312 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
313 NanoHTTPD
.MIME_PLAINTEXT
,
314 "Invalid SET story part: " + type
);
317 if (meta(luid
, login
) != null) {
318 BasicLibrary lib
= Instance
.getInstance().getLibrary();
319 if ("source".equals(type
)) {
320 lib
.changeSource(luid
, value
, null);
321 } else if ("title".equals(type
)) {
322 lib
.changeTitle(luid
, value
, null);
323 } else if ("author".equals(type
)) {
324 lib
.changeAuthor(luid
, value
, null);
328 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
332 protected Response
getCover(String uri
, WLoginResult login
)
334 String
[] uriParts
= uri
.split("/");
335 int off
= 2; // "" and "cover"
337 if (uriParts
.length
< off
+ 2) {
338 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
339 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
342 String type
= uriParts
[off
+ 0];
343 String id
= uriParts
[off
+ 1];
345 InputStream in
= null;
347 if ("story".equals(type
)) {
348 Image img
= storyCover(id
, login
);
350 in
= img
.newInputStream();
352 } else if ("source".equals(type
)) {
353 Image img
= sourceCover(id
, login
);
355 in
= img
.newInputStream();
357 } else if ("author".equals(type
)) {
358 Image img
= authorCover(id
, login
);
360 in
= img
.newInputStream();
363 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
364 NanoHTTPD
.MIME_PLAINTEXT
,
365 "Invalid GET cover type: " + type
);
368 // TODO: get correct image type
369 return newInputStreamResponse("image/png", in
);
373 protected Response
setCover(String uri
, String luid
, WLoginResult login
)
375 String
[] uriParts
= uri
.split("/");
376 int off
= 2; // "" and "cover"
378 if (uriParts
.length
< off
+ 2) {
379 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
380 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
384 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
385 NanoHTTPD
.MIME_PLAINTEXT
, "Cover request not allowed");
388 String type
= uriParts
[off
+ 0];
389 String id
= uriParts
[off
+ 1];
391 if ("source".equals(type
)) {
392 sourceCover(id
, login
, luid
);
393 } else if ("author".equals(type
)) {
394 authorCover(id
, login
, luid
);
396 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
397 NanoHTTPD
.MIME_PLAINTEXT
,
398 "Invalid SET cover type: " + type
);
401 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
405 protected Response
imprt(String uri
, String urlStr
, WLoginResult login
)
407 final BasicLibrary lib
= Instance
.getInstance().getLibrary();
410 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
411 NanoHTTPD
.MIME_PLAINTEXT
, "Import not allowed");
414 final URL url
= new URL(urlStr
);
415 final Progress pg
= new Progress();
416 final String luid
= lib
.getNextId();
418 // Keep the latest name
419 pg
.addProgressListener(new ProgressListener() {
421 public void progress(Progress progress
, String name
) {
426 synchronized (imprts
) {
427 imprts
.put(luid
, pg
);
430 new Thread(new Runnable() {
434 lib
.imprt(url
, luid
, pg
);
435 } catch (IOException e
) {
436 Instance
.getInstance().getTraceHandler().error(e
);
438 synchronized (imprts
) {
443 }, "Import story: " + urlStr
).start();
445 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
446 NanoHTTPD
.MIME_PLAINTEXT
, luid
);
450 protected Response
imprtProgress(String uri
, WLoginResult login
) {
451 String
[] uriParts
= uri
.split("/");
452 int off
= 2; // "" and "import"
454 if (uriParts
.length
< off
+ 1) {
455 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
456 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
459 String luid
= uriParts
[off
+ 0];
462 synchronized (imprts
) {
463 pg
= imprts
.get(luid
);
466 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
467 "application/json", JsonIO
.toJson(pg
).toString());
470 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
474 protected Response
delete(String uri
, WLoginResult login
)
476 String
[] uriParts
= uri
.split("/");
477 int off
= 2; // "" and "delete"
479 if (uriParts
.length
< off
+ 1) {
480 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
481 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid delete request");
485 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
486 NanoHTTPD
.MIME_PLAINTEXT
, "Delete not allowed");
489 String luid
= uriParts
[off
+ 0];
491 BasicLibrary lib
= Instance
.getInstance().getLibrary();
494 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
498 protected List
<MetaData
> metas(WLoginResult login
) throws IOException
{
499 BasicLibrary lib
= Instance
.getInstance().getLibrary();
500 List
<MetaData
> metas
= new ArrayList
<MetaData
>();
501 for (MetaData meta
: lib
.getList().getMetas()) {
502 if (isAllowed(meta
, login
)) {
510 // NULL if not whitelist OK or if not found
512 protected Story
story(String luid
, WLoginResult login
) throws IOException
{
513 synchronized (storyCache
) {
514 if (storyCache
.containsKey(luid
)) {
515 Story story
= storyCache
.get(luid
);
516 if (!isAllowed(story
.getMeta(), login
))
524 MetaData meta
= meta(luid
, login
);
526 BasicLibrary lib
= Instance
.getInstance().getLibrary();
527 story
= lib
.getStory(luid
, null);
528 long size
= sizeOf(story
);
530 synchronized (storyCache
) {
531 // Could have been added by another request
532 if (!storyCache
.containsKey(luid
)) {
533 while (!storyCacheOrder
.isEmpty()
534 && storyCacheSize
+ size
> maxStoryCacheSize
) {
535 String oldestLuid
= storyCacheOrder
.removeFirst();
536 Story oldestStory
= storyCache
.remove(oldestLuid
);
537 maxStoryCacheSize
-= sizeOf(oldestStory
);
540 storyCacheOrder
.add(luid
);
541 storyCache
.put(luid
, story
);
549 private MetaData
meta(String luid
, WLoginResult login
) throws IOException
{
550 BasicLibrary lib
= Instance
.getInstance().getLibrary();
551 MetaData meta
= lib
.getInfo(luid
);
552 if (!isAllowed(meta
, login
))
558 private Image
storyCover(String luid
, WLoginResult login
)
560 MetaData meta
= meta(luid
, login
);
562 BasicLibrary lib
= Instance
.getInstance().getLibrary();
563 return lib
.getCover(meta
.getLuid());
569 private Image
authorCover(String author
, WLoginResult login
)
573 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(null,
575 if (metas
.size() > 0) {
576 BasicLibrary lib
= Instance
.getInstance().getLibrary();
577 img
= lib
.getCustomAuthorCover(author
);
579 img
= lib
.getCover(metas
.get(0).getLuid());
586 private void authorCover(String author
, WLoginResult login
, String luid
)
588 if (meta(luid
, login
) != null) {
589 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(null,
591 if (metas
.size() > 0) {
592 BasicLibrary lib
= Instance
.getInstance().getLibrary();
593 lib
.setAuthorCover(author
, luid
);
598 private Image
sourceCover(String source
, WLoginResult login
)
602 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(source
,
604 if (metas
.size() > 0) {
605 BasicLibrary lib
= Instance
.getInstance().getLibrary();
606 img
= lib
.getCustomSourceCover(source
);
608 img
= lib
.getCover(metas
.get(0).getLuid());
614 private void sourceCover(String source
, WLoginResult login
, String luid
)
616 if (meta(luid
, login
) != null) {
617 List
<MetaData
> metas
= new MetaResultList(metas(login
))
618 .filter(source
, null, null);
619 if (metas
.size() > 0) {
620 BasicLibrary lib
= Instance
.getInstance().getLibrary();
621 lib
.setSourceCover(source
, luid
);
626 private boolean isAllowed(MetaData meta
, WLoginResult login
) {
627 MetaResultList one
= new MetaResultList(Arrays
.asList(meta
));
628 if (login
.isWl() && !whitelist
.isEmpty()) {
629 if (one
.filter(whitelist
, null, null).isEmpty()) {
633 if (login
.isBl() && !blacklist
.isEmpty()) {
634 if (!one
.filter(blacklist
, null, null).isEmpty()) {
642 private long sizeOf(Story story
) {
644 for (Chapter chap
: story
) {
645 for (Paragraph para
: chap
) {
646 if (para
.getType() == ParagraphType
.IMAGE
) {
647 size
+= para
.getContentImage().getSize();
649 size
+= para
.getContent().length();
657 public static void main(String
[] args
) throws IOException
{
659 WebLibraryServer web
= new WebLibraryServer(false);