1 package be
.nikiroo
.fanfix
.library
;
3 import java
.io
.ByteArrayInputStream
;
4 import java
.io
.IOException
;
5 import java
.io
.InputStream
;
6 import java
.util
.ArrayList
;
7 import java
.util
.Arrays
;
8 import java
.util
.HashMap
;
9 import java
.util
.LinkedList
;
10 import java
.util
.List
;
13 import org
.json
.JSONArray
;
14 import org
.json
.JSONObject
;
16 import be
.nikiroo
.fanfix
.Instance
;
17 import be
.nikiroo
.fanfix
.bundles
.Config
;
18 import be
.nikiroo
.fanfix
.data
.Chapter
;
19 import be
.nikiroo
.fanfix
.data
.JsonIO
;
20 import be
.nikiroo
.fanfix
.data
.MetaData
;
21 import be
.nikiroo
.fanfix
.data
.Paragraph
;
22 import be
.nikiroo
.fanfix
.data
.Paragraph
.ParagraphType
;
23 import be
.nikiroo
.fanfix
.data
.Story
;
24 import be
.nikiroo
.utils
.Image
;
25 import be
.nikiroo
.utils
.LoginResult
;
26 import be
.nikiroo
.utils
.NanoHTTPD
;
27 import be
.nikiroo
.utils
.NanoHTTPD
.Response
;
28 import be
.nikiroo
.utils
.NanoHTTPD
.Response
.Status
;
30 public class WebLibraryServer
extends WebLibraryServerHtml
{
31 class WLoginResult
extends LoginResult
{
36 public WLoginResult(boolean badLogin
, boolean badCookie
) {
37 super(badLogin
, badCookie
);
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" : "") + "|");
49 public WLoginResult(String cookie
, String who
, String key
,
50 List
<String
> subkeys
) {
51 super(cookie
, who
, key
, subkeys
,
52 subkeys
== null || subkeys
.isEmpty());
55 public boolean isRw() {
56 return getOption().contains("|rw|");
59 public boolean isWl() {
60 return getOption().contains("|wl|");
63 public boolean isBl() {
64 return getOption().contains("|bl|");
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
;
73 private List
<String
> whitelist
;
74 private List
<String
> blacklist
;
76 public WebLibraryServer(boolean secure
) throws IOException
{
79 int cacheMb
= Instance
.getInstance().getConfig()
80 .getInteger(Config
.SERVER_MAX_CACHE_MB
, 100);
81 maxStoryCacheSize
= cacheMb
* 1024 * 1024;
83 setTraceHandler(Instance
.getInstance().getTraceHandler());
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
>());
92 * Start the server (listen on the network for new connections).
94 * Can only be called once.
96 * This call is asynchronous, and will just start a new {@link Thread} on
97 * itself (see {@link WebLibraryServer#run()}).
100 new Thread(this).start();
104 protected WLoginResult
login(boolean badLogin
, boolean badCookie
) {
105 return new WLoginResult(false, false);
109 protected WLoginResult
login(String who
, String cookie
) {
110 List
<String
> subkeys
= Instance
.getInstance().getConfig()
111 .getList(Config
.SERVER_ALLOWED_SUBKEYS
);
112 String realKey
= Instance
.getInstance().getConfig()
113 .getString(Config
.SERVER_KEY
);
115 return new WLoginResult(cookie
, who
, realKey
, subkeys
);
120 protected WLoginResult
login(String who
, String key
, String subkey
) {
121 String realKey
= Instance
.getInstance().getConfig()
122 .getString(Config
.SERVER_KEY
, "");
124 // I don't like NULLs...
125 key
= key
== null ?
"" : key
;
126 subkey
= subkey
== null ?
"" : subkey
;
128 if (!realKey
.equals(key
)) {
129 return new WLoginResult(true, false);
132 // defaults are true (as previous versions without the feature)
137 rw
= Instance
.getInstance().getConfig().getBoolean(Config
.SERVER_RW
,
140 List
<String
> allowed
= Instance
.getInstance().getConfig().getList(
141 Config
.SERVER_ALLOWED_SUBKEYS
, new ArrayList
<String
>());
143 if (!allowed
.isEmpty()) {
144 if (!allowed
.contains(subkey
)) {
145 return new WLoginResult(true, false);
148 if ((subkey
+ "|").contains("|rw|")) {
151 if ((subkey
+ "|").contains("|wl|")) {
152 wl
= false; // |wl| = bypass whitelist
154 if ((subkey
+ "|").contains("|bl|")) {
155 bl
= false; // |bl| = bypass blacklist
159 return new WLoginResult(who
, key
, subkey
, rw
, wl
, bl
);
163 protected Response
getList(String uri
, WLoginResult login
)
165 if (WebLibraryUrls
.LIST_URL_METADATA
.equals(uri
)) {
166 List
<JSONObject
> jsons
= new ArrayList
<JSONObject
>();
167 for (MetaData meta
: metas(login
)) {
168 jsons
.add(JsonIO
.toJson(meta
));
171 return newInputStreamResponse("application/json",
172 new ByteArrayInputStream(
173 new JSONArray(jsons
).toString().getBytes()));
176 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
177 NanoHTTPD
.MIME_PLAINTEXT
, null);
180 // /story/luid/chapter/para <-- text/image
181 // /story/luid/cover <-- image
182 // /story/luid/metadata <-- json
183 // /story/luid/json <-- json, whole chapter (no images)
185 protected Response
getStoryPart(String uri
, WLoginResult login
) {
186 String
[] uriParts
= uri
.split("/");
189 if (uriParts
.length
< off
+ 2) {
190 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
191 NanoHTTPD
.MIME_PLAINTEXT
, null);
194 String luid
= uriParts
[off
+ 0];
195 String chapterStr
= uriParts
[off
+ 1];
196 String imageStr
= uriParts
.length
< off
+ 3 ?
null : uriParts
[off
+ 2];
198 // 1-based (0 = desc)
200 if (chapterStr
!= null && !"cover".equals(chapterStr
)
201 && !"metadata".equals(chapterStr
)
202 && !"json".equals(chapterStr
)) {
204 chapter
= Integer
.parseInt(chapterStr
);
206 throw new NumberFormatException();
208 } catch (NumberFormatException e
) {
209 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
210 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
216 if (imageStr
!= null) {
218 paragraph
= Integer
.parseInt(imageStr
);
220 throw new NumberFormatException();
222 } catch (NumberFormatException e
) {
223 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
224 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
228 String mimeType
= NanoHTTPD
.MIME_PLAINTEXT
;
229 InputStream in
= null;
231 if ("cover".equals(chapterStr
)) {
232 Image img
= storyCover(luid
, login
);
234 in
= img
.newInputStream();
236 // TODO: get correct image type
237 mimeType
= "image/png";
238 } else if ("metadata".equals(chapterStr
)) {
239 MetaData meta
= meta(luid
, login
);
240 JSONObject json
= JsonIO
.toJson(meta
);
241 mimeType
= "application/json";
242 in
= new ByteArrayInputStream(json
.toString().getBytes());
243 } else if ("json".equals(chapterStr
)) {
244 Story story
= story(luid
, login
);
245 JSONObject json
= JsonIO
.toJson(story
);
246 mimeType
= "application/json";
247 in
= new ByteArrayInputStream(json
.toString().getBytes());
249 Story story
= story(luid
, login
);
252 StringBuilder builder
= new StringBuilder();
253 for (Paragraph p
: story
.getMeta().getResume()) {
254 if (builder
.length() == 0) {
255 builder
.append("\n");
257 builder
.append(p
.getContent());
260 in
= new ByteArrayInputStream(
261 builder
.toString().getBytes("utf-8"));
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();
271 in
= new ByteArrayInputStream(
272 para
.getContent().getBytes("utf-8"));
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");
288 return newInputStreamResponse(mimeType
, in
);
291 // /story/luid/source
293 // /story/luid/author
295 protected Response
setStoryPart(String uri
, String value
,
296 WLoginResult login
) throws IOException
{
297 String
[] uriParts
= uri
.split("/");
298 int off
= 2; // "" and "story"
300 if (uriParts
.length
< off
+ 2) {
301 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
302 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid story part request");
305 String luid
= uriParts
[off
+ 0];
306 String type
= uriParts
[off
+ 1];
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
);
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);
325 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
329 protected Response
getCover(String uri
, WLoginResult login
)
331 String
[] uriParts
= uri
.split("/");
332 int off
= 2; // "" and "cover"
334 if (uriParts
.length
< off
+ 2) {
335 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
336 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
339 String type
= uriParts
[off
+ 0];
340 String id
= uriParts
[off
+ 1];
342 InputStream in
= null;
344 if ("story".equals(type
)) {
345 Image img
= storyCover(id
, login
);
347 in
= img
.newInputStream();
349 } else if ("source".equals(type
)) {
350 Image img
= sourceCover(id
, login
);
352 in
= img
.newInputStream();
354 } else if ("author".equals(type
)) {
355 Image img
= authorCover(id
, login
);
357 in
= img
.newInputStream();
360 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
361 NanoHTTPD
.MIME_PLAINTEXT
,
362 "Invalid GET cover type: " + type
);
365 // TODO: get correct image type
366 return newInputStreamResponse("image/png", in
);
370 protected Response
setCover(String uri
, String luid
, WLoginResult login
)
372 String
[] uriParts
= uri
.split("/");
373 int off
= 2; // "" and "cover"
375 if (uriParts
.length
< off
+ 2) {
376 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
377 NanoHTTPD
.MIME_PLAINTEXT
, "Invalid cover request");
380 String type
= uriParts
[off
+ 0];
381 String id
= uriParts
[off
+ 1];
383 if ("source".equals(type
)) {
384 sourceCover(id
, login
, luid
);
385 } else if ("author".equals(type
)) {
386 authorCover(id
, login
, luid
);
388 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
389 NanoHTTPD
.MIME_PLAINTEXT
,
390 "Invalid SET cover type: " + type
);
393 return newInputStreamResponse(NanoHTTPD
.MIME_PLAINTEXT
, null);
397 protected List
<MetaData
> metas(WLoginResult login
) throws IOException
{
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
)) {
409 // NULL if not whitelist OK or if not found
411 protected Story
story(String luid
, WLoginResult login
) throws IOException
{
412 synchronized (storyCache
) {
413 if (storyCache
.containsKey(luid
)) {
414 Story story
= storyCache
.get(luid
);
415 if (!isAllowed(story
.getMeta(), login
))
423 MetaData meta
= meta(luid
, login
);
425 BasicLibrary lib
= Instance
.getInstance().getLibrary();
426 story
= lib
.getStory(luid
, null);
427 long size
= sizeOf(story
);
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
);
439 storyCacheOrder
.add(luid
);
440 storyCache
.put(luid
, story
);
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
))
457 private Image
storyCover(String luid
, WLoginResult login
)
459 MetaData meta
= meta(luid
, login
);
461 BasicLibrary lib
= Instance
.getInstance().getLibrary();
462 return lib
.getCover(meta
.getLuid());
468 private Image
authorCover(String author
, WLoginResult login
)
472 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(null,
474 if (metas
.size() > 0) {
475 BasicLibrary lib
= Instance
.getInstance().getLibrary();
476 img
= lib
.getCustomAuthorCover(author
);
478 img
= lib
.getCover(metas
.get(0).getLuid());
485 private void authorCover(String author
, WLoginResult login
, String luid
)
487 if (meta(luid
, login
) != null) {
488 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(null,
490 if (metas
.size() > 0) {
491 BasicLibrary lib
= Instance
.getInstance().getLibrary();
492 lib
.setAuthorCover(author
, luid
);
497 private Image
sourceCover(String source
, WLoginResult login
)
501 List
<MetaData
> metas
= new MetaResultList(metas(login
)).filter(source
,
503 if (metas
.size() > 0) {
504 BasicLibrary lib
= Instance
.getInstance().getLibrary();
505 img
= lib
.getCustomSourceCover(source
);
507 img
= lib
.getCover(metas
.get(0).getLuid());
513 private void sourceCover(String source
, WLoginResult login
, String luid
)
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
);
525 private boolean isAllowed(MetaData meta
, WLoginResult login
) {
526 MetaResultList one
= new MetaResultList(Arrays
.asList(meta
));
527 if (login
.isWl() && !whitelist
.isEmpty()) {
528 if (one
.filter(whitelist
, null, null).isEmpty()) {
532 if (login
.isBl() && !blacklist
.isEmpty()) {
533 if (!one
.filter(blacklist
, null, null).isEmpty()) {
541 private long sizeOf(Story story
) {
543 for (Chapter chap
: story
) {
544 for (Paragraph para
: chap
) {
545 if (para
.getType() == ParagraphType
.IMAGE
) {
546 size
+= para
.getContentImage().getSize();
548 size
+= para
.getContent().length();
556 public static void main(String
[] args
) throws IOException
{
558 WebLibraryServer web
= new WebLibraryServer(false);