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 public WebLibraryServer(boolean secure
) throws IOException
{
76 int cacheMb
= Instance
.getInstance().getConfig()
77 .getInteger(Config
.SERVER_MAX_CACHE_MB
, 100);
78 maxStoryCacheSize
= cacheMb
* 1024 * 1024;
80 setTraceHandler(Instance
.getInstance().getTraceHandler());
82 whitelist
= Instance
.getInstance().getConfig()
83 .getList(Config
.SERVER_WHITELIST
, new ArrayList
<String
>());
84 blacklist
= Instance
.getInstance().getConfig()
85 .getList(Config
.SERVER_BLACKLIST
, new ArrayList
<String
>());
89 * Start the server (listen on the network for new connections).
91 * Can only be called once.
93 * This call is asynchronous, and will just start a new {@link Thread} on
94 * itself (see {@link WebLibraryServer#run()}).
97 new Thread(this).start();
101 protected WLoginResult
login(boolean badLogin
, boolean badCookie
) {
102 return new WLoginResult(false, false);
106 protected WLoginResult
login(String who
, String cookie
) {
107 List
<String
> subkeys
= Instance
.getInstance().getConfig()
108 .getList(Config
.SERVER_ALLOWED_SUBKEYS
);
109 String realKey
= Instance
.getInstance().getConfig()
110 .getString(Config
.SERVER_KEY
);
112 return new WLoginResult(cookie
, who
, realKey
, subkeys
);
117 protected WLoginResult
login(String who
, String key
, String subkey
) {
118 String realKey
= Instance
.getInstance().getConfig()
119 .getString(Config
.SERVER_KEY
, "");
121 // I don't like NULLs...
122 key
= key
== null ?
"" : key
;
123 subkey
= subkey
== null ?
"" : subkey
;
125 if (!realKey
.equals(key
)) {
126 return new WLoginResult(true, false);
129 // defaults are true (as previous versions without the feature)
134 rw
= Instance
.getInstance().getConfig().getBoolean(Config
.SERVER_RW
,
137 List
<String
> allowed
= Instance
.getInstance().getConfig().getList(
138 Config
.SERVER_ALLOWED_SUBKEYS
, new ArrayList
<String
>());
140 if (!allowed
.isEmpty()) {
141 if (!allowed
.contains(subkey
)) {
142 return new WLoginResult(true, false);
145 if ((subkey
+ "|").contains("|rw|")) {
148 if ((subkey
+ "|").contains("|wl|")) {
149 wl
= false; // |wl| = bypass whitelist
151 if ((subkey
+ "|").contains("|bl|")) {
152 bl
= false; // |bl| = bypass blacklist
156 return new WLoginResult(who
, key
, subkey
, rw
, wl
, bl
);
160 protected Response
getList(String uri
, WLoginResult login
)
162 if (WebLibraryUrls
.LIST_URL_METADATA
.equals(uri
)) {
163 List
<JSONObject
> jsons
= new ArrayList
<JSONObject
>();
164 for (MetaData meta
: metas(login
)) {
165 jsons
.add(JsonIO
.toJson(meta
));
168 return newInputStreamResponse("application/json",
169 new ByteArrayInputStream(
170 new JSONArray(jsons
).toString().getBytes()));
173 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
174 NanoHTTPD
.MIME_PLAINTEXT
, null);
177 // /story/luid/chapter/para <-- text/image
178 // /story/luid/cover <-- image
179 // /story/luid/metadata <-- json
180 // /story/luid/json <-- json, whole chapter (no images)
182 protected Response
getStoryPart(String uri
, WLoginResult login
) {
183 String
[] uriParts
= uri
.split("/");
186 if (uriParts
.length
< off
+ 2) {
187 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
188 NanoHTTPD
.MIME_PLAINTEXT
, null);
191 String luid
= uriParts
[off
+ 0];
192 String chapterStr
= uriParts
[off
+ 1];
193 String imageStr
= uriParts
.length
< off
+ 3 ?
null : uriParts
[off
+ 2];
195 // 1-based (0 = desc)
197 if (chapterStr
!= null && !"cover".equals(chapterStr
)
198 && !"metadata".equals(chapterStr
)
199 && !"json".equals(chapterStr
)) {
201 chapter
= Integer
.parseInt(chapterStr
);
203 throw new NumberFormatException();
205 } catch (NumberFormatException e
) {
206 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
207 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
213 if (imageStr
!= null) {
215 paragraph
= Integer
.parseInt(imageStr
);
217 throw new NumberFormatException();
219 } catch (NumberFormatException e
) {
220 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
221 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
225 String mimeType
= NanoHTTPD
.MIME_PLAINTEXT
;
226 InputStream in
= null;
228 if ("cover".equals(chapterStr
)) {
229 Image img
= storyCover(luid
, login
);
231 in
= img
.newInputStream();
233 // TODO: get correct image type
234 mimeType
= "image/png";
235 } else if ("metadata".equals(chapterStr
)) {
236 MetaData meta
= meta(luid
, login
);
237 JSONObject json
= JsonIO
.toJson(meta
);
238 mimeType
= "application/json";
239 in
= new ByteArrayInputStream(json
.toString().getBytes());
240 } else if ("json".equals(chapterStr
)) {
241 Story story
= story(luid
, login
);
242 JSONObject json
= JsonIO
.toJson(story
);
243 mimeType
= "application/json";
244 in
= new ByteArrayInputStream(json
.toString().getBytes());
246 Story story
= story(luid
, login
);
249 StringBuilder builder
= new StringBuilder();
250 for (Paragraph p
: story
.getMeta().getResume()) {
251 if (builder
.length() == 0) {
252 builder
.append("\n");
254 builder
.append(p
.getContent());
257 in
= new ByteArrayInputStream(
258 builder
.toString().getBytes("utf-8"));
260 Paragraph para
= story
.getChapters().get(chapter
- 1)
261 .getParagraphs().get(paragraph
- 1);
262 Image img
= para
.getContentImage();
263 if (para
.getType() == ParagraphType
.IMAGE
) {
264 // TODO: get correct image type
265 mimeType
= "image/png";
266 in
= img
.newInputStream();
268 in
= new ByteArrayInputStream(
269 para
.getContent().getBytes("utf-8"));
274 } catch (IndexOutOfBoundsException e
) {
275 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
276 NanoHTTPD
.MIME_PLAINTEXT
,
277 "Chapter or paragraph does not exist");
278 } catch (IOException e
) {
279 Instance
.getInstance().getTraceHandler()
280 .error(new IOException("Cannot get image: " + uri
, e
));
281 return NanoHTTPD
.newFixedLengthResponse(Status
.INTERNAL_ERROR
,
282 NanoHTTPD
.MIME_PLAINTEXT
, "Error when processing request");
285 return newInputStreamResponse(mimeType
, in
);
288 // /story/luid/source
290 // /story/luid/author
292 protected Response
setStoryPart(String uri
, String value
,
293 WLoginResult login
) throws IOException
{
294 String
[] uriParts
= uri
.split("/");
295 int off
= 2; // "" and "story"
297 if (uriParts
.length
< off
+ 2) {
298 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
299 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid story part request");
303 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
304 NanoHTTPD
.MIME_PLAINTEXT
, "SET story part not allowed");
307 String luid
= uriParts
[off
+ 0];
308 String type
= uriParts
[off
+ 1];
310 if (!Arrays
.asList("source", "title", "author").contains(type
)) {
311 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
312 NanoHTTPD
.MIME_PLAINTEXT
,
313 "Invalid SET story part: " + type
);
316 if (meta(luid
, login
) != null) {
317 BasicLibrary lib
= Instance
.getInstance().getLibrary();
318 if ("source".equals(type
)) {
319 lib
.changeSource(luid
, value
, null);
320 } else if ("title".equals(type
)) {
321 lib
.changeTitle(luid
, value
, null);
322 } else if ("author".equals(type
)) {
323 lib
.changeAuthor(luid
, value
, null);
327 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
331 protected Response
getCover(String uri
, WLoginResult login
)
333 String
[] uriParts
= uri
.split("/");
334 int off
= 2; // "" and "cover"
336 if (uriParts
.length
< off
+ 2) {
337 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
338 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
341 String type
= uriParts
[off
+ 0];
342 String id
= uriParts
[off
+ 1];
344 InputStream in
= null;
346 if ("story".equals(type
)) {
347 Image img
= storyCover(id
, login
);
349 in
= img
.newInputStream();
351 } else if ("source".equals(type
)) {
352 Image img
= sourceCover(id
, login
);
354 in
= img
.newInputStream();
356 } else if ("author".equals(type
)) {
357 Image img
= authorCover(id
, login
);
359 in
= img
.newInputStream();
362 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
363 NanoHTTPD
.MIME_PLAINTEXT
,
364 "Invalid GET cover type: " + type
);
367 // TODO: get correct image type
368 return newInputStreamResponse("image/png", in
);
372 protected Response
setCover(String uri
, String luid
, WLoginResult login
)
374 String
[] uriParts
= uri
.split("/");
375 int off
= 2; // "" and "cover"
377 if (uriParts
.length
< off
+ 2) {
378 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
379 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
383 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
384 NanoHTTPD
.MIME_PLAINTEXT
, "Cover request not allowed");
387 String type
= uriParts
[off
+ 0];
388 String id
= uriParts
[off
+ 1];
390 if ("source".equals(type
)) {
391 sourceCover(id
, login
, luid
);
392 } else if ("author".equals(type
)) {
393 authorCover(id
, login
, luid
);
395 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
396 NanoHTTPD
.MIME_PLAINTEXT
,
397 "Invalid SET cover type: " + type
);
400 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
404 protected Response
imprt(String uri
, String urlStr
, WLoginResult login
)
406 final BasicLibrary lib
= Instance
.getInstance().getLibrary();
409 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
410 NanoHTTPD
.MIME_PLAINTEXT
, "Import not allowed");
413 final URL url
= new URL(urlStr
);
414 final Progress pg
= new Progress();
415 final String luid
= lib
.getNextId();
417 synchronized (imprts
) {
418 imprts
.put(luid
, pg
);
421 new Thread(new Runnable() {
425 lib
.imprt(url
, luid
, pg
);
426 } catch (IOException e
) {
427 Instance
.getInstance().getTraceHandler().error(e
);
429 synchronized (imprts
) {
434 }, "Import story: " + urlStr
).start();
436 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
437 NanoHTTPD
.MIME_PLAINTEXT
, luid
);
441 protected Response
imprtProgress(String uri
, WLoginResult login
) {
442 String
[] uriParts
= uri
.split("/");
443 int off
= 2; // "" and "import"
445 if (uriParts
.length
< off
+ 1) {
446 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
447 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
450 String luid
= uriParts
[off
+ 0];
453 synchronized (imprts
) {
454 pg
= imprts
.get(luid
);
457 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
458 "application/json", JsonIO
.toJson(pg
).toString());
461 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
465 protected Response
delete(String uri
, WLoginResult login
)
467 String
[] uriParts
= uri
.split("/");
468 int off
= 2; // "" and "delete"
470 if (uriParts
.length
< off
+ 1) {
471 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
472 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid delete request");
476 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
477 NanoHTTPD
.MIME_PLAINTEXT
, "Delete not allowed");
480 String luid
= uriParts
[off
+ 0];
482 BasicLibrary lib
= Instance
.getInstance().getLibrary();
485 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
489 protected List
<MetaData
> metas(WLoginResult login
) throws IOException
{
490 BasicLibrary lib
= Instance
.getInstance().getLibrary();
491 List
<MetaData
> metas
= new ArrayList
<MetaData
>();
492 for (MetaData meta
: lib
.getList().getMetas()) {
493 if (isAllowed(meta
, login
)) {
501 // NULL if not whitelist OK or if not found
503 protected Story
story(String luid
, WLoginResult login
) throws IOException
{
504 synchronized (storyCache
) {
505 if (storyCache
.containsKey(luid
)) {
506 Story story
= storyCache
.get(luid
);
507 if (!isAllowed(story
.getMeta(), login
))
515 MetaData meta
= meta(luid
, login
);
517 BasicLibrary lib
= Instance
.getInstance().getLibrary();
518 story
= lib
.getStory(luid
, null);
519 long size
= sizeOf(story
);
521 synchronized (storyCache
) {
522 // Could have been added by another request
523 if (!storyCache
.containsKey(luid
)) {
524 while (!storyCacheOrder
.isEmpty()
525 && storyCacheSize
+ size
> maxStoryCacheSize
) {
526 String oldestLuid
= storyCacheOrder
.removeFirst();
527 Story oldestStory
= storyCache
.remove(oldestLuid
);
528 maxStoryCacheSize
-= sizeOf(oldestStory
);
531 storyCacheOrder
.add(luid
);
532 storyCache
.put(luid
, story
);
540 private MetaData
meta(String luid
, WLoginResult login
) throws IOException
{
541 BasicLibrary lib
= Instance
.getInstance().getLibrary();
542 MetaData meta
= lib
.getInfo(luid
);
543 if (!isAllowed(meta
, login
))
549 private Image
storyCover(String luid
, WLoginResult login
)
551 MetaData meta
= meta(luid
, login
);
553 BasicLibrary lib
= Instance
.getInstance().getLibrary();
554 return lib
.getCover(meta
.getLuid());
560 private Image
authorCover(String author
, WLoginResult login
)
564 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(null,
566 if (metas
.size() > 0) {
567 BasicLibrary lib
= Instance
.getInstance().getLibrary();
568 img
= lib
.getCustomAuthorCover(author
);
570 img
= lib
.getCover(metas
.get(0).getLuid());
577 private void authorCover(String author
, WLoginResult login
, String luid
)
579 if (meta(luid
, login
) != null) {
580 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(null,
582 if (metas
.size() > 0) {
583 BasicLibrary lib
= Instance
.getInstance().getLibrary();
584 lib
.setAuthorCover(author
, luid
);
589 private Image
sourceCover(String source
, WLoginResult login
)
593 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(source
,
595 if (metas
.size() > 0) {
596 BasicLibrary lib
= Instance
.getInstance().getLibrary();
597 img
= lib
.getCustomSourceCover(source
);
599 img
= lib
.getCover(metas
.get(0).getLuid());
605 private void sourceCover(String source
, WLoginResult login
, String luid
)
607 if (meta(luid
, login
) != null) {
608 List
<MetaData
> metas
= new MetaResultList(metas(login
))
609 .filter(source
, null, null);
610 if (metas
.size() > 0) {
611 BasicLibrary lib
= Instance
.getInstance().getLibrary();
612 lib
.setSourceCover(source
, luid
);
617 private boolean isAllowed(MetaData meta
, WLoginResult login
) {
618 MetaResultList one
= new MetaResultList(Arrays
.asList(meta
));
619 if (login
.isWl() && !whitelist
.isEmpty()) {
620 if (one
.filter(whitelist
, null, null).isEmpty()) {
624 if (login
.isBl() && !blacklist
.isEmpty()) {
625 if (!one
.filter(blacklist
, null, null).isEmpty()) {
633 private long sizeOf(Story story
) {
635 for (Chapter chap
: story
) {
636 for (Paragraph para
: chap
) {
637 if (para
.getType() == ParagraphType
.IMAGE
) {
638 size
+= para
.getContentImage().getSize();
640 size
+= para
.getContent().length();
648 public static void main(String
[] args
) throws IOException
{
650 WebLibraryServer web
= new WebLibraryServer(false);