1 package be
.nikiroo
.fanfix
.library
;
3 import java
.io
.ByteArrayInputStream
;
5 import java
.io
.FileInputStream
;
6 import java
.io
.IOException
;
7 import java
.io
.InputStream
;
8 import java
.security
.KeyStore
;
9 import java
.util
.ArrayList
;
10 import java
.util
.HashMap
;
11 import java
.util
.LinkedList
;
12 import java
.util
.List
;
15 import javax
.net
.ssl
.KeyManagerFactory
;
16 import javax
.net
.ssl
.SSLServerSocketFactory
;
18 import org
.json
.JSONArray
;
19 import org
.json
.JSONObject
;
21 import be
.nikiroo
.fanfix
.Instance
;
22 import be
.nikiroo
.fanfix
.bundles
.Config
;
23 import be
.nikiroo
.fanfix
.bundles
.UiConfig
;
24 import be
.nikiroo
.fanfix
.data
.Chapter
;
25 import be
.nikiroo
.fanfix
.data
.JsonIO
;
26 import be
.nikiroo
.fanfix
.data
.MetaData
;
27 import be
.nikiroo
.fanfix
.data
.Paragraph
;
28 import be
.nikiroo
.fanfix
.data
.Paragraph
.ParagraphType
;
29 import be
.nikiroo
.fanfix
.data
.Story
;
30 import be
.nikiroo
.fanfix
.library
.web
.WebLibraryServerIndex
;
31 import be
.nikiroo
.fanfix
.reader
.TextOutput
;
32 import be
.nikiroo
.utils
.IOUtils
;
33 import be
.nikiroo
.utils
.Image
;
34 import be
.nikiroo
.utils
.LoginResult
;
35 import be
.nikiroo
.utils
.NanoHTTPD
;
36 import be
.nikiroo
.utils
.NanoHTTPD
.IHTTPSession
;
37 import be
.nikiroo
.utils
.NanoHTTPD
.Response
;
38 import be
.nikiroo
.utils
.NanoHTTPD
.Response
.Status
;
39 import be
.nikiroo
.utils
.TraceHandler
;
40 import be
.nikiroo
.utils
.Version
;
42 public class WebLibraryServer
implements Runnable
{
43 static private String VIEWER_URL_BASE
= "/view/story/";
44 static private String VIEWER_URL
= VIEWER_URL_BASE
+ "{luid}/{chap}/{para}";
45 static private String STORY_URL_BASE
= "/story/";
46 static private String STORY_URL
= STORY_URL_BASE
+ "{luid}/{chap}/{para}";
47 static private String STORY_URL_COVER
= STORY_URL_BASE
+ "{luid}/cover";
48 static private String LIST_URL
= "/list/";
50 private class WLoginResult
extends LoginResult
{
55 public WLoginResult(boolean badLogin
, boolean badCookie
) {
56 super(badLogin
, badCookie
);
59 public WLoginResult(String who
, String key
, String subkey
, boolean rw
,
60 boolean wl
, boolean bl
) {
61 super(who
, key
, subkey
, (rw ?
"|rw" : "") + (wl ?
"|wl" : "")
62 + (bl ?
"|bl" : "") + "|");
68 public WLoginResult(String cookie
, String who
, String key
,
69 List
<String
> subkeys
) {
70 super(cookie
, who
, key
, subkeys
,
71 subkeys
== null || subkeys
.isEmpty());
74 public boolean isRw() {
75 return getOption().contains("|rw|");
78 public boolean isWl() {
79 return getOption().contains("|wl|");
82 public boolean isBl() {
83 return getOption().contains("|bl|");
87 private NanoHTTPD server
;
88 private Map
<String
, Story
> storyCache
= new HashMap
<String
, Story
>();
89 private LinkedList
<String
> storyCacheOrder
= new LinkedList
<String
>();
90 private long storyCacheSize
= 0;
91 private long maxStoryCacheSize
;
92 private TraceHandler tracer
= new TraceHandler();
94 private List
<String
> whitelist
;
95 private List
<String
> blacklist
;
97 public WebLibraryServer(boolean secure
) throws IOException
{
98 Integer port
= Instance
.getInstance().getConfig()
99 .getInteger(Config
.SERVER_PORT
);
101 throw new IOException(
102 "Cannot start web server: port not specified");
105 int cacheMb
= Instance
.getInstance().getConfig()
106 .getInteger(Config
.SERVER_MAX_CACHE_MB
, 100);
107 maxStoryCacheSize
= cacheMb
* 1024 * 1024;
109 setTraceHandler(Instance
.getInstance().getTraceHandler());
111 whitelist
= Instance
.getInstance().getConfig()
112 .getList(Config
.SERVER_WHITELIST
, new ArrayList
<String
>());
113 blacklist
= Instance
.getInstance().getConfig()
114 .getList(Config
.SERVER_BLACKLIST
, new ArrayList
<String
>());
116 SSLServerSocketFactory ssf
= null;
118 String keystorePath
= Instance
.getInstance().getConfig()
119 .getString(Config
.SERVER_SSL_KEYSTORE
, "");
120 String keystorePass
= Instance
.getInstance().getConfig()
121 .getString(Config
.SERVER_SSL_KEYSTORE_PASS
);
123 if (secure
&& keystorePath
.isEmpty()) {
124 throw new IOException(
125 "Cannot start a secure web server: no keystore.jks file povided");
128 if (!keystorePath
.isEmpty()) {
129 File keystoreFile
= new File(keystorePath
);
131 KeyStore keystore
= KeyStore
132 .getInstance(KeyStore
.getDefaultType());
133 InputStream keystoreStream
= new FileInputStream(
136 keystore
.load(keystoreStream
,
137 keystorePass
.toCharArray());
138 KeyManagerFactory keyManagerFactory
= KeyManagerFactory
139 .getInstance(KeyManagerFactory
140 .getDefaultAlgorithm());
141 keyManagerFactory
.init(keystore
,
142 keystorePass
.toCharArray());
143 ssf
= NanoHTTPD
.makeSSLSocketFactory(keystore
,
146 keystoreStream
.close();
148 } catch (Exception e
) {
149 throw new IOException(e
.getMessage());
154 server
= new NanoHTTPD(port
) {
156 public Response
serve(final IHTTPSession session
) {
157 super.serve(session
);
159 String query
= session
.getQueryParameterString(); // a=a%20b&dd=2
160 Method method
= session
.getMethod(); // GET, POST..
161 String uri
= session
.getUri(); // /home.html
163 // need them in real time (not just those sent by the UA)
164 Map
<String
, String
> cookies
= new HashMap
<String
, String
>();
165 for (String cookie
: session
.getCookies()) {
166 cookies
.put(cookie
, session
.getCookies().read(cookie
));
169 WLoginResult login
= null;
170 Map
<String
, String
> params
= session
.getParms();
171 String who
= session
.getRemoteHostName()
172 + session
.getRemoteIpAddress();
173 if (params
.get("login") != null) {
174 login
= login(who
, params
.get("password"),
175 params
.get("login"));
177 String cookie
= cookies
.get("cookie");
178 login
= login(who
, cookie
);
181 if (login
.isSuccess()) {
183 session
.getCookies().set(new Cookie("cookie",
184 login
.getCookie(), "30; path=/"));
187 String optionName
= params
.get("optionName");
188 if (optionName
!= null && !optionName
.isEmpty()) {
189 String optionNo
= params
.get("optionNo");
190 String optionValue
= params
.get("optionValue");
191 if (optionNo
!= null || optionValue
== null
192 || optionValue
.isEmpty()) {
193 session
.getCookies().delete(optionName
);
194 cookies
.remove(optionName
);
196 session
.getCookies().set(new Cookie(optionName
,
197 optionValue
, "; path=/"));
198 cookies
.put(optionName
, optionValue
);
204 if (!login
.isSuccess() && (uri
.equals("/") //
205 || uri
.startsWith(STORY_URL_BASE
) //
206 || uri
.startsWith(VIEWER_URL_BASE
) //
207 || uri
.startsWith(LIST_URL
))) {
208 rep
= loginPage(login
, uri
);
213 if (uri
.equals("/")) {
214 rep
= root(session
, cookies
, login
);
215 } else if (uri
.startsWith(LIST_URL
)) {
216 rep
= getList(uri
, login
);
217 } else if (uri
.startsWith(STORY_URL_BASE
)) {
218 rep
= getStoryPart(uri
, login
);
219 } else if (uri
.startsWith(VIEWER_URL_BASE
)) {
220 rep
= getViewer(cookies
, uri
, login
);
221 } else if (uri
.equals("/logout")) {
222 session
.getCookies().delete("cookie");
223 cookies
.remove("cookie");
224 rep
= loginPage(login
, uri
);
226 if (uri
.startsWith("/"))
227 uri
= uri
.substring(1);
228 InputStream in
= IOUtils
.openResource(
229 WebLibraryServerIndex
.class, uri
);
231 String mimeType
= MIME_PLAINTEXT
;
232 if (uri
.endsWith(".css")) {
233 mimeType
= "text/css";
234 } else if (uri
.endsWith(".html")) {
235 mimeType
= "text/html";
236 } else if (uri
.endsWith(".js")) {
237 mimeType
= "text/javascript";
239 rep
= newChunkedResponse(Status
.OK
, mimeType
,
242 getTraceHandler().trace("404: " + uri
);
247 rep
= newFixedLengthResponse(Status
.NOT_FOUND
,
248 NanoHTTPD
.MIME_PLAINTEXT
, "Not Found");
250 } catch (Exception e
) {
251 Instance
.getInstance().getTraceHandler().error(
252 new IOException("Cannot process web request",
254 rep
= newFixedLengthResponse(Status
.INTERNAL_ERROR
,
255 NanoHTTPD
.MIME_PLAINTEXT
, "An error occured");
264 getTraceHandler().trace("Install SSL on the web server...");
265 server
.makeSecure(ssf
, null);
266 getTraceHandler().trace("Done.");
273 server
.start(NanoHTTPD
.SOCKET_READ_TIMEOUT
, false);
274 } catch (IOException e
) {
275 tracer
.error(new IOException("Cannot start the web server", e
));
280 * Start the server (listen on the network for new connections).
282 * Can only be called once.
284 * This call is asynchronous, and will just start a new {@link Thread} on
285 * itself (see {@link WebLibraryServer#run()}).
287 public void start() {
288 new Thread(this).start();
292 * The traces handler for this {@link WebLibraryServer}.
294 * @return the traces handler
296 public TraceHandler
getTraceHandler() {
301 * The traces handler for this {@link WebLibraryServer}.
304 * the new traces handler
306 public void setTraceHandler(TraceHandler tracer
) {
307 if (tracer
== null) {
308 tracer
= new TraceHandler(false, false, false);
311 this.tracer
= tracer
;
314 private WLoginResult
login(String who
, String cookie
) {
315 List
<String
> subkeys
= Instance
.getInstance().getConfig()
316 .getList(Config
.SERVER_ALLOWED_SUBKEYS
);
317 String realKey
= Instance
.getInstance().getConfig()
318 .getString(Config
.SERVER_KEY
);
320 return new WLoginResult(cookie
, who
, realKey
, subkeys
);
324 private WLoginResult
login(String who
, String key
, String subkey
) {
325 String realKey
= Instance
.getInstance().getConfig()
326 .getString(Config
.SERVER_KEY
, "");
328 // I don't like NULLs...
329 key
= key
== null ?
"" : key
;
330 subkey
= subkey
== null ?
"" : subkey
;
332 if (!realKey
.equals(key
)) {
333 return new WLoginResult(true, false);
336 // defaults are true (as previous versions without the feature)
341 rw
= Instance
.getInstance().getConfig().getBoolean(Config
.SERVER_RW
,
344 List
<String
> allowed
= Instance
.getInstance().getConfig().getList(
345 Config
.SERVER_ALLOWED_SUBKEYS
, new ArrayList
<String
>());
347 if (!allowed
.isEmpty()) {
348 if (!allowed
.contains(subkey
)) {
349 return new WLoginResult(true, false);
352 if ((subkey
+ "|").contains("|rw|")) {
355 if ((subkey
+ "|").contains("|wl|")) {
356 wl
= false; // |wl| = bypass whitelist
358 if ((subkey
+ "|").contains("|bl|")) {
359 bl
= false; // |bl| = bypass blacklist
363 return new WLoginResult(who
, key
, subkey
, rw
, wl
, bl
);
366 private Response
loginPage(WLoginResult login
, String uri
) {
367 StringBuilder builder
= new StringBuilder();
369 appendPreHtml(builder
, true);
371 if (login
.isBadLogin()) {
372 builder
.append("<div class='error'>Bad login or password</div>");
373 } else if (login
.isBadCookie()) {
374 builder
.append("<div class='error'>Your session timed out</div>");
377 if (uri
.equals("/logout")) {
382 "<form method='POST' action='" + uri
+ "' class='login'>\n");
384 "<p>You must be logged into the system to see the stories.</p>");
385 builder
.append("\t<input type='text' name='login' />\n");
386 builder
.append("\t<input type='password' name='password' />\n");
387 builder
.append("\t<input type='submit' value='Login' />\n");
388 builder
.append("</form>\n");
390 appendPostHtml(builder
);
392 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
393 NanoHTTPD
.MIME_HTML
, builder
.toString());
396 protected Response
getList(String uri
, WLoginResult login
)
398 if (uri
.equals("/list/luids")) {
399 List
<JSONObject
> jsons
= new ArrayList
<JSONObject
>();
400 for (MetaData meta
: metas(login
)) {
401 jsons
.add(JsonIO
.toJson(meta
));
404 return newInputStreamResponse("application/json",
405 new ByteArrayInputStream(
406 new JSONArray(jsons
).toString().getBytes()));
409 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
410 NanoHTTPD
.MIME_PLAINTEXT
, null);
413 private Response
root(IHTTPSession session
, Map
<String
, String
> cookies
,
414 WLoginResult login
) throws IOException
{
415 BasicLibrary lib
= Instance
.getInstance().getLibrary();
416 MetaResultList result
= new MetaResultList(metas(login
));
417 StringBuilder builder
= new StringBuilder();
419 appendPreHtml(builder
, true);
421 Map
<String
, String
> params
= session
.getParms();
423 String filter
= cookies
.get("filter");
424 if (params
.get("optionNo") != null)
426 if (filter
== null) {
430 String browser
= params
.get("browser") == null ?
""
431 : params
.get("browser");
432 String browser2
= params
.get("browser2") == null ?
""
433 : params
.get("browser2");
434 String browser3
= params
.get("browser3") == null ?
""
435 : params
.get("browser3");
437 String filterSource
= null;
438 String filterAuthor
= null;
439 String filterTag
= null;
441 // TODO: javascript in realtime, using visible=false + hide [submit]
443 builder
.append("<form class='browser'>\n");
444 builder
.append("<div class='breadcrumbs'>\n");
446 builder
.append("\t<select name='browser'>");
447 appendOption(builder
, 2, "", "", browser
);
448 appendOption(builder
, 2, "Sources", "sources", browser
);
449 appendOption(builder
, 2, "Authors", "authors", browser
);
450 appendOption(builder
, 2, "Tags", "tags", browser
);
451 builder
.append("\t</select>\n");
453 if (!browser
.isEmpty()) {
454 builder
.append("\t<select name='browser2'>");
455 if (browser
.equals("sources")) {
456 filterSource
= browser2
.isEmpty() ? filterSource
: browser2
;
457 // TODO: if 1 group -> no group
458 appendOption(builder
, 2, "", "", browser2
);
459 Map
<String
, List
<String
>> sources
= result
.getSourcesGrouped();
460 for (String source
: sources
.keySet()) {
461 appendOption(builder
, 2, source
, source
, browser2
);
463 } else if (browser
.equals("authors")) {
464 filterAuthor
= browser2
.isEmpty() ? filterAuthor
: browser2
;
465 // TODO: if 1 group -> no group
466 appendOption(builder
, 2, "", "", browser2
);
467 Map
<String
, List
<String
>> authors
= result
.getAuthorsGrouped();
468 for (String author
: authors
.keySet()) {
469 appendOption(builder
, 2, author
, author
, browser2
);
471 } else if (browser
.equals("tags")) {
472 filterTag
= browser2
.isEmpty() ? filterTag
: browser2
;
473 appendOption(builder
, 2, "", "", browser2
);
474 for (String tag
: result
.getTags()) {
475 appendOption(builder
, 2, tag
, tag
, browser2
);
478 builder
.append("\t</select>\n");
481 if (!browser2
.isEmpty()) {
482 if (browser
.equals("sources")) {
483 filterSource
= browser3
.isEmpty() ? filterSource
: browser3
;
484 Map
<String
, List
<String
>> sourcesGrouped
= result
485 .getSourcesGrouped();
486 List
<String
> sources
= sourcesGrouped
.get(browser2
);
487 if (sources
!= null && !sources
.isEmpty()) {
488 // TODO: single empty value
489 builder
.append("\t<select name='browser3'>");
490 appendOption(builder
, 2, "", "", browser3
);
491 for (String source
: sources
) {
492 appendOption(builder
, 2, source
, source
, browser3
);
494 builder
.append("\t</select>\n");
496 } else if (browser
.equals("authors")) {
497 filterAuthor
= browser3
.isEmpty() ? filterAuthor
: browser3
;
498 Map
<String
, List
<String
>> authorsGrouped
= result
499 .getAuthorsGrouped();
500 List
<String
> authors
= authorsGrouped
.get(browser2
);
501 if (authors
!= null && !authors
.isEmpty()) {
502 // TODO: single empty value
503 builder
.append("\t<select name='browser3'>");
504 appendOption(builder
, 2, "", "", browser3
);
505 for (String author
: authors
) {
506 appendOption(builder
, 2, author
, author
, browser3
);
508 builder
.append("\t</select>\n");
513 builder
.append("\t<input type='submit' value='Select'/>\n");
514 builder
.append("</div>\n");
516 // TODO: javascript in realtime, using visible=false + hide [submit]
517 builder
.append("<div class='filter'>\n");
518 builder
.append("\t<span class='label'>Filter: </span>\n");
520 "\t<input name='optionName' type='hidden' value='filter' />\n");
521 builder
.append("\t<input name='optionValue' type='text' value='"
522 + filter
+ "' place-holder='...' />\n");
523 builder
.append("\t<input name='optionNo' type='submit' value='x' />");
525 "\t<input name='submit' type='submit' value='Filter' />\n");
526 builder
.append("</div>\n");
527 builder
.append("</form>\n");
529 builder
.append("\t<div class='books'>");
530 for (MetaData meta
: result
.getMetas()) {
531 if (!filter
.isEmpty() && !meta
.getTitle().toLowerCase()
532 .contains(filter
.toLowerCase())) {
537 if (filterSource
!= null
538 && !filterSource
.equals(meta
.getSource())) {
543 if (filterAuthor
!= null
544 && !filterAuthor
.equals(meta
.getAuthor())) {
548 if (filterTag
!= null && !meta
.getTags().contains(filterTag
)) {
552 builder
.append("<div class='book_line'>");
553 builder
.append("<a href='");
554 builder
.append(getViewUrl(meta
.getLuid(), null, null));
556 builder
.append(" class='link'>");
558 if (lib
.isCached(meta
.getLuid())) {
561 "<span class='cache_icon cached'>◉</span>");
565 "<span class='cache_icon uncached'>○</span>");
567 builder
.append("<span class='luid'>");
568 builder
.append(meta
.getLuid());
569 builder
.append("</span>");
570 builder
.append("<span class='title'>");
571 builder
.append(meta
.getTitle());
572 builder
.append("</span>");
573 builder
.append("<span class='author'>");
574 if (meta
.getAuthor() != null && !meta
.getAuthor().isEmpty()) {
575 builder
.append("(").append(meta
.getAuthor()).append(")");
577 builder
.append("</span>");
578 builder
.append("</a></div>\n");
580 builder
.append("</div>");
582 appendPostHtml(builder
);
583 return NanoHTTPD
.newFixedLengthResponse(builder
.toString());
586 // /story/luid/chapter/para <-- text/image
587 // /story/luid/cover <-- image
588 // /story/luid/metadata <-- json
589 // /story/luid/json <-- json, whole chapter (no images)
590 private Response
getStoryPart(String uri
, WLoginResult login
) {
591 String
[] cover
= uri
.split("/");
594 if (cover
.length
< off
+ 2) {
595 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
596 NanoHTTPD
.MIME_PLAINTEXT
, null);
599 String luid
= cover
[off
+ 0];
600 String chapterStr
= cover
[off
+ 1];
601 String imageStr
= cover
.length
< off
+ 3 ?
null : cover
[off
+ 2];
603 // 1-based (0 = desc)
605 if (chapterStr
!= null && !"cover".equals(chapterStr
)
606 && !"metadata".equals(chapterStr
)
607 && !"json".equals(chapterStr
)) {
609 chapter
= Integer
.parseInt(chapterStr
);
611 throw new NumberFormatException();
613 } catch (NumberFormatException e
) {
614 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
615 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
621 if (imageStr
!= null) {
623 paragraph
= Integer
.parseInt(imageStr
);
625 throw new NumberFormatException();
627 } catch (NumberFormatException e
) {
628 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
629 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
633 String mimeType
= NanoHTTPD
.MIME_PLAINTEXT
;
634 InputStream in
= null;
636 if ("cover".equals(chapterStr
)) {
637 Image img
= getCover(luid
, login
);
639 in
= img
.newInputStream();
641 // TODO: get correct image type
642 mimeType
= "image/png";
643 } else if ("metadata".equals(chapterStr
)) {
644 MetaData meta
= meta(luid
, login
);
645 JSONObject json
= JsonIO
.toJson(meta
);
646 mimeType
= "application/json";
647 in
= new ByteArrayInputStream(json
.toString().getBytes());
648 } else if ("json".equals(chapterStr
)) {
649 Story story
= story(luid
, login
);
650 JSONObject json
= JsonIO
.toJson(story
);
651 mimeType
= "application/json";
652 in
= new ByteArrayInputStream(json
.toString().getBytes());
654 Story story
= story(luid
, login
);
657 StringBuilder builder
= new StringBuilder();
658 for (Paragraph p
: story
.getMeta().getResume()) {
659 if (builder
.length() == 0) {
660 builder
.append("\n");
662 builder
.append(p
.getContent());
665 in
= new ByteArrayInputStream(
666 builder
.toString().getBytes("utf-8"));
668 Paragraph para
= story
.getChapters().get(chapter
- 1)
669 .getParagraphs().get(paragraph
- 1);
670 Image img
= para
.getContentImage();
671 if (para
.getType() == ParagraphType
.IMAGE
) {
672 // TODO: get correct image type
673 mimeType
= "image/png";
674 in
= img
.newInputStream();
676 in
= new ByteArrayInputStream(
677 para
.getContent().getBytes("utf-8"));
682 } catch (IndexOutOfBoundsException e
) {
683 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
684 NanoHTTPD
.MIME_PLAINTEXT
,
685 "Chapter or paragraph does not exist");
686 } catch (IOException e
) {
687 Instance
.getInstance().getTraceHandler()
688 .error(new IOException("Cannot get image: " + uri
, e
));
689 return NanoHTTPD
.newFixedLengthResponse(Status
.INTERNAL_ERROR
,
690 NanoHTTPD
.MIME_PLAINTEXT
, "Error when processing request");
693 return newInputStreamResponse(mimeType
, in
);
696 private Response
getViewer(Map
<String
, String
> cookies
, String uri
,
697 WLoginResult login
) {
698 String
[] cover
= uri
.split("/");
701 if (cover
.length
< off
+ 2) {
702 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
703 NanoHTTPD
.MIME_PLAINTEXT
, null);
706 String type
= cover
[off
+ 0];
707 String luid
= cover
[off
+ 1];
708 String chapterStr
= cover
.length
< off
+ 3 ?
null : cover
[off
+ 2];
709 String paragraphStr
= cover
.length
< off
+ 4 ?
null : cover
[off
+ 3];
711 // 1-based (0 = desc)
713 if (chapterStr
!= null) {
715 chapter
= Integer
.parseInt(chapterStr
);
717 throw new NumberFormatException();
719 } catch (NumberFormatException e
) {
720 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
721 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
727 if (paragraphStr
!= null) {
729 paragraph
= Integer
.parseInt(paragraphStr
);
730 if (paragraph
<= 0) {
731 throw new NumberFormatException();
733 } catch (NumberFormatException e
) {
734 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
735 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
740 Story story
= story(luid
, login
);
742 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
743 NanoHTTPD
.MIME_PLAINTEXT
, "Story not found");
746 StringBuilder builder
= new StringBuilder();
747 appendPreHtml(builder
, false);
749 // For images documents, always go to the images if not chap 0 desc
750 if (story
.getMeta().isImageDocument()) {
751 if (chapter
> 0 && paragraph
<= 0)
757 chap
= story
.getMeta().getResume();
760 chap
= story
.getChapters().get(chapter
- 1);
761 } catch (IndexOutOfBoundsException e
) {
762 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
763 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter not found");
767 String first
, previous
, next
, last
;
769 StringBuilder content
= new StringBuilder();
771 String disabledLeft
= "";
772 String disabledRight
= "";
773 String disabledZoomReal
= "";
774 String disabledZoomWidth
= "";
775 String disabledZoomHeight
= "";
777 if (paragraph
<= 0) {
778 first
= getViewUrl(luid
, 0, null);
779 previous
= getViewUrl(luid
, (Math
.max(chapter
- 1, 0)), null);
780 next
= getViewUrl(luid
,
781 (Math
.min(chapter
+ 1, story
.getChapters().size())),
783 last
= getViewUrl(luid
, story
.getChapters().size(), null);
785 StringBuilder desc
= new StringBuilder();
788 desc
.append("<h1 class='title'>");
789 desc
.append(story
.getMeta().getTitle());
790 desc
.append("</h1>\n");
791 desc
.append("<div class='desc'>\n");
792 desc
.append("\t<a href='" + next
+ "' class='cover'>\n");
793 desc
.append("\t\t<img src='/story/" + luid
+ "/cover'/>\n");
794 desc
.append("\t</a>\n");
795 desc
.append("\t<table class='details'>\n");
796 Map
<String
, String
> details
= BasicLibrary
797 .getMetaDesc(story
.getMeta());
798 for (String key
: details
.keySet()) {
799 appendTableRow(desc
, 2, key
, details
.get(key
));
801 desc
.append("\t</table>\n");
802 desc
.append("</div>\n");
803 desc
.append("<h1 class='title'>Description</h1>\n");
806 content
.append("<div class='viewer text'>\n");
807 content
.append(desc
);
808 String description
= new TextOutput(false).convert(chap
,
810 content
.append(chap
.getParagraphs().size() <= 0
811 ?
"No content provided."
813 content
.append("</div>\n");
816 disabledLeft
= " disabled='disbaled'";
817 if (chapter
>= story
.getChapters().size())
818 disabledRight
= " disabled='disbaled'";
820 first
= getViewUrl(luid
, chapter
, 1);
821 previous
= getViewUrl(luid
, chapter
,
822 (Math
.max(paragraph
- 1, 1)));
823 next
= getViewUrl(luid
, chapter
,
824 (Math
.min(paragraph
+ 1, chap
.getParagraphs().size())));
825 last
= getViewUrl(luid
, chapter
, chap
.getParagraphs().size());
828 disabledLeft
= " disabled='disbaled'";
829 if (paragraph
>= chap
.getParagraphs().size())
830 disabledRight
= " disabled='disbaled'";
832 // First -> previous *chapter*
835 first
= getViewUrl(luid
, (Math
.max(chapter
- 1, 0)), null);
836 if (paragraph
<= 1) {
840 Paragraph para
= null;
842 para
= chap
.getParagraphs().get(paragraph
- 1);
843 } catch (IndexOutOfBoundsException e
) {
844 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
845 NanoHTTPD
.MIME_PLAINTEXT
,
846 "Paragraph " + paragraph
+ " not found");
849 if (para
.getType() == ParagraphType
.IMAGE
) {
850 String zoomStyle
= "max-width: 100%;";
851 disabledZoomWidth
= " disabled='disabled'";
852 String zoomOption
= cookies
.get("zoom");
853 if (zoomOption
!= null && !zoomOption
.isEmpty()) {
854 if (zoomOption
.equals("real")) {
856 disabledZoomWidth
= "";
857 disabledZoomReal
= " disabled='disabled'";
858 } else if (zoomOption
.equals("width")) {
859 zoomStyle
= "max-width: 100%;";
860 } else if (zoomOption
.equals("height")) {
861 // see height of navbar + optionbar
862 zoomStyle
= "max-height: calc(100% - 128px);";
863 disabledZoomWidth
= "";
864 disabledZoomHeight
= " disabled='disabled'";
868 String javascript
= "document.getElementById(\"previous\").click(); return false;";
869 content
.append(String
.format("" //
870 + "<a class='viewer link' oncontextmenu='%s' href='%s'>"
871 + "<img class='viewer img' style='%s' src='%s'/>"
876 getStoryUrl(luid
, chapter
, paragraph
)));
878 content
.append(String
.format("" //
879 + "<div class='viewer text'>%s</div>", //
884 builder
.append(String
.format("" //
885 + "<div class='bar navbar'>\n" //
886 + "\t<a%s class='button first' href='%s'><<</a>\n"//
887 + "\t<a%s id='previous' class='button previous' href='%s'><</a>\n" //
888 + "\t<div class='gotobox itemsbox'>\n" //
889 + "\t\t<div class='button goto'>%d</div>\n" //
890 + "\t\t<div class='items goto'>\n", //
891 disabledLeft
, first
, //
892 disabledLeft
, previous
, //
893 paragraph
> 0 ? paragraph
: chapter
//
896 // List of chap/para links
898 appendItemA(builder
, 3, getViewUrl(luid
, 0, null), "Description",
899 paragraph
== 0 && chapter
== 0);
901 for (int i
= 1; i
<= chap
.getParagraphs().size(); i
++) {
902 appendItemA(builder
, 3, getViewUrl(luid
, chapter
, i
),
903 "Image " + i
, paragraph
== i
);
907 for (Chapter c
: story
.getChapters()) {
908 String chapName
= "Chapter " + c
.getNumber();
909 if (c
.getName() != null && !c
.getName().isEmpty()) {
910 chapName
+= ": " + c
.getName();
913 appendItemA(builder
, 3, getViewUrl(luid
, i
, null), chapName
,
920 builder
.append(String
.format("" //
923 + "\t<a%s class='button next' href='%s'>></a>\n" //
924 + "\t<a%s class='button last' href='%s'>>></a>\n"//
926 disabledRight
, next
, //
927 disabledRight
, last
//
930 builder
.append(content
);
932 builder
.append("<div class='bar optionbar ");
934 builder
.append("s4");
936 builder
.append("s1");
938 builder
.append("'>\n");
939 builder
.append(" <a class='button back' href='/'>BACK</a>\n");
942 builder
.append(String
.format("" //
943 + "\t<a%s class='button zoomreal' href='%s'>REAL</a>\n"//
944 + "\t<a%s class='button zoomwidth' href='%s'>WIDTH</a>\n"//
945 + "\t<a%s class='button zoomheight' href='%s'>HEIGHT</a>\n"//
948 uri
+ "?optionName=zoom&optionValue=real", //
950 uri
+ "?optionName=zoom&optionValue=width", //
952 uri
+ "?optionName=zoom&optionValue=height" //
956 appendPostHtml(builder
);
957 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
958 NanoHTTPD
.MIME_HTML
, builder
.toString());
959 } catch (IOException e
) {
960 Instance
.getInstance().getTraceHandler()
961 .error(new IOException("Cannot get image: " + uri
, e
));
962 return NanoHTTPD
.newFixedLengthResponse(Status
.INTERNAL_ERROR
,
963 NanoHTTPD
.MIME_PLAINTEXT
, "Error when processing request");
967 private Response
newInputStreamResponse(String mimeType
, InputStream in
) {
969 return NanoHTTPD
.newFixedLengthResponse(Status
.NO_CONTENT
, "",
972 return NanoHTTPD
.newChunkedResponse(Status
.OK
, mimeType
, in
);
975 private String
getContentOf(String file
) {
976 InputStream in
= IOUtils
.openResource(WebLibraryServerIndex
.class,
980 return IOUtils
.readSmallStream(in
);
981 } catch (IOException e
) {
982 Instance
.getInstance().getTraceHandler().error(
983 new IOException("Cannot get file: index.pre.html", e
));
990 private String
getViewUrl(String luid
, Integer chap
, Integer para
) {
992 .replace("{luid}", luid
) //
993 .replace("/{chap}", chap
== null ?
"" : "/" + chap
) //
995 (chap
== null || para
== null) ?
"" : "/" + para
);
998 private String
getStoryUrl(String luid
, int chap
, Integer para
) {
1000 .replace("{luid}", luid
) //
1001 .replace("{chap}", Integer
.toString(chap
)) //
1002 .replace("{para}", para
== null ?
"" : Integer
.toString(para
));
1005 private String
getStoryUrlCover(String luid
) {
1006 return STORY_URL_COVER
//
1007 .replace("{luid}", luid
);
1010 private boolean isAllowed(MetaData meta
, WLoginResult login
) {
1011 if (login
.isWl() && !whitelist
.isEmpty()
1012 && !whitelist
.contains(meta
.getSource())) {
1015 if (login
.isBl() && blacklist
.contains(meta
.getSource())) {
1022 private List
<MetaData
> metas(WLoginResult login
) throws IOException
{
1023 BasicLibrary lib
= Instance
.getInstance().getLibrary();
1024 List
<MetaData
> metas
= new ArrayList
<MetaData
>();
1025 for (MetaData meta
: lib
.getList().getMetas()) {
1026 if (isAllowed(meta
, login
)) {
1034 private MetaData
meta(String luid
, WLoginResult login
) throws IOException
{
1035 BasicLibrary lib
= Instance
.getInstance().getLibrary();
1036 MetaData meta
= lib
.getInfo(luid
);
1037 if (!isAllowed(meta
, login
))
1043 private Image
getCover(String luid
, WLoginResult login
) throws IOException
{
1044 MetaData meta
= meta(luid
, login
);
1046 BasicLibrary lib
= Instance
.getInstance().getLibrary();
1047 return lib
.getCover(meta
.getLuid());
1053 // NULL if not whitelist OK or if not found
1054 private Story
story(String luid
, WLoginResult login
) throws IOException
{
1055 synchronized (storyCache
) {
1056 if (storyCache
.containsKey(luid
)) {
1057 Story story
= storyCache
.get(luid
);
1058 if (!isAllowed(story
.getMeta(), login
))
1066 MetaData meta
= meta(luid
, login
);
1068 BasicLibrary lib
= Instance
.getInstance().getLibrary();
1069 story
= lib
.getStory(luid
, null);
1070 long size
= sizeOf(story
);
1072 synchronized (storyCache
) {
1073 // Could have been added by another request
1074 if (!storyCache
.containsKey(luid
)) {
1075 while (!storyCacheOrder
.isEmpty()
1076 && storyCacheSize
+ size
> maxStoryCacheSize
) {
1077 String oldestLuid
= storyCacheOrder
.removeFirst();
1078 Story oldestStory
= storyCache
.remove(oldestLuid
);
1079 maxStoryCacheSize
-= sizeOf(oldestStory
);
1082 storyCacheOrder
.add(luid
);
1083 storyCache
.put(luid
, story
);
1091 private long sizeOf(Story story
) {
1093 for (Chapter chap
: story
) {
1094 for (Paragraph para
: chap
) {
1095 if (para
.getType() == ParagraphType
.IMAGE
) {
1096 size
+= para
.getContentImage().getSize();
1098 size
+= para
.getContent().length();
1106 private void appendPreHtml(StringBuilder builder
, boolean banner
) {
1107 String favicon
= "favicon.ico";
1108 String icon
= Instance
.getInstance().getUiConfig()
1109 .getString(UiConfig
.PROGRAM_ICON
);
1111 favicon
= "icon_" + icon
.replace("-", "_") + ".png";
1115 getContentOf("index.pre.html").replace("favicon.ico", favicon
));
1118 builder
.append("<div class='banner'>\n");
1119 builder
.append("\t<img class='ico' src='/") //
1122 builder
.append("\t<h1>Fanfix</h1>\n");
1123 builder
.append("\t<h2>") //
1124 .append(Version
.getCurrentVersion()) //
1126 builder
.append("</div>\n");
1130 private void appendPostHtml(StringBuilder builder
) {
1131 builder
.append(getContentOf("index.post.html"));
1134 private void appendOption(StringBuilder builder
, int depth
, String name
,
1135 String value
, String selected
) {
1136 for (int i
= 0; i
< depth
; i
++) {
1137 builder
.append("\t");
1139 builder
.append("<option value='").append(value
).append("'");
1140 if (value
.equals(selected
)) {
1141 builder
.append(" selected='selected'");
1143 builder
.append(">").append(name
).append("</option>\n");
1146 private void appendTableRow(StringBuilder builder
, int depth
,
1148 for (int i
= 0; i
< depth
; i
++) {
1149 builder
.append("\t");
1153 builder
.append("<tr>");
1154 for (String td
: tds
) {
1155 builder
.append("<td class='col");
1156 builder
.append(col
++);
1157 builder
.append("'>");
1159 builder
.append("</td>");
1161 builder
.append("</tr>\n");
1164 private void appendItemA(StringBuilder builder
, int depth
, String link
,
1165 String name
, boolean selected
) {
1166 for (int i
= 0; i
< depth
; i
++) {
1167 builder
.append("\t");
1170 builder
.append("<a href='");
1171 builder
.append(link
);
1172 builder
.append("' class='item goto");
1174 builder
.append(" selected");
1176 builder
.append("'>");
1177 builder
.append(name
);
1178 builder
.append("</a>\n");
1181 public static void main(String
[] args
) throws IOException
{
1183 WebLibraryServer web
= new WebLibraryServer(false);