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
.HashMap
;
8 import java
.util
.LinkedList
;
12 import org
.json
.JSONArray
;
13 import org
.json
.JSONObject
;
15 import be
.nikiroo
.fanfix
.Instance
;
16 import be
.nikiroo
.fanfix
.bundles
.Config
;
17 import be
.nikiroo
.fanfix
.data
.Chapter
;
18 import be
.nikiroo
.fanfix
.data
.JsonIO
;
19 import be
.nikiroo
.fanfix
.data
.MetaData
;
20 import be
.nikiroo
.fanfix
.data
.Paragraph
;
21 import be
.nikiroo
.fanfix
.data
.Paragraph
.ParagraphType
;
22 import be
.nikiroo
.fanfix
.data
.Story
;
23 import be
.nikiroo
.utils
.Image
;
24 import be
.nikiroo
.utils
.LoginResult
;
25 import be
.nikiroo
.utils
.NanoHTTPD
;
26 import be
.nikiroo
.utils
.NanoHTTPD
.Response
;
27 import be
.nikiroo
.utils
.NanoHTTPD
.Response
.Status
;
29 public class WebLibraryServer
extends WebLibraryServerHtml
{
30 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" : "") + "|");
48 public WLoginResult(String cookie
, String who
, String key
,
49 List
<String
> subkeys
) {
50 super(cookie
, who
, key
, subkeys
,
51 subkeys
== null || subkeys
.isEmpty());
54 public boolean isRw() {
55 return getOption().contains("|rw|");
58 public boolean isWl() {
59 return getOption().contains("|wl|");
62 public boolean isBl() {
63 return getOption().contains("|bl|");
67 private Map
<String
, Story
> storyCache
= new HashMap
<String
, Story
>();
68 private LinkedList
<String
> storyCacheOrder
= new LinkedList
<String
>();
69 private long storyCacheSize
= 0;
70 private long maxStoryCacheSize
;
72 private List
<String
> whitelist
;
73 private List
<String
> blacklist
;
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 WLoginResult
login(boolean badLogin
, boolean badCookie
) {
104 return new WLoginResult(false, false);
108 protected WLoginResult
login(String who
, String cookie
) {
109 List
<String
> subkeys
= Instance
.getInstance().getConfig()
110 .getList(Config
.SERVER_ALLOWED_SUBKEYS
);
111 String realKey
= Instance
.getInstance().getConfig()
112 .getString(Config
.SERVER_KEY
);
114 return new WLoginResult(cookie
, who
, realKey
, subkeys
);
119 protected WLoginResult
login(String who
, String key
, String subkey
) {
120 String realKey
= Instance
.getInstance().getConfig()
121 .getString(Config
.SERVER_KEY
, "");
123 // I don't like NULLs...
124 key
= key
== null ?
"" : key
;
125 subkey
= subkey
== null ?
"" : subkey
;
127 if (!realKey
.equals(key
)) {
128 return new WLoginResult(true, false);
131 // defaults are true (as previous versions without the feature)
136 rw
= Instance
.getInstance().getConfig().getBoolean(Config
.SERVER_RW
,
139 List
<String
> allowed
= Instance
.getInstance().getConfig().getList(
140 Config
.SERVER_ALLOWED_SUBKEYS
, new ArrayList
<String
>());
142 if (!allowed
.isEmpty()) {
143 if (!allowed
.contains(subkey
)) {
144 return new WLoginResult(true, false);
147 if ((subkey
+ "|").contains("|rw|")) {
150 if ((subkey
+ "|").contains("|wl|")) {
151 wl
= false; // |wl| = bypass whitelist
153 if ((subkey
+ "|").contains("|bl|")) {
154 bl
= false; // |bl| = bypass blacklist
158 return new WLoginResult(who
, key
, subkey
, rw
, wl
, bl
);
162 protected Response
getList(String uri
, WLoginResult login
)
164 if (WebLibraryUrls
.LIST_URL_METADATA
.equals(uri
)) {
165 List
<JSONObject
> jsons
= new ArrayList
<JSONObject
>();
166 for (MetaData meta
: metas(login
)) {
167 jsons
.add(JsonIO
.toJson(meta
));
170 return newInputStreamResponse("application/json",
171 new ByteArrayInputStream(
172 new JSONArray(jsons
).toString().getBytes()));
175 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
176 NanoHTTPD
.MIME_PLAINTEXT
, null);
179 // /story/luid/chapter/para <-- text/image
180 // /story/luid/cover <-- image
181 // /story/luid/metadata <-- json
182 // /story/luid/json <-- json, whole chapter (no images)
184 protected Response
getStoryPart(String uri
, WLoginResult login
) {
185 String
[] cover
= uri
.split("/");
188 if (cover
.length
< off
+ 2) {
189 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
190 NanoHTTPD
.MIME_PLAINTEXT
, null);
193 String luid
= cover
[off
+ 0];
194 String chapterStr
= cover
[off
+ 1];
195 String imageStr
= cover
.length
< off
+ 3 ?
null : cover
[off
+ 2];
197 // 1-based (0 = desc)
199 if (chapterStr
!= null && !"cover".equals(chapterStr
)
200 && !"metadata".equals(chapterStr
)
201 && !"json".equals(chapterStr
)) {
203 chapter
= Integer
.parseInt(chapterStr
);
205 throw new NumberFormatException();
207 } catch (NumberFormatException e
) {
208 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
209 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
215 if (imageStr
!= null) {
217 paragraph
= Integer
.parseInt(imageStr
);
219 throw new NumberFormatException();
221 } catch (NumberFormatException e
) {
222 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
223 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
227 String mimeType
= NanoHTTPD
.MIME_PLAINTEXT
;
228 InputStream in
= null;
230 if ("cover".equals(chapterStr
)) {
231 Image img
= cover(luid
, login
);
233 in
= img
.newInputStream();
235 // TODO: get correct image type
236 mimeType
= "image/png";
237 } else if ("metadata".equals(chapterStr
)) {
238 MetaData meta
= meta(luid
, login
);
239 JSONObject json
= JsonIO
.toJson(meta
);
240 mimeType
= "application/json";
241 in
= new ByteArrayInputStream(json
.toString().getBytes());
242 } else if ("json".equals(chapterStr
)) {
243 Story story
= story(luid
, login
);
244 JSONObject json
= JsonIO
.toJson(story
);
245 mimeType
= "application/json";
246 in
= new ByteArrayInputStream(json
.toString().getBytes());
248 Story story
= story(luid
, login
);
251 StringBuilder builder
= new StringBuilder();
252 for (Paragraph p
: story
.getMeta().getResume()) {
253 if (builder
.length() == 0) {
254 builder
.append("\n");
256 builder
.append(p
.getContent());
259 in
= new ByteArrayInputStream(
260 builder
.toString().getBytes("utf-8"));
262 Paragraph para
= story
.getChapters().get(chapter
- 1)
263 .getParagraphs().get(paragraph
- 1);
264 Image img
= para
.getContentImage();
265 if (para
.getType() == ParagraphType
.IMAGE
) {
266 // TODO: get correct image type
267 mimeType
= "image/png";
268 in
= img
.newInputStream();
270 in
= new ByteArrayInputStream(
271 para
.getContent().getBytes("utf-8"));
276 } catch (IndexOutOfBoundsException e
) {
277 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
278 NanoHTTPD
.MIME_PLAINTEXT
,
279 "Chapter or paragraph does not exist");
280 } catch (IOException e
) {
281 Instance
.getInstance().getTraceHandler()
282 .error(new IOException("Cannot get image: " + uri
, e
));
283 return NanoHTTPD
.newFixedLengthResponse(Status
.INTERNAL_ERROR
,
284 NanoHTTPD
.MIME_PLAINTEXT
, "Error when processing request");
287 return newInputStreamResponse(mimeType
, in
);
291 protected List
<MetaData
> metas(WLoginResult login
) throws IOException
{
292 BasicLibrary lib
= Instance
.getInstance().getLibrary();
293 List
<MetaData
> metas
= new ArrayList
<MetaData
>();
294 for (MetaData meta
: lib
.getList().getMetas()) {
295 if (isAllowed(meta
, login
)) {
303 // NULL if not whitelist OK or if not found
305 protected Story
story(String luid
, WLoginResult login
) throws IOException
{
306 synchronized (storyCache
) {
307 if (storyCache
.containsKey(luid
)) {
308 Story story
= storyCache
.get(luid
);
309 if (!isAllowed(story
.getMeta(), login
))
317 MetaData meta
= meta(luid
, login
);
319 BasicLibrary lib
= Instance
.getInstance().getLibrary();
320 story
= lib
.getStory(luid
, null);
321 long size
= sizeOf(story
);
323 synchronized (storyCache
) {
324 // Could have been added by another request
325 if (!storyCache
.containsKey(luid
)) {
326 while (!storyCacheOrder
.isEmpty()
327 && storyCacheSize
+ size
> maxStoryCacheSize
) {
328 String oldestLuid
= storyCacheOrder
.removeFirst();
329 Story oldestStory
= storyCache
.remove(oldestLuid
);
330 maxStoryCacheSize
-= sizeOf(oldestStory
);
333 storyCacheOrder
.add(luid
);
334 storyCache
.put(luid
, story
);
342 private MetaData
meta(String luid
, WLoginResult login
) throws IOException
{
343 BasicLibrary lib
= Instance
.getInstance().getLibrary();
344 MetaData meta
= lib
.getInfo(luid
);
345 if (!isAllowed(meta
, login
))
351 private Image
cover(String luid
, WLoginResult login
) throws IOException
{
352 MetaData meta
= meta(luid
, login
);
354 BasicLibrary lib
= Instance
.getInstance().getLibrary();
355 return lib
.getCover(meta
.getLuid());
361 private boolean isAllowed(MetaData meta
, WLoginResult login
) {
362 if (login
.isWl() && !whitelist
.isEmpty()
363 && !whitelist
.contains(meta
.getSource())) {
366 if (login
.isBl() && blacklist
.contains(meta
.getSource())) {
373 private long sizeOf(Story story
) {
375 for (Chapter chap
: story
) {
376 for (Paragraph para
: chap
) {
377 if (para
.getType() == ParagraphType
.IMAGE
) {
378 size
+= para
.getContentImage().getSize();
380 size
+= para
.getContent().length();
388 public static void main(String
[] args
) throws IOException
{
390 WebLibraryServer web
= new WebLibraryServer(false);