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
{
38 public WLoginResult(boolean badLogin
, boolean badCookie
) {
39 super(badLogin
, badCookie
);
42 public WLoginResult(String who
, String key
, String subkey
, boolean rw
,
43 boolean wl
, boolean bl
) {
44 super(who
, key
, subkey
, (rw ?
"|rw" : "") + (wl ?
"|wl" : "")
45 + (bl ?
"|bl" : "") + "|");
51 public WLoginResult(String cookie
, String who
, String key
,
52 List
<String
> subkeys
) {
53 super(cookie
, who
, key
, subkeys
,
54 subkeys
== null || subkeys
.isEmpty());
57 public boolean isRw() {
58 return getOption().contains("|rw|");
61 public boolean isWl() {
62 return getOption().contains("|wl|");
65 public boolean isBl() {
66 return getOption().contains("|bl|");
70 private Map
<String
, Story
> storyCache
= new HashMap
<String
, Story
>();
71 private LinkedList
<String
> storyCacheOrder
= new LinkedList
<String
>();
72 private long storyCacheSize
= 0;
73 private long maxStoryCacheSize
;
75 private List
<String
> whitelist
;
76 private List
<String
> blacklist
;
78 private Map
<String
, Progress
> imprts
= new HashMap
<String
, Progress
>();
80 public WebLibraryServer(boolean secure
) throws IOException
{
83 int cacheMb
= Instance
.getInstance().getConfig()
84 .getInteger(Config
.SERVER_MAX_CACHE_MB
, 100);
85 maxStoryCacheSize
= cacheMb
* 1024 * 1024;
87 setTraceHandler(Instance
.getInstance().getTraceHandler());
89 whitelist
= Instance
.getInstance().getConfig()
90 .getList(Config
.SERVER_WHITELIST
, new ArrayList
<String
>());
91 blacklist
= Instance
.getInstance().getConfig()
92 .getList(Config
.SERVER_BLACKLIST
, new ArrayList
<String
>());
96 * Start the server (listen on the network for new connections).
98 * Can only be called once.
100 * This call is asynchronous, and will just start a new {@link Thread} on
101 * itself (see {@link WebLibraryServer#run()}).
103 public void start() {
104 new Thread(this).start();
108 protected WLoginResult
login(boolean badLogin
, boolean badCookie
) {
109 return new WLoginResult(false, false);
113 protected WLoginResult
login(String who
, String cookie
) {
114 List
<String
> subkeys
= Instance
.getInstance().getConfig()
115 .getList(Config
.SERVER_ALLOWED_SUBKEYS
);
116 String realKey
= Instance
.getInstance().getConfig()
117 .getString(Config
.SERVER_KEY
);
119 return new WLoginResult(cookie
, who
, realKey
, subkeys
);
124 protected WLoginResult
login(String who
, String key
, String subkey
) {
125 String realKey
= Instance
.getInstance().getConfig()
126 .getString(Config
.SERVER_KEY
, "");
128 // I don't like NULLs...
129 key
= key
== null ?
"" : key
;
130 subkey
= subkey
== null ?
"" : subkey
;
132 if (!realKey
.equals(key
)) {
133 return new WLoginResult(true, false);
136 // defaults are true (as previous versions without the feature)
141 rw
= Instance
.getInstance().getConfig().getBoolean(Config
.SERVER_RW
,
144 List
<String
> allowed
= Instance
.getInstance().getConfig().getList(
145 Config
.SERVER_ALLOWED_SUBKEYS
, new ArrayList
<String
>());
147 if (!allowed
.isEmpty()) {
148 if (!allowed
.contains(subkey
)) {
149 return new WLoginResult(true, false);
152 if ((subkey
+ "|").contains("|rw|")) {
155 if ((subkey
+ "|").contains("|wl|")) {
156 wl
= false; // |wl| = bypass whitelist
158 if ((subkey
+ "|").contains("|bl|")) {
159 bl
= false; // |bl| = bypass blacklist
163 return new WLoginResult(who
, key
, subkey
, rw
, wl
, bl
);
167 protected Response
getList(String uri
, WLoginResult login
)
169 if (WebLibraryUrls
.LIST_URL_METADATA
.equals(uri
)) {
170 List
<JSONObject
> jsons
= new ArrayList
<JSONObject
>();
171 for (MetaData meta
: metas(login
)) {
172 jsons
.add(JsonIO
.toJson(meta
));
175 return newInputStreamResponse("application/json",
176 new ByteArrayInputStream(
177 new JSONArray(jsons
).toString().getBytes()));
180 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
181 NanoHTTPD
.MIME_PLAINTEXT
, null);
184 // /story/luid/chapter/para <-- text/image
185 // /story/luid/cover <-- image
186 // /story/luid/metadata <-- json
187 // /story/luid/json <-- json, whole chapter (no images)
189 protected Response
getStoryPart(String uri
, WLoginResult login
) {
190 String
[] uriParts
= uri
.split("/");
193 if (uriParts
.length
< off
+ 2) {
194 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
195 NanoHTTPD
.MIME_PLAINTEXT
, null);
198 String luid
= uriParts
[off
+ 0];
199 String chapterStr
= uriParts
[off
+ 1];
200 String imageStr
= uriParts
.length
< off
+ 3 ?
null : uriParts
[off
+ 2];
202 // 1-based (0 = desc)
204 if (chapterStr
!= null && !"cover".equals(chapterStr
)
205 && !"metadata".equals(chapterStr
)
206 && !"json".equals(chapterStr
)) {
208 chapter
= Integer
.parseInt(chapterStr
);
210 throw new NumberFormatException();
212 } catch (NumberFormatException e
) {
213 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
214 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
220 if (imageStr
!= null) {
222 paragraph
= Integer
.parseInt(imageStr
);
224 throw new NumberFormatException();
226 } catch (NumberFormatException e
) {
227 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
228 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
232 String mimeType
= NanoHTTPD
.MIME_PLAINTEXT
;
233 InputStream in
= null;
235 if ("cover".equals(chapterStr
)) {
236 Image img
= storyCover(luid
, login
);
238 in
= img
.newInputStream();
240 // TODO: get correct image type
241 mimeType
= "image/png";
242 } else if ("metadata".equals(chapterStr
)) {
243 MetaData meta
= meta(luid
, login
);
244 JSONObject json
= JsonIO
.toJson(meta
);
245 mimeType
= "application/json";
246 in
= new ByteArrayInputStream(json
.toString().getBytes());
247 } else if ("json".equals(chapterStr
)) {
248 Story story
= story(luid
, login
);
249 JSONObject json
= JsonIO
.toJson(story
);
250 mimeType
= "application/json";
251 in
= new ByteArrayInputStream(json
.toString().getBytes());
253 Story story
= story(luid
, login
);
256 StringBuilder builder
= new StringBuilder();
257 for (Paragraph p
: story
.getMeta().getResume()) {
258 if (builder
.length() == 0) {
259 builder
.append("\n");
261 builder
.append(p
.getContent());
264 in
= new ByteArrayInputStream(
265 builder
.toString().getBytes("utf-8"));
267 Paragraph para
= story
.getChapters().get(chapter
- 1)
268 .getParagraphs().get(paragraph
- 1);
269 Image img
= para
.getContentImage();
270 if (para
.getType() == ParagraphType
.IMAGE
) {
271 // TODO: get correct image type
272 mimeType
= "image/png";
273 in
= img
.newInputStream();
275 in
= new ByteArrayInputStream(
276 para
.getContent().getBytes("utf-8"));
281 } catch (IndexOutOfBoundsException e
) {
282 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
283 NanoHTTPD
.MIME_PLAINTEXT
,
284 "Chapter or paragraph does not exist");
285 } catch (IOException e
) {
286 Instance
.getInstance().getTraceHandler()
287 .error(new IOException("Cannot get image: " + uri
, e
));
288 return NanoHTTPD
.newFixedLengthResponse(Status
.INTERNAL_ERROR
,
289 NanoHTTPD
.MIME_PLAINTEXT
, "Error when processing request");
292 return newInputStreamResponse(mimeType
, in
);
295 // /story/luid/source
297 // /story/luid/author
299 protected Response
setStoryPart(String uri
, String value
,
300 WLoginResult login
) throws IOException
{
301 String
[] uriParts
= uri
.split("/");
302 int off
= 2; // "" and "story"
304 if (uriParts
.length
< off
+ 2) {
305 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
306 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid story part request");
309 String luid
= uriParts
[off
+ 0];
310 String type
= uriParts
[off
+ 1];
312 if (!Arrays
.asList("source", "title", "author").contains(type
)) {
313 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
314 NanoHTTPD
.MIME_PLAINTEXT
,
315 "Invalid SET story part: " + type
);
318 if (meta(luid
, login
) != null) {
319 BasicLibrary lib
= Instance
.getInstance().getLibrary();
320 if ("source".equals(type
)) {
321 lib
.changeSource(luid
, value
, null);
322 } else if ("title".equals(type
)) {
323 lib
.changeTitle(luid
, value
, null);
324 } else if ("author".equals(type
)) {
325 lib
.changeAuthor(luid
, value
, null);
329 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
333 protected Response
getCover(String uri
, WLoginResult login
)
335 String
[] uriParts
= uri
.split("/");
336 int off
= 2; // "" and "cover"
338 if (uriParts
.length
< off
+ 2) {
339 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
340 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
343 String type
= uriParts
[off
+ 0];
344 String id
= uriParts
[off
+ 1];
346 InputStream in
= null;
348 if ("story".equals(type
)) {
349 Image img
= storyCover(id
, login
);
351 in
= img
.newInputStream();
353 } else if ("source".equals(type
)) {
354 Image img
= sourceCover(id
, login
);
356 in
= img
.newInputStream();
358 } else if ("author".equals(type
)) {
359 Image img
= authorCover(id
, login
);
361 in
= img
.newInputStream();
364 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
365 NanoHTTPD
.MIME_PLAINTEXT
,
366 "Invalid GET cover type: " + type
);
369 // TODO: get correct image type
370 return newInputStreamResponse("image/png", in
);
374 protected Response
setCover(String uri
, String luid
, WLoginResult login
)
376 String
[] uriParts
= uri
.split("/");
377 int off
= 2; // "" and "cover"
379 if (uriParts
.length
< off
+ 2) {
380 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
381 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
384 String type
= uriParts
[off
+ 0];
385 String id
= uriParts
[off
+ 1];
387 if ("source".equals(type
)) {
388 sourceCover(id
, login
, luid
);
389 } else if ("author".equals(type
)) {
390 authorCover(id
, login
, luid
);
392 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
393 NanoHTTPD
.MIME_PLAINTEXT
,
394 "Invalid SET cover type: " + type
);
397 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
401 protected Response
imprt(String uri
, String urlStr
, WLoginResult login
)
403 final BasicLibrary lib
= Instance
.getInstance().getLibrary();
405 final URL url
= new URL(urlStr
);
406 final Progress pg
= new Progress();
407 final String luid
= lib
.getNextId();
409 synchronized (imprts
) {
410 imprts
.put(luid
, pg
);
413 new Thread(new Runnable() {
418 } catch (IOException e
) {
419 Instance
.getInstance().getTraceHandler().error(e
);
421 synchronized (imprts
) {
426 }, "Import story: " + urlStr
).start();
430 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
431 NanoHTTPD
.MIME_PLAINTEXT
, luid
);
435 protected Response
imprtProgress(String uri
, WLoginResult login
) {
436 String
[] uriParts
= uri
.split("/");
437 int off
= 2; // "" and "import"
439 if (uriParts
.length
< off
+ 1) {
440 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
441 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
444 String luid
= uriParts
[off
+ 0];
447 synchronized (imprts
) {
448 pg
= imprts
.get(luid
);
451 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
452 "application/json", JsonIO
.toJson(pg
).toString());
455 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
459 protected List
<MetaData
> metas(WLoginResult login
) throws IOException
{
460 BasicLibrary lib
= Instance
.getInstance().getLibrary();
461 List
<MetaData
> metas
= new ArrayList
<MetaData
>();
462 for (MetaData meta
: lib
.getList().getMetas()) {
463 if (isAllowed(meta
, login
)) {
471 // NULL if not whitelist OK or if not found
473 protected Story
story(String luid
, WLoginResult login
) throws IOException
{
474 synchronized (storyCache
) {
475 if (storyCache
.containsKey(luid
)) {
476 Story story
= storyCache
.get(luid
);
477 if (!isAllowed(story
.getMeta(), login
))
485 MetaData meta
= meta(luid
, login
);
487 BasicLibrary lib
= Instance
.getInstance().getLibrary();
488 story
= lib
.getStory(luid
, null);
489 long size
= sizeOf(story
);
491 synchronized (storyCache
) {
492 // Could have been added by another request
493 if (!storyCache
.containsKey(luid
)) {
494 while (!storyCacheOrder
.isEmpty()
495 && storyCacheSize
+ size
> maxStoryCacheSize
) {
496 String oldestLuid
= storyCacheOrder
.removeFirst();
497 Story oldestStory
= storyCache
.remove(oldestLuid
);
498 maxStoryCacheSize
-= sizeOf(oldestStory
);
501 storyCacheOrder
.add(luid
);
502 storyCache
.put(luid
, story
);
510 private MetaData
meta(String luid
, WLoginResult login
) throws IOException
{
511 BasicLibrary lib
= Instance
.getInstance().getLibrary();
512 MetaData meta
= lib
.getInfo(luid
);
513 if (!isAllowed(meta
, login
))
519 private Image
storyCover(String luid
, WLoginResult login
)
521 MetaData meta
= meta(luid
, login
);
523 BasicLibrary lib
= Instance
.getInstance().getLibrary();
524 return lib
.getCover(meta
.getLuid());
530 private Image
authorCover(String author
, WLoginResult login
)
534 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(null,
536 if (metas
.size() > 0) {
537 BasicLibrary lib
= Instance
.getInstance().getLibrary();
538 img
= lib
.getCustomAuthorCover(author
);
540 img
= lib
.getCover(metas
.get(0).getLuid());
547 private void authorCover(String author
, WLoginResult login
, String luid
)
549 if (meta(luid
, login
) != null) {
550 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(null,
552 if (metas
.size() > 0) {
553 BasicLibrary lib
= Instance
.getInstance().getLibrary();
554 lib
.setAuthorCover(author
, luid
);
559 private Image
sourceCover(String source
, WLoginResult login
)
563 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(source
,
565 if (metas
.size() > 0) {
566 BasicLibrary lib
= Instance
.getInstance().getLibrary();
567 img
= lib
.getCustomSourceCover(source
);
569 img
= lib
.getCover(metas
.get(0).getLuid());
575 private void sourceCover(String source
, WLoginResult login
, String luid
)
577 if (meta(luid
, login
) != null) {
578 List
<MetaData
> metas
= new MetaResultList(metas(login
))
579 .filter(source
, null, null);
580 if (metas
.size() > 0) {
581 BasicLibrary lib
= Instance
.getInstance().getLibrary();
582 lib
.setSourceCover(source
, luid
);
587 private boolean isAllowed(MetaData meta
, WLoginResult login
) {
588 MetaResultList one
= new MetaResultList(Arrays
.asList(meta
));
589 if (login
.isWl() && !whitelist
.isEmpty()) {
590 if (one
.filter(whitelist
, null, null).isEmpty()) {
594 if (login
.isBl() && !blacklist
.isEmpty()) {
595 if (!one
.filter(blacklist
, null, null).isEmpty()) {
603 private long sizeOf(Story story
) {
605 for (Chapter chap
: story
) {
606 for (Paragraph para
: chap
) {
607 if (para
.getType() == ParagraphType
.IMAGE
) {
608 size
+= para
.getContentImage().getSize();
610 size
+= para
.getContent().length();
618 public static void main(String
[] args
) throws IOException
{
620 WebLibraryServer web
= new WebLibraryServer(false);