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 private class WLoginResult
extends LoginResult
{
48 public WLoginResult(boolean badLogin
, boolean badCookie
) {
49 super(badLogin
, badCookie
);
52 public WLoginResult(String who
, String key
, String subkey
, boolean rw
,
53 boolean wl
, boolean bl
) {
54 super(who
, key
, subkey
, (rw ?
"|rw" : "") + (wl ?
"|wl" : "")
55 + (bl ?
"|bl" : "") + "|");
61 public WLoginResult(String cookie
, String who
, String key
,
62 List
<String
> subkeys
) {
63 super(cookie
, who
, key
, subkeys
,
64 subkeys
== null || subkeys
.isEmpty());
67 public boolean isRw() {
68 return getOption().contains("|rw|");
71 public boolean isWl() {
72 return getOption().contains("|wl|");
75 public boolean isBl() {
76 return getOption().contains("|bl|");
80 private NanoHTTPD server
;
81 private Map
<String
, Story
> storyCache
= new HashMap
<String
, Story
>();
82 private LinkedList
<String
> storyCacheOrder
= new LinkedList
<String
>();
83 private long storyCacheSize
= 0;
84 private long maxStoryCacheSize
;
85 private TraceHandler tracer
= new TraceHandler();
87 private List
<String
> whitelist
;
88 private List
<String
> blacklist
;
90 public WebLibraryServer(boolean secure
) throws IOException
{
91 Integer port
= Instance
.getInstance().getConfig()
92 .getInteger(Config
.SERVER_PORT
);
94 throw new IOException(
95 "Cannot start web server: port not specified");
98 int cacheMb
= Instance
.getInstance().getConfig()
99 .getInteger(Config
.SERVER_MAX_CACHE_MB
, 100);
100 maxStoryCacheSize
= cacheMb
* 1024 * 1024;
102 setTraceHandler(Instance
.getInstance().getTraceHandler());
104 whitelist
= Instance
.getInstance().getConfig()
105 .getList(Config
.SERVER_WHITELIST
, new ArrayList
<String
>());
106 blacklist
= Instance
.getInstance().getConfig()
107 .getList(Config
.SERVER_BLACKLIST
, new ArrayList
<String
>());
109 SSLServerSocketFactory ssf
= null;
111 String keystorePath
= Instance
.getInstance().getConfig()
112 .getString(Config
.SERVER_SSL_KEYSTORE
, "");
113 String keystorePass
= Instance
.getInstance().getConfig()
114 .getString(Config
.SERVER_SSL_KEYSTORE_PASS
);
116 if (secure
&& keystorePath
.isEmpty()) {
117 throw new IOException(
118 "Cannot start a secure web server: no keystore.jks file povided");
121 if (!keystorePath
.isEmpty()) {
122 File keystoreFile
= new File(keystorePath
);
124 KeyStore keystore
= KeyStore
125 .getInstance(KeyStore
.getDefaultType());
126 InputStream keystoreStream
= new FileInputStream(
129 keystore
.load(keystoreStream
,
130 keystorePass
.toCharArray());
131 KeyManagerFactory keyManagerFactory
= KeyManagerFactory
132 .getInstance(KeyManagerFactory
133 .getDefaultAlgorithm());
134 keyManagerFactory
.init(keystore
,
135 keystorePass
.toCharArray());
136 ssf
= NanoHTTPD
.makeSSLSocketFactory(keystore
,
139 keystoreStream
.close();
141 } catch (Exception e
) {
142 throw new IOException(e
.getMessage());
147 server
= new NanoHTTPD(port
) {
149 public Response
serve(final IHTTPSession session
) {
150 super.serve(session
);
152 String query
= session
.getQueryParameterString(); // a=a%20b&dd=2
153 Method method
= session
.getMethod(); // GET, POST..
154 String uri
= session
.getUri(); // /home.html
156 // need them in real time (not just those sent by the UA)
157 Map
<String
, String
> cookies
= new HashMap
<String
, String
>();
158 for (String cookie
: session
.getCookies()) {
159 cookies
.put(cookie
, session
.getCookies().read(cookie
));
162 WLoginResult login
= null;
163 Map
<String
, String
> params
= session
.getParms();
164 String who
= session
.getRemoteHostName()
165 + session
.getRemoteIpAddress();
166 if (params
.get("login") != null) {
167 login
= login(who
, params
.get("password"),
168 params
.get("login"));
170 String cookie
= cookies
.get("cookie");
171 login
= login(who
, cookie
);
174 if (login
.isSuccess()) {
176 session
.getCookies().set(new Cookie("cookie",
177 login
.getCookie(), "30; path=/"));
180 String optionName
= params
.get("optionName");
181 if (optionName
!= null && !optionName
.isEmpty()) {
182 String optionNo
= params
.get("optionNo");
183 String optionValue
= params
.get("optionValue");
184 if (optionNo
!= null || optionValue
== null
185 || optionValue
.isEmpty()) {
186 session
.getCookies().delete(optionName
);
187 cookies
.remove(optionName
);
189 session
.getCookies().set(new Cookie(optionName
,
190 optionValue
, "; path=/"));
191 cookies
.put(optionName
, optionValue
);
197 if (!login
.isSuccess() && WebLibraryUrls
.isSupportedUrl(uri
)) {
198 rep
= loginPage(login
, uri
);
203 if (WebLibraryUrls
.isSupportedUrl(uri
)) {
204 if (WebLibraryUrls
.INDEX_URL
.equals(uri
)) {
205 rep
= root(session
, cookies
, login
);
206 } else if (WebLibraryUrls
.VERSION_URL
.equals(uri
)) {
207 rep
= newFixedLengthResponse(Status
.OK
,
209 Version
.getCurrentVersion().toString());
210 } else if (WebLibraryUrls
.isListUrl(uri
)) {
211 rep
= getList(uri
, login
);
212 } else if (WebLibraryUrls
.isStoryUrl(uri
)) {
213 rep
= getStoryPart(uri
, login
);
214 } else if (WebLibraryUrls
.isViewUrl(uri
)) {
215 rep
= getViewer(cookies
, uri
, login
);
216 } else if (WebLibraryUrls
.LOGOUT_URL
.equals(uri
)) {
217 session
.getCookies().delete("cookie");
218 cookies
.remove("cookie");
219 rep
= loginPage(new WLoginResult(false, false),
222 getTraceHandler().error(
223 "Supported URL was not processed: "
225 rep
= newFixedLengthResponse(
226 Status
.INTERNAL_ERROR
,
227 NanoHTTPD
.MIME_PLAINTEXT
,
228 "An error happened");
231 if (uri
.startsWith("/"))
232 uri
= uri
.substring(1);
233 InputStream in
= IOUtils
.openResource(
234 WebLibraryServerIndex
.class, uri
);
236 String mimeType
= MIME_PLAINTEXT
;
237 if (uri
.endsWith(".css")) {
238 mimeType
= "text/css";
239 } else if (uri
.endsWith(".html")) {
240 mimeType
= "text/html";
241 } else if (uri
.endsWith(".js")) {
242 mimeType
= "text/javascript";
244 rep
= newChunkedResponse(Status
.OK
, mimeType
,
249 getTraceHandler().trace("404: " + uri
);
250 rep
= newFixedLengthResponse(Status
.NOT_FOUND
,
251 NanoHTTPD
.MIME_PLAINTEXT
, "Not Found");
254 } catch (Exception e
) {
255 Instance
.getInstance().getTraceHandler().error(
256 new IOException("Cannot process web request",
258 rep
= newFixedLengthResponse(Status
.INTERNAL_ERROR
,
259 NanoHTTPD
.MIME_PLAINTEXT
, "An error occured");
268 getTraceHandler().trace("Install SSL on the web server...");
269 server
.makeSecure(ssf
, null);
270 getTraceHandler().trace("Done.");
277 server
.start(NanoHTTPD
.SOCKET_READ_TIMEOUT
, false);
278 } catch (IOException e
) {
279 tracer
.error(new IOException("Cannot start the web server", e
));
284 * Start the server (listen on the network for new connections).
286 * Can only be called once.
288 * This call is asynchronous, and will just start a new {@link Thread} on
289 * itself (see {@link WebLibraryServer#run()}).
291 public void start() {
292 new Thread(this).start();
296 * The traces handler for this {@link WebLibraryServer}.
298 * @return the traces handler
300 public TraceHandler
getTraceHandler() {
305 * The traces handler for this {@link WebLibraryServer}.
308 * the new traces handler
310 public void setTraceHandler(TraceHandler tracer
) {
311 if (tracer
== null) {
312 tracer
= new TraceHandler(false, false, false);
315 this.tracer
= tracer
;
318 private WLoginResult
login(String who
, String cookie
) {
319 List
<String
> subkeys
= Instance
.getInstance().getConfig()
320 .getList(Config
.SERVER_ALLOWED_SUBKEYS
);
321 String realKey
= Instance
.getInstance().getConfig()
322 .getString(Config
.SERVER_KEY
);
324 return new WLoginResult(cookie
, who
, realKey
, subkeys
);
328 private WLoginResult
login(String who
, String key
, String subkey
) {
329 String realKey
= Instance
.getInstance().getConfig()
330 .getString(Config
.SERVER_KEY
, "");
332 // I don't like NULLs...
333 key
= key
== null ?
"" : key
;
334 subkey
= subkey
== null ?
"" : subkey
;
336 if (!realKey
.equals(key
)) {
337 return new WLoginResult(true, false);
340 // defaults are true (as previous versions without the feature)
345 rw
= Instance
.getInstance().getConfig().getBoolean(Config
.SERVER_RW
,
348 List
<String
> allowed
= Instance
.getInstance().getConfig().getList(
349 Config
.SERVER_ALLOWED_SUBKEYS
, new ArrayList
<String
>());
351 if (!allowed
.isEmpty()) {
352 if (!allowed
.contains(subkey
)) {
353 return new WLoginResult(true, false);
356 if ((subkey
+ "|").contains("|rw|")) {
359 if ((subkey
+ "|").contains("|wl|")) {
360 wl
= false; // |wl| = bypass whitelist
362 if ((subkey
+ "|").contains("|bl|")) {
363 bl
= false; // |bl| = bypass blacklist
367 return new WLoginResult(who
, key
, subkey
, rw
, wl
, bl
);
370 private Response
loginPage(WLoginResult login
, String uri
) {
371 StringBuilder builder
= new StringBuilder();
373 appendPreHtml(builder
, true);
375 if (login
.isBadLogin()) {
376 builder
.append("<div class='error'>Bad login or password</div>");
377 } else if (login
.isBadCookie()) {
378 builder
.append("<div class='error'>Your session timed out</div>");
381 if (WebLibraryUrls
.LOGOUT_URL
.equals(uri
)) {
382 uri
= WebLibraryUrls
.INDEX_URL
;
386 "<form method='POST' action='" + uri
+ "' class='login'>\n");
388 "<p>You must be logged into the system to see the stories.</p>");
389 builder
.append("\t<input type='text' name='login' />\n");
390 builder
.append("\t<input type='password' name='password' />\n");
391 builder
.append("\t<input type='submit' value='Login' />\n");
392 builder
.append("</form>\n");
394 appendPostHtml(builder
);
396 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
397 NanoHTTPD
.MIME_HTML
, builder
.toString());
400 protected Response
getList(String uri
, WLoginResult login
)
402 if (WebLibraryUrls
.LIST_URL_METADATA
.equals(uri
)) {
403 List
<JSONObject
> jsons
= new ArrayList
<JSONObject
>();
404 for (MetaData meta
: metas(login
)) {
405 jsons
.add(JsonIO
.toJson(meta
));
408 return newInputStreamResponse("application/json",
409 new ByteArrayInputStream(
410 new JSONArray(jsons
).toString().getBytes()));
413 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
414 NanoHTTPD
.MIME_PLAINTEXT
, null);
417 private Response
root(IHTTPSession session
, Map
<String
, String
> cookies
,
418 WLoginResult login
) throws IOException
{
419 BasicLibrary lib
= Instance
.getInstance().getLibrary();
420 MetaResultList result
= new MetaResultList(metas(login
));
421 StringBuilder builder
= new StringBuilder();
423 appendPreHtml(builder
, true);
425 Map
<String
, String
> params
= session
.getParms();
427 String filter
= cookies
.get("filter");
428 if (params
.get("optionNo") != null)
430 if (filter
== null) {
434 String browser
= params
.get("browser") == null ?
""
435 : params
.get("browser");
436 String browser2
= params
.get("browser2") == null ?
""
437 : params
.get("browser2");
438 String browser3
= params
.get("browser3") == null ?
""
439 : params
.get("browser3");
441 String filterSource
= null;
442 String filterAuthor
= null;
443 String filterTag
= null;
445 // TODO: javascript in realtime, using visible=false + hide [submit]
447 builder
.append("<form class='browser'>\n");
448 builder
.append("<div class='breadcrumbs'>\n");
450 builder
.append("\t<select name='browser'>");
451 appendOption(builder
, 2, "", "", browser
);
452 appendOption(builder
, 2, "Sources", "sources", browser
);
453 appendOption(builder
, 2, "Authors", "authors", browser
);
454 appendOption(builder
, 2, "Tags", "tags", browser
);
455 builder
.append("\t</select>\n");
457 if (!browser
.isEmpty()) {
458 builder
.append("\t<select name='browser2'>");
459 if (browser
.equals("sources")) {
460 filterSource
= browser2
.isEmpty() ? filterSource
: browser2
;
461 // TODO: if 1 group -> no group
462 appendOption(builder
, 2, "", "", browser2
);
463 Map
<String
, List
<String
>> sources
= result
.getSourcesGrouped();
464 for (String source
: sources
.keySet()) {
465 appendOption(builder
, 2, source
, source
, browser2
);
467 } else if (browser
.equals("authors")) {
468 filterAuthor
= browser2
.isEmpty() ? filterAuthor
: browser2
;
469 // TODO: if 1 group -> no group
470 appendOption(builder
, 2, "", "", browser2
);
471 Map
<String
, List
<String
>> authors
= result
.getAuthorsGrouped();
472 for (String author
: authors
.keySet()) {
473 appendOption(builder
, 2, author
, author
, browser2
);
475 } else if (browser
.equals("tags")) {
476 filterTag
= browser2
.isEmpty() ? filterTag
: browser2
;
477 appendOption(builder
, 2, "", "", browser2
);
478 for (String tag
: result
.getTags()) {
479 appendOption(builder
, 2, tag
, tag
, browser2
);
482 builder
.append("\t</select>\n");
485 if (!browser2
.isEmpty()) {
486 if (browser
.equals("sources")) {
487 filterSource
= browser3
.isEmpty() ? filterSource
: browser3
;
488 Map
<String
, List
<String
>> sourcesGrouped
= result
489 .getSourcesGrouped();
490 List
<String
> sources
= sourcesGrouped
.get(browser2
);
491 if (sources
!= null && !sources
.isEmpty()) {
492 // TODO: single empty value
493 builder
.append("\t<select name='browser3'>");
494 appendOption(builder
, 2, "", "", browser3
);
495 for (String source
: sources
) {
496 appendOption(builder
, 2, source
, source
, browser3
);
498 builder
.append("\t</select>\n");
500 } else if (browser
.equals("authors")) {
501 filterAuthor
= browser3
.isEmpty() ? filterAuthor
: browser3
;
502 Map
<String
, List
<String
>> authorsGrouped
= result
503 .getAuthorsGrouped();
504 List
<String
> authors
= authorsGrouped
.get(browser2
);
505 if (authors
!= null && !authors
.isEmpty()) {
506 // TODO: single empty value
507 builder
.append("\t<select name='browser3'>");
508 appendOption(builder
, 2, "", "", browser3
);
509 for (String author
: authors
) {
510 appendOption(builder
, 2, author
, author
, browser3
);
512 builder
.append("\t</select>\n");
517 builder
.append("\t<input type='submit' value='Select'/>\n");
518 builder
.append("</div>\n");
520 // TODO: javascript in realtime, using visible=false + hide [submit]
521 builder
.append("<div class='filter'>\n");
522 builder
.append("\t<span class='label'>Filter: </span>\n");
524 "\t<input name='optionName' type='hidden' value='filter' />\n");
525 builder
.append("\t<input name='optionValue' type='text' value='"
526 + filter
+ "' place-holder='...' />\n");
527 builder
.append("\t<input name='optionNo' type='submit' value='x' />");
529 "\t<input name='submit' type='submit' value='Filter' />\n");
530 builder
.append("</div>\n");
531 builder
.append("</form>\n");
533 builder
.append("\t<div class='books'>");
534 for (MetaData meta
: result
.getMetas()) {
535 if (!filter
.isEmpty() && !meta
.getTitle().toLowerCase()
536 .contains(filter
.toLowerCase())) {
541 if (filterSource
!= null
542 && !filterSource
.equals(meta
.getSource())) {
547 if (filterAuthor
!= null
548 && !filterAuthor
.equals(meta
.getAuthor())) {
552 if (filterTag
!= null && !meta
.getTags().contains(filterTag
)) {
556 builder
.append("<div class='book_line'>");
557 builder
.append("<a href='");
559 WebLibraryUrls
.getViewUrl(meta
.getLuid(), null, null));
561 builder
.append(" class='link'>");
563 if (lib
.isCached(meta
.getLuid())) {
566 "<span class='cache_icon cached'>◉</span>");
570 "<span class='cache_icon uncached'>○</span>");
572 builder
.append("<span class='luid'>");
573 builder
.append(meta
.getLuid());
574 builder
.append("</span>");
575 builder
.append("<span class='title'>");
576 builder
.append(meta
.getTitle());
577 builder
.append("</span>");
578 builder
.append("<span class='author'>");
579 if (meta
.getAuthor() != null && !meta
.getAuthor().isEmpty()) {
580 builder
.append("(").append(meta
.getAuthor()).append(")");
582 builder
.append("</span>");
583 builder
.append("</a></div>\n");
585 builder
.append("</div>");
587 appendPostHtml(builder
);
588 return NanoHTTPD
.newFixedLengthResponse(builder
.toString());
591 // /story/luid/chapter/para <-- text/image
592 // /story/luid/cover <-- image
593 // /story/luid/metadata <-- json
594 // /story/luid/json <-- json, whole chapter (no images)
595 private Response
getStoryPart(String uri
, WLoginResult login
) {
596 String
[] cover
= uri
.split("/");
599 if (cover
.length
< off
+ 2) {
600 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
601 NanoHTTPD
.MIME_PLAINTEXT
, null);
604 String luid
= cover
[off
+ 0];
605 String chapterStr
= cover
[off
+ 1];
606 String imageStr
= cover
.length
< off
+ 3 ?
null : cover
[off
+ 2];
608 // 1-based (0 = desc)
610 if (chapterStr
!= null && !"cover".equals(chapterStr
)
611 && !"metadata".equals(chapterStr
)
612 && !"json".equals(chapterStr
)) {
614 chapter
= Integer
.parseInt(chapterStr
);
616 throw new NumberFormatException();
618 } catch (NumberFormatException e
) {
619 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
620 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
626 if (imageStr
!= null) {
628 paragraph
= Integer
.parseInt(imageStr
);
630 throw new NumberFormatException();
632 } catch (NumberFormatException e
) {
633 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
634 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
638 String mimeType
= NanoHTTPD
.MIME_PLAINTEXT
;
639 InputStream in
= null;
641 if ("cover".equals(chapterStr
)) {
642 Image img
= getCover(luid
, login
);
644 in
= img
.newInputStream();
646 // TODO: get correct image type
647 mimeType
= "image/png";
648 } else if ("metadata".equals(chapterStr
)) {
649 MetaData meta
= meta(luid
, login
);
650 JSONObject json
= JsonIO
.toJson(meta
);
651 mimeType
= "application/json";
652 in
= new ByteArrayInputStream(json
.toString().getBytes());
653 } else if ("json".equals(chapterStr
)) {
654 Story story
= story(luid
, login
);
655 JSONObject json
= JsonIO
.toJson(story
);
656 mimeType
= "application/json";
657 in
= new ByteArrayInputStream(json
.toString().getBytes());
659 Story story
= story(luid
, login
);
662 StringBuilder builder
= new StringBuilder();
663 for (Paragraph p
: story
.getMeta().getResume()) {
664 if (builder
.length() == 0) {
665 builder
.append("\n");
667 builder
.append(p
.getContent());
670 in
= new ByteArrayInputStream(
671 builder
.toString().getBytes("utf-8"));
673 Paragraph para
= story
.getChapters().get(chapter
- 1)
674 .getParagraphs().get(paragraph
- 1);
675 Image img
= para
.getContentImage();
676 if (para
.getType() == ParagraphType
.IMAGE
) {
677 // TODO: get correct image type
678 mimeType
= "image/png";
679 in
= img
.newInputStream();
681 in
= new ByteArrayInputStream(
682 para
.getContent().getBytes("utf-8"));
687 } catch (IndexOutOfBoundsException e
) {
688 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
689 NanoHTTPD
.MIME_PLAINTEXT
,
690 "Chapter or paragraph does not exist");
691 } catch (IOException e
) {
692 Instance
.getInstance().getTraceHandler()
693 .error(new IOException("Cannot get image: " + uri
, e
));
694 return NanoHTTPD
.newFixedLengthResponse(Status
.INTERNAL_ERROR
,
695 NanoHTTPD
.MIME_PLAINTEXT
, "Error when processing request");
698 return newInputStreamResponse(mimeType
, in
);
701 private Response
getViewer(Map
<String
, String
> cookies
, String uri
,
702 WLoginResult login
) {
703 String
[] cover
= uri
.split("/");
706 if (cover
.length
< off
+ 2) {
707 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
708 NanoHTTPD
.MIME_PLAINTEXT
, null);
711 String type
= cover
[off
+ 0];
712 String luid
= cover
[off
+ 1];
713 String chapterStr
= cover
.length
< off
+ 3 ?
null : cover
[off
+ 2];
714 String paragraphStr
= cover
.length
< off
+ 4 ?
null : cover
[off
+ 3];
716 // 1-based (0 = desc)
718 if (chapterStr
!= null) {
720 chapter
= Integer
.parseInt(chapterStr
);
722 throw new NumberFormatException();
724 } catch (NumberFormatException e
) {
725 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
726 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
732 if (paragraphStr
!= null) {
734 paragraph
= Integer
.parseInt(paragraphStr
);
735 if (paragraph
<= 0) {
736 throw new NumberFormatException();
738 } catch (NumberFormatException e
) {
739 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
740 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
745 Story story
= story(luid
, login
);
747 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
748 NanoHTTPD
.MIME_PLAINTEXT
, "Story not found");
751 StringBuilder builder
= new StringBuilder();
752 appendPreHtml(builder
, false);
754 // For images documents, always go to the images if not chap 0 desc
755 if (story
.getMeta().isImageDocument()) {
756 if (chapter
> 0 && paragraph
<= 0)
762 chap
= story
.getMeta().getResume();
765 chap
= story
.getChapters().get(chapter
- 1);
766 } catch (IndexOutOfBoundsException e
) {
767 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
768 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter not found");
772 String first
, previous
, next
, last
;
774 StringBuilder content
= new StringBuilder();
776 String disabledLeft
= "";
777 String disabledRight
= "";
778 String disabledZoomReal
= "";
779 String disabledZoomWidth
= "";
780 String disabledZoomHeight
= "";
782 if (paragraph
<= 0) {
783 first
= WebLibraryUrls
.getViewUrl(luid
, 0, null);
784 previous
= WebLibraryUrls
.getViewUrl(luid
,
785 (Math
.max(chapter
- 1, 0)), null);
786 next
= WebLibraryUrls
.getViewUrl(luid
,
787 (Math
.min(chapter
+ 1, story
.getChapters().size())),
789 last
= WebLibraryUrls
.getViewUrl(luid
,
790 story
.getChapters().size(), null);
792 StringBuilder desc
= new StringBuilder();
795 desc
.append("<h1 class='title'>");
796 desc
.append(story
.getMeta().getTitle());
797 desc
.append("</h1>\n");
798 desc
.append("<div class='desc'>\n");
799 desc
.append("\t<a href='" + next
+ "' class='cover'>\n");
800 desc
.append("\t\t<img src='/story/" + luid
+ "/cover'/>\n");
801 desc
.append("\t</a>\n");
802 desc
.append("\t<table class='details'>\n");
803 Map
<String
, String
> details
= BasicLibrary
804 .getMetaDesc(story
.getMeta());
805 for (String key
: details
.keySet()) {
806 appendTableRow(desc
, 2, key
, details
.get(key
));
808 desc
.append("\t</table>\n");
809 desc
.append("</div>\n");
810 desc
.append("<h1 class='title'>Description</h1>\n");
813 content
.append("<div class='viewer text'>\n");
814 content
.append(desc
);
815 String description
= new TextOutput(false).convert(chap
,
817 content
.append(chap
.getParagraphs().size() <= 0
818 ?
"No content provided."
820 content
.append("</div>\n");
823 disabledLeft
= " disabled='disbaled'";
824 if (chapter
>= story
.getChapters().size())
825 disabledRight
= " disabled='disbaled'";
827 first
= WebLibraryUrls
.getViewUrl(luid
, chapter
, 1);
828 previous
= WebLibraryUrls
.getViewUrl(luid
, chapter
,
829 (Math
.max(paragraph
- 1, 1)));
830 next
= WebLibraryUrls
.getViewUrl(luid
, chapter
,
831 (Math
.min(paragraph
+ 1, chap
.getParagraphs().size())));
832 last
= WebLibraryUrls
.getViewUrl(luid
, chapter
,
833 chap
.getParagraphs().size());
836 disabledLeft
= " disabled='disbaled'";
837 if (paragraph
>= chap
.getParagraphs().size())
838 disabledRight
= " disabled='disbaled'";
840 // First -> previous *chapter*
843 first
= WebLibraryUrls
.getViewUrl(luid
,
844 (Math
.max(chapter
- 1, 0)), null);
845 if (paragraph
<= 1) {
849 Paragraph para
= null;
851 para
= chap
.getParagraphs().get(paragraph
- 1);
852 } catch (IndexOutOfBoundsException e
) {
853 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
854 NanoHTTPD
.MIME_PLAINTEXT
,
855 "Paragraph " + paragraph
+ " not found");
858 if (para
.getType() == ParagraphType
.IMAGE
) {
859 String zoomStyle
= "max-width: 100%;";
860 disabledZoomWidth
= " disabled='disabled'";
861 String zoomOption
= cookies
.get("zoom");
862 if (zoomOption
!= null && !zoomOption
.isEmpty()) {
863 if (zoomOption
.equals("real")) {
865 disabledZoomWidth
= "";
866 disabledZoomReal
= " disabled='disabled'";
867 } else if (zoomOption
.equals("width")) {
868 zoomStyle
= "max-width: 100%;";
869 } else if (zoomOption
.equals("height")) {
870 // see height of navbar + optionbar
871 zoomStyle
= "max-height: calc(100% - 128px);";
872 disabledZoomWidth
= "";
873 disabledZoomHeight
= " disabled='disabled'";
877 String javascript
= "document.getElementById(\"previous\").click(); return false;";
878 content
.append(String
.format("" //
879 + "<a class='viewer link' oncontextmenu='%s' href='%s'>"
880 + "<img class='viewer img' style='%s' src='%s'/>"
885 WebLibraryUrls
.getStoryUrl(luid
, chapter
,
888 content
.append(String
.format("" //
889 + "<div class='viewer text'>%s</div>", //
894 builder
.append(String
.format("" //
895 + "<div class='bar navbar'>\n" //
896 + "\t<a%s class='button first' href='%s'><<</a>\n"//
897 + "\t<a%s id='previous' class='button previous' href='%s'><</a>\n" //
898 + "\t<div class='gotobox itemsbox'>\n" //
899 + "\t\t<div class='button goto'>%d</div>\n" //
900 + "\t\t<div class='items goto'>\n", //
901 disabledLeft
, first
, //
902 disabledLeft
, previous
, //
903 paragraph
> 0 ? paragraph
: chapter
//
906 // List of chap/para links
908 appendItemA(builder
, 3, WebLibraryUrls
.getViewUrl(luid
, 0, null),
909 "Description", paragraph
== 0 && chapter
== 0);
911 for (int i
= 1; i
<= chap
.getParagraphs().size(); i
++) {
912 appendItemA(builder
, 3,
913 WebLibraryUrls
.getViewUrl(luid
, chapter
, i
),
914 "Image " + i
, paragraph
== i
);
918 for (Chapter c
: story
.getChapters()) {
919 String chapName
= "Chapter " + c
.getNumber();
920 if (c
.getName() != null && !c
.getName().isEmpty()) {
921 chapName
+= ": " + c
.getName();
924 appendItemA(builder
, 3,
925 WebLibraryUrls
.getViewUrl(luid
, i
, null), chapName
,
932 builder
.append(String
.format("" //
935 + "\t<a%s class='button next' href='%s'>></a>\n" //
936 + "\t<a%s class='button last' href='%s'>>></a>\n"//
938 disabledRight
, next
, //
939 disabledRight
, last
//
942 builder
.append(content
);
944 builder
.append("<div class='bar optionbar ");
946 builder
.append("s4");
948 builder
.append("s1");
950 builder
.append("'>\n");
951 builder
.append(" <a class='button back' href='/'>BACK</a>\n");
954 builder
.append(String
.format("" //
955 + "\t<a%s class='button zoomreal' href='%s'>REAL</a>\n"//
956 + "\t<a%s class='button zoomwidth' href='%s'>WIDTH</a>\n"//
957 + "\t<a%s class='button zoomheight' href='%s'>HEIGHT</a>\n"//
960 uri
+ "?optionName=zoom&optionValue=real", //
962 uri
+ "?optionName=zoom&optionValue=width", //
964 uri
+ "?optionName=zoom&optionValue=height" //
968 appendPostHtml(builder
);
969 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
970 NanoHTTPD
.MIME_HTML
, builder
.toString());
971 } catch (IOException e
) {
972 Instance
.getInstance().getTraceHandler()
973 .error(new IOException("Cannot get image: " + uri
, e
));
974 return NanoHTTPD
.newFixedLengthResponse(Status
.INTERNAL_ERROR
,
975 NanoHTTPD
.MIME_PLAINTEXT
, "Error when processing request");
979 private Response
newInputStreamResponse(String mimeType
, InputStream in
) {
981 return NanoHTTPD
.newFixedLengthResponse(Status
.NO_CONTENT
, "",
984 return NanoHTTPD
.newChunkedResponse(Status
.OK
, mimeType
, in
);
987 private String
getContentOf(String file
) {
988 InputStream in
= IOUtils
.openResource(WebLibraryServerIndex
.class,
992 return IOUtils
.readSmallStream(in
);
993 } catch (IOException e
) {
994 Instance
.getInstance().getTraceHandler().error(
995 new IOException("Cannot get file: index.pre.html", e
));
1002 private boolean isAllowed(MetaData meta
, WLoginResult login
) {
1003 if (login
.isWl() && !whitelist
.isEmpty()
1004 && !whitelist
.contains(meta
.getSource())) {
1007 if (login
.isBl() && blacklist
.contains(meta
.getSource())) {
1014 private List
<MetaData
> metas(WLoginResult login
) throws IOException
{
1015 BasicLibrary lib
= Instance
.getInstance().getLibrary();
1016 List
<MetaData
> metas
= new ArrayList
<MetaData
>();
1017 for (MetaData meta
: lib
.getList().getMetas()) {
1018 if (isAllowed(meta
, login
)) {
1026 private MetaData
meta(String luid
, WLoginResult login
) throws IOException
{
1027 BasicLibrary lib
= Instance
.getInstance().getLibrary();
1028 MetaData meta
= lib
.getInfo(luid
);
1029 if (!isAllowed(meta
, login
))
1035 private Image
getCover(String luid
, WLoginResult login
) throws IOException
{
1036 MetaData meta
= meta(luid
, login
);
1038 BasicLibrary lib
= Instance
.getInstance().getLibrary();
1039 return lib
.getCover(meta
.getLuid());
1045 // NULL if not whitelist OK or if not found
1046 private Story
story(String luid
, WLoginResult login
) throws IOException
{
1047 synchronized (storyCache
) {
1048 if (storyCache
.containsKey(luid
)) {
1049 Story story
= storyCache
.get(luid
);
1050 if (!isAllowed(story
.getMeta(), login
))
1058 MetaData meta
= meta(luid
, login
);
1060 BasicLibrary lib
= Instance
.getInstance().getLibrary();
1061 story
= lib
.getStory(luid
, null);
1062 long size
= sizeOf(story
);
1064 synchronized (storyCache
) {
1065 // Could have been added by another request
1066 if (!storyCache
.containsKey(luid
)) {
1067 while (!storyCacheOrder
.isEmpty()
1068 && storyCacheSize
+ size
> maxStoryCacheSize
) {
1069 String oldestLuid
= storyCacheOrder
.removeFirst();
1070 Story oldestStory
= storyCache
.remove(oldestLuid
);
1071 maxStoryCacheSize
-= sizeOf(oldestStory
);
1074 storyCacheOrder
.add(luid
);
1075 storyCache
.put(luid
, story
);
1083 private long sizeOf(Story story
) {
1085 for (Chapter chap
: story
) {
1086 for (Paragraph para
: chap
) {
1087 if (para
.getType() == ParagraphType
.IMAGE
) {
1088 size
+= para
.getContentImage().getSize();
1090 size
+= para
.getContent().length();
1098 private void appendPreHtml(StringBuilder builder
, boolean banner
) {
1099 String favicon
= "favicon.ico";
1100 String icon
= Instance
.getInstance().getUiConfig()
1101 .getString(UiConfig
.PROGRAM_ICON
);
1103 favicon
= "icon_" + icon
.replace("-", "_") + ".png";
1107 getContentOf("index.pre.html").replace("favicon.ico", favicon
));
1110 builder
.append("<div class='banner'>\n");
1111 builder
.append("\t<img class='ico' src='/") //
1114 builder
.append("\t<h1>Fanfix</h1>\n");
1115 builder
.append("\t<h2>") //
1116 .append(Version
.getCurrentVersion()) //
1118 builder
.append("</div>\n");
1122 private void appendPostHtml(StringBuilder builder
) {
1123 builder
.append(getContentOf("index.post.html"));
1126 private void appendOption(StringBuilder builder
, int depth
, String name
,
1127 String value
, String selected
) {
1128 for (int i
= 0; i
< depth
; i
++) {
1129 builder
.append("\t");
1131 builder
.append("<option value='").append(value
).append("'");
1132 if (value
.equals(selected
)) {
1133 builder
.append(" selected='selected'");
1135 builder
.append(">").append(name
).append("</option>\n");
1138 private void appendTableRow(StringBuilder builder
, int depth
,
1140 for (int i
= 0; i
< depth
; i
++) {
1141 builder
.append("\t");
1145 builder
.append("<tr>");
1146 for (String td
: tds
) {
1147 builder
.append("<td class='col");
1148 builder
.append(col
++);
1149 builder
.append("'>");
1151 builder
.append("</td>");
1153 builder
.append("</tr>\n");
1156 private void appendItemA(StringBuilder builder
, int depth
, String link
,
1157 String name
, boolean selected
) {
1158 for (int i
= 0; i
< depth
; i
++) {
1159 builder
.append("\t");
1162 builder
.append("<a href='");
1163 builder
.append(link
);
1164 builder
.append("' class='item goto");
1166 builder
.append(" selected");
1168 builder
.append("'>");
1169 builder
.append(name
);
1170 builder
.append("</a>\n");
1173 public static void main(String
[] args
) throws IOException
{
1175 WebLibraryServer web
= new WebLibraryServer(false);