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
.CookieUtils
;
33 import be
.nikiroo
.utils
.IOUtils
;
34 import be
.nikiroo
.utils
.Image
;
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 LoginResult
{
51 private boolean success
;
54 private String wookie
;
56 private boolean badLogin
;
57 private boolean badToken
;
59 public LoginResult(String who
, String key
, String subkey
,
60 boolean success
, boolean rw
, boolean wl
) {
61 this.success
= success
;
64 this.wookie
= CookieUtils
.generateCookie(who
+ key
, 0);
72 this.token
= wookie
+ "~"
73 + CookieUtils
.generateCookie(wookie
+ subkey
+ opts
, 0)
75 this.badLogin
= !success
;
78 public LoginResult(String token
, String who
, String key
,
79 List
<String
> subkeys
) {
82 String hashes
[] = token
.split("~");
83 if (hashes
.length
>= 2) {
84 String wookie
= hashes
[0];
85 String rehashed
= hashes
[1];
86 String opts
= hashes
.length
> 2 ? hashes
[2] : "";
88 if (CookieUtils
.validateCookie(who
+ key
, wookie
)) {
89 if (subkeys
== null) {
90 subkeys
= new ArrayList
<String
>();
92 subkeys
= new ArrayList
<String
>(subkeys
);
95 for (String subkey
: subkeys
) {
96 if (CookieUtils
.validateCookie(
97 wookie
+ subkey
+ opts
, rehashed
)) {
102 this.rw
= opts
.contains("|rw");
103 this.wl
= !opts
.contains("|wl");
109 this.badToken
= !success
;
112 // No token -> no bad token
115 public boolean isSuccess() {
119 public boolean isRw() {
123 public boolean isWl() {
127 public String
getToken() {
131 public boolean isBadLogin() {
135 public boolean isBadToken() {
140 private NanoHTTPD server
;
141 private Map
<String
, Story
> storyCache
= new HashMap
<String
, Story
>();
142 private LinkedList
<String
> storyCacheOrder
= new LinkedList
<String
>();
143 private long storyCacheSize
= 0;
144 private long maxStoryCacheSize
;
145 private TraceHandler tracer
= new TraceHandler();
147 public WebLibraryServer(boolean secure
) throws IOException
{
148 Integer port
= Instance
.getInstance().getConfig()
149 .getInteger(Config
.SERVER_PORT
);
151 throw new IOException(
152 "Cannot start web server: port not specified");
155 int cacheMb
= Instance
.getInstance().getConfig()
156 .getInteger(Config
.SERVER_MAX_CACHE_MB
, 100);
157 maxStoryCacheSize
= cacheMb
* 1024 * 1024;
159 setTraceHandler(Instance
.getInstance().getTraceHandler());
161 SSLServerSocketFactory ssf
= null;
163 String keystorePath
= Instance
.getInstance().getConfig()
164 .getString(Config
.SERVER_SSL_KEYSTORE
, "");
165 String keystorePass
= Instance
.getInstance().getConfig()
166 .getString(Config
.SERVER_SSL_KEYSTORE_PASS
);
168 if (secure
&& keystorePath
.isEmpty()) {
169 throw new IOException(
170 "Cannot start a secure web server: no keystore.jks file povided");
173 if (!keystorePath
.isEmpty()) {
174 File keystoreFile
= new File(keystorePath
);
176 KeyStore keystore
= KeyStore
177 .getInstance(KeyStore
.getDefaultType());
178 InputStream keystoreStream
= new FileInputStream(
181 keystore
.load(keystoreStream
,
182 keystorePass
.toCharArray());
183 KeyManagerFactory keyManagerFactory
= KeyManagerFactory
184 .getInstance(KeyManagerFactory
185 .getDefaultAlgorithm());
186 keyManagerFactory
.init(keystore
,
187 keystorePass
.toCharArray());
188 ssf
= NanoHTTPD
.makeSSLSocketFactory(keystore
,
191 keystoreStream
.close();
193 } catch (Exception e
) {
194 throw new IOException(e
.getMessage());
199 server
= new NanoHTTPD(port
) {
201 public Response
serve(final IHTTPSession session
) {
202 super.serve(session
);
204 String query
= session
.getQueryParameterString(); // a=a%20b&dd=2
205 Method method
= session
.getMethod(); // GET, POST..
206 String uri
= session
.getUri(); // /home.html
208 // need them in real time (not just those sent by the UA)
209 Map
<String
, String
> cookies
= new HashMap
<String
, String
>();
210 for (String cookie
: session
.getCookies()) {
211 cookies
.put(cookie
, session
.getCookies().read(cookie
));
214 List
<String
> whitelist
= Instance
.getInstance().getConfig()
215 .getList(Config
.SERVER_WHITELIST
);
216 if (whitelist
== null) {
217 whitelist
= new ArrayList
<String
>();
220 LoginResult login
= null;
221 Map
<String
, String
> params
= session
.getParms();
222 String who
= session
.getRemoteHostName()
223 + session
.getRemoteIpAddress();
224 if (params
.get("login") != null) {
225 login
= login(who
, params
.get("password"),
226 params
.get("login"), whitelist
);
228 String token
= cookies
.get("token");
229 login
= login(who
, token
, Instance
.getInstance().getConfig()
230 .getList(Config
.SERVER_ALLOWED_SUBKEYS
));
233 if (login
.isSuccess()) {
239 session
.getCookies().set(new Cookie("token",
240 login
.getToken(), "30; path=/"));
243 String optionName
= params
.get("optionName");
244 if (optionName
!= null && !optionName
.isEmpty()) {
245 String optionNo
= params
.get("optionNo");
246 String optionValue
= params
.get("optionValue");
247 if (optionNo
!= null || optionValue
== null
248 || optionValue
.isEmpty()) {
249 session
.getCookies().delete(optionName
);
250 cookies
.remove(optionName
);
252 session
.getCookies().set(new Cookie(optionName
,
253 optionValue
, "; path=/"));
254 cookies
.put(optionName
, optionValue
);
260 if (!login
.isSuccess() && (uri
.equals("/") //
261 || uri
.startsWith(STORY_URL_BASE
) //
262 || uri
.startsWith(VIEWER_URL_BASE
) //
263 || uri
.startsWith(LIST_URL
))) {
264 rep
= loginPage(login
, uri
);
269 if (uri
.equals("/")) {
270 rep
= root(session
, cookies
, whitelist
);
271 } else if (uri
.startsWith(LIST_URL
)) {
272 rep
= getList(uri
, whitelist
);
273 } else if (uri
.startsWith(STORY_URL_BASE
)) {
274 rep
= getStoryPart(uri
, whitelist
);
275 } else if (uri
.startsWith(VIEWER_URL_BASE
)) {
276 rep
= getViewer(cookies
, uri
, whitelist
);
277 } else if (uri
.equals("/logout")) {
278 session
.getCookies().delete("token");
279 cookies
.remove("token");
280 rep
= loginPage(login
, uri
);
282 if (uri
.startsWith("/"))
283 uri
= uri
.substring(1);
284 InputStream in
= IOUtils
.openResource(
285 WebLibraryServerIndex
.class, uri
);
287 String mimeType
= MIME_PLAINTEXT
;
288 if (uri
.endsWith(".css")) {
289 mimeType
= "text/css";
290 } else if (uri
.endsWith(".html")) {
291 mimeType
= "text/html";
292 } else if (uri
.endsWith(".js")) {
293 mimeType
= "text/javascript";
295 rep
= newChunkedResponse(Status
.OK
, mimeType
,
298 getTraceHandler().trace("404: " + uri
);
303 rep
= newFixedLengthResponse(Status
.NOT_FOUND
,
304 NanoHTTPD
.MIME_PLAINTEXT
, "Not Found");
306 } catch (Exception e
) {
307 Instance
.getInstance().getTraceHandler().error(
308 new IOException("Cannot process web request",
310 rep
= newFixedLengthResponse(Status
.INTERNAL_ERROR
,
311 NanoHTTPD
.MIME_PLAINTEXT
, "An error occured");
317 // Get status: for story, use "luid" + active map of current
319 // map must use a addRef/removeRef and delete at 0
321 // http://localhost:2000/?token=ok
324 // MetaData meta = new MetaData();
325 // meta.setTitle("Title");
326 // meta.setLuid("000");
328 // JSONObject json = new JSONObject();
329 // json.put("", MetaData.class.getName());
330 // json.put("title", meta.getTitle());
331 // json.put("luid", meta.getLuid());
333 // return newFixedLengthResponse(json.toString());
338 getTraceHandler().trace("Install SSL on the web server...");
339 server
.makeSecure(ssf
, null);
340 getTraceHandler().trace("Done.");
347 server
.start(NanoHTTPD
.SOCKET_READ_TIMEOUT
, false);
348 } catch (IOException e
) {
349 tracer
.error(new IOException("Cannot start the web server", e
));
354 * Start the server (listen on the network for new connections).
356 * Can only be called once.
358 * This call is asynchronous, and will just start a new {@link Thread} on
359 * itself (see {@link WebLibraryServer#run()}).
361 public void start() {
362 new Thread(this).start();
366 * The traces handler for this {@link WebLibraryServer}.
368 * @return the traces handler
370 public TraceHandler
getTraceHandler() {
375 * The traces handler for this {@link WebLibraryServer}.
378 * the new traces handler
380 public void setTraceHandler(TraceHandler tracer
) {
381 if (tracer
== null) {
382 tracer
= new TraceHandler(false, false, false);
385 this.tracer
= tracer
;
388 private LoginResult
login(String who
, String token
, List
<String
> subkeys
) {
389 String realKey
= Instance
.getInstance().getConfig()
390 .getString(Config
.SERVER_KEY
);
391 realKey
= realKey
== null ?
"" : realKey
;
392 return new LoginResult(token
, who
, realKey
, subkeys
);
396 private LoginResult
login(String who
, String key
, String subkey
,
397 List
<String
> whitelist
) {
398 String realKey
= Instance
.getInstance().getConfig()
399 .getString(Config
.SERVER_KEY
);
401 // I don't like NULLs...
402 realKey
= realKey
== null ?
"" : realKey
;
403 key
= key
== null ?
"" : key
;
404 subkey
= subkey
== null ?
"" : subkey
;
406 if (!realKey
.equals(key
)) {
407 return new LoginResult(null, null, null, false, false, false);
410 // defaults are positive (as previous versions without the feature)
414 if (whitelist
.isEmpty()) {
418 rw
= Instance
.getInstance().getConfig().getBoolean(Config
.SERVER_RW
,
420 if (!subkey
.isEmpty()) {
421 List
<String
> allowed
= Instance
.getInstance().getConfig()
422 .getList(Config
.SERVER_ALLOWED_SUBKEYS
);
423 if (allowed
!= null && allowed
.contains(subkey
)) {
424 if ((subkey
+ "|").contains("|rw|")) {
427 if ((subkey
+ "|").contains("|wl|")) {
428 wl
= false; // |wl| = bypass whitelist
431 return new LoginResult(null, null, null, false, false, false);
435 return new LoginResult(who
, key
, subkey
, true, rw
, wl
);
438 private Response
loginPage(LoginResult login
, String uri
) {
439 StringBuilder builder
= new StringBuilder();
441 appendPreHtml(builder
, true);
443 if (login
.isBadLogin()) {
444 builder
.append("<div class='error'>Bad login or password</div>");
445 } else if (login
.isBadToken()) {
446 builder
.append("<div class='error'>Your session timed out</div>");
449 if (uri
.equals("/logout")) {
454 "<form method='POST' action='" + uri
+ "' class='login'>\n");
456 "<p>You must be logged into the system to see the stories.</p>");
457 builder
.append("\t<input type='text' name='login' />\n");
458 builder
.append("\t<input type='password' name='password' />\n");
459 builder
.append("\t<input type='submit' value='Login' />\n");
460 builder
.append("</form>\n");
462 appendPostHtml(builder
);
464 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
465 NanoHTTPD
.MIME_HTML
, builder
.toString());
468 protected Response
getList(String uri
, List
<String
> whitelist
)
470 if (uri
.equals("/list/luids")) {
471 BasicLibrary lib
= Instance
.getInstance().getLibrary();
472 List
<MetaData
> metas
= lib
.getList().filter(whitelist
, null, null);
473 List
<JSONObject
> jsons
= new ArrayList
<JSONObject
>();
474 for (MetaData meta
: metas
) {
475 jsons
.add(JsonIO
.toJson(meta
));
478 return newInputStreamResponse("application/json",
479 new ByteArrayInputStream(
480 new JSONArray(jsons
).toString().getBytes()));
483 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
484 NanoHTTPD
.MIME_PLAINTEXT
, null);
487 private Response
root(IHTTPSession session
, Map
<String
, String
> cookies
,
488 List
<String
> whitelist
) throws IOException
{
489 BasicLibrary lib
= Instance
.getInstance().getLibrary();
490 MetaResultList result
= lib
.getList();
491 result
= new MetaResultList(result
.filter(whitelist
, null, null));
492 StringBuilder builder
= new StringBuilder();
494 appendPreHtml(builder
, true);
496 Map
<String
, String
> params
= session
.getParms();
498 String filter
= cookies
.get("filter");
499 if (params
.get("optionNo") != null)
501 if (filter
== null) {
505 String browser
= params
.get("browser") == null ?
""
506 : params
.get("browser");
507 String browser2
= params
.get("browser2") == null ?
""
508 : params
.get("browser2");
509 String browser3
= params
.get("browser3") == null ?
""
510 : params
.get("browser3");
512 String filterSource
= null;
513 String filterAuthor
= null;
514 String filterTag
= null;
516 // TODO: javascript in realtime, using visible=false + hide [submit]
518 builder
.append("<form class='browser'>\n");
519 builder
.append("<div class='breadcrumbs'>\n");
521 builder
.append("\t<select name='browser'>");
522 appendOption(builder
, 2, "", "", browser
);
523 appendOption(builder
, 2, "Sources", "sources", browser
);
524 appendOption(builder
, 2, "Authors", "authors", browser
);
525 appendOption(builder
, 2, "Tags", "tags", browser
);
526 builder
.append("\t</select>\n");
528 if (!browser
.isEmpty()) {
529 builder
.append("\t<select name='browser2'>");
530 if (browser
.equals("sources")) {
531 filterSource
= browser2
.isEmpty() ? filterSource
: browser2
;
532 // TODO: if 1 group -> no group
533 appendOption(builder
, 2, "", "", browser2
);
534 Map
<String
, List
<String
>> sources
= result
.getSourcesGrouped();
535 for (String source
: sources
.keySet()) {
536 appendOption(builder
, 2, source
, source
, browser2
);
538 } else if (browser
.equals("authors")) {
539 filterAuthor
= browser2
.isEmpty() ? filterAuthor
: browser2
;
540 // TODO: if 1 group -> no group
541 appendOption(builder
, 2, "", "", browser2
);
542 Map
<String
, List
<String
>> authors
= result
.getAuthorsGrouped();
543 for (String author
: authors
.keySet()) {
544 appendOption(builder
, 2, author
, author
, browser2
);
546 } else if (browser
.equals("tags")) {
547 filterTag
= browser2
.isEmpty() ? filterTag
: browser2
;
548 appendOption(builder
, 2, "", "", browser2
);
549 for (String tag
: result
.getTags()) {
550 appendOption(builder
, 2, tag
, tag
, browser2
);
553 builder
.append("\t</select>\n");
556 if (!browser2
.isEmpty()) {
557 if (browser
.equals("sources")) {
558 filterSource
= browser3
.isEmpty() ? filterSource
: browser3
;
559 Map
<String
, List
<String
>> sourcesGrouped
= result
560 .getSourcesGrouped();
561 List
<String
> sources
= sourcesGrouped
.get(browser2
);
562 if (sources
!= null && !sources
.isEmpty()) {
563 // TODO: single empty value
564 builder
.append("\t<select name='browser3'>");
565 appendOption(builder
, 2, "", "", browser3
);
566 for (String source
: sources
) {
567 appendOption(builder
, 2, source
, source
, browser3
);
569 builder
.append("\t</select>\n");
571 } else if (browser
.equals("authors")) {
572 filterAuthor
= browser3
.isEmpty() ? filterAuthor
: browser3
;
573 Map
<String
, List
<String
>> authorsGrouped
= result
574 .getAuthorsGrouped();
575 List
<String
> authors
= authorsGrouped
.get(browser2
);
576 if (authors
!= null && !authors
.isEmpty()) {
577 // TODO: single empty value
578 builder
.append("\t<select name='browser3'>");
579 appendOption(builder
, 2, "", "", browser3
);
580 for (String author
: authors
) {
581 appendOption(builder
, 2, author
, author
, browser3
);
583 builder
.append("\t</select>\n");
588 builder
.append("\t<input type='submit' value='Select'/>\n");
589 builder
.append("</div>\n");
591 // TODO: javascript in realtime, using visible=false + hide [submit]
592 builder
.append("<div class='filter'>\n");
593 builder
.append("\t<span class='label'>Filter: </span>\n");
595 "\t<input name='optionName' type='hidden' value='filter' />\n");
596 builder
.append("\t<input name='optionValue' type='text' value='"
597 + filter
+ "' place-holder='...' />\n");
598 builder
.append("\t<input name='optionNo' type='submit' value='x' />");
600 "\t<input name='submit' type='submit' value='Filter' />\n");
601 builder
.append("</div>\n");
602 builder
.append("</form>\n");
604 builder
.append("\t<div class='books'>");
605 for (MetaData meta
: result
.getMetas()) {
606 if (!filter
.isEmpty() && !meta
.getTitle().toLowerCase()
607 .contains(filter
.toLowerCase())) {
612 if (filterSource
!= null
613 && !filterSource
.equals(meta
.getSource())) {
618 if (filterAuthor
!= null
619 && !filterAuthor
.equals(meta
.getAuthor())) {
623 if (filterTag
!= null && !meta
.getTags().contains(filterTag
)) {
627 builder
.append("<div class='book_line'>");
628 builder
.append("<a href='");
629 builder
.append(getViewUrl(meta
.getLuid(), 0, null));
631 builder
.append(" class='link'>");
633 if (lib
.isCached(meta
.getLuid())) {
636 "<span class='cache_icon cached'>◉</span>");
640 "<span class='cache_icon uncached'>○</span>");
642 builder
.append("<span class='luid'>");
643 builder
.append(meta
.getLuid());
644 builder
.append("</span>");
645 builder
.append("<span class='title'>");
646 builder
.append(meta
.getTitle());
647 builder
.append("</span>");
648 builder
.append("<span class='author'>");
649 if (meta
.getAuthor() != null && !meta
.getAuthor().isEmpty()) {
650 builder
.append("(").append(meta
.getAuthor()).append(")");
652 builder
.append("</span>");
653 builder
.append("</a></div>\n");
655 builder
.append("</div>");
657 appendPostHtml(builder
);
658 return NanoHTTPD
.newFixedLengthResponse(builder
.toString());
661 // /story/luid/chapter/para <-- text/image
662 // /story/luid/cover <-- image
663 // /story/luid/metadata <-- json
664 // /story/luid/json <-- json, whole chapter (no images)
665 private Response
getStoryPart(String uri
, List
<String
> whitelist
) {
666 String
[] cover
= uri
.split("/");
669 if (cover
.length
< off
+ 2) {
670 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
671 NanoHTTPD
.MIME_PLAINTEXT
, null);
674 String luid
= cover
[off
+ 0];
675 String chapterStr
= cover
[off
+ 1];
676 String imageStr
= cover
.length
< off
+ 3 ?
null : cover
[off
+ 2];
678 // 1-based (0 = desc)
680 if (chapterStr
!= null && !"cover".equals(chapterStr
)
681 && !"metadata".equals(chapterStr
)
682 && !"json".equals(chapterStr
)) {
684 chapter
= Integer
.parseInt(chapterStr
);
686 throw new NumberFormatException();
688 } catch (NumberFormatException e
) {
689 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
690 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
696 if (imageStr
!= null) {
698 paragraph
= Integer
.parseInt(imageStr
);
700 throw new NumberFormatException();
702 } catch (NumberFormatException e
) {
703 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
704 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
708 String mimeType
= NanoHTTPD
.MIME_PLAINTEXT
;
709 InputStream in
= null;
711 if ("cover".equals(chapterStr
)) {
712 Image img
= getCover(luid
, whitelist
);
714 in
= img
.newInputStream();
716 // TODO: get correct image type
717 mimeType
= "image/png";
718 } else if ("metadata".equals(chapterStr
)) {
719 MetaData meta
= meta(luid
, whitelist
);
720 JSONObject json
= JsonIO
.toJson(meta
);
721 mimeType
= "application/json";
722 in
= new ByteArrayInputStream(json
.toString().getBytes());
723 } else if ("json".equals(chapterStr
)) {
724 Story story
= story(luid
, whitelist
);
725 JSONObject json
= JsonIO
.toJson(story
);
726 mimeType
= "application/json";
727 in
= new ByteArrayInputStream(json
.toString().getBytes());
729 Story story
= story(luid
, whitelist
);
732 StringBuilder builder
= new StringBuilder();
733 for (Paragraph p
: story
.getMeta().getResume()) {
734 if (builder
.length() == 0) {
735 builder
.append("\n");
737 builder
.append(p
.getContent());
740 in
= new ByteArrayInputStream(
741 builder
.toString().getBytes("utf-8"));
743 Paragraph para
= story
.getChapters().get(chapter
- 1)
744 .getParagraphs().get(paragraph
- 1);
745 Image img
= para
.getContentImage();
746 if (para
.getType() == ParagraphType
.IMAGE
) {
747 // TODO: get correct image type
748 mimeType
= "image/png";
749 in
= img
.newInputStream();
751 in
= new ByteArrayInputStream(
752 para
.getContent().getBytes("utf-8"));
757 } catch (IndexOutOfBoundsException e
) {
758 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
759 NanoHTTPD
.MIME_PLAINTEXT
,
760 "Chapter or paragraph does not exist");
761 } catch (IOException e
) {
762 Instance
.getInstance().getTraceHandler()
763 .error(new IOException("Cannot get image: " + uri
, e
));
764 return NanoHTTPD
.newFixedLengthResponse(Status
.INTERNAL_ERROR
,
765 NanoHTTPD
.MIME_PLAINTEXT
, "Error when processing request");
768 return newInputStreamResponse(mimeType
, in
);
771 private Response
getViewer(Map
<String
, String
> cookies
, String uri
,
772 List
<String
> whitelist
) {
773 String
[] cover
= uri
.split("/");
776 if (cover
.length
< off
+ 2) {
777 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
778 NanoHTTPD
.MIME_PLAINTEXT
, null);
781 String type
= cover
[off
+ 0];
782 String luid
= cover
[off
+ 1];
783 String chapterStr
= cover
.length
< off
+ 3 ?
null : cover
[off
+ 2];
784 String paragraphStr
= cover
.length
< off
+ 4 ?
null : cover
[off
+ 3];
786 // 1-based (0 = desc)
788 if (chapterStr
!= null) {
790 chapter
= Integer
.parseInt(chapterStr
);
792 throw new NumberFormatException();
794 } catch (NumberFormatException e
) {
795 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
796 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
802 if (paragraphStr
!= null) {
804 paragraph
= Integer
.parseInt(paragraphStr
);
805 if (paragraph
<= 0) {
806 throw new NumberFormatException();
808 } catch (NumberFormatException e
) {
809 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
810 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
815 Story story
= story(luid
, whitelist
);
817 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
818 NanoHTTPD
.MIME_PLAINTEXT
, "Story not found");
821 StringBuilder builder
= new StringBuilder();
822 appendPreHtml(builder
, false);
824 // For images documents, always go to the images if not chap 0 desc
825 if (story
.getMeta().isImageDocument()) {
826 if (chapter
> 0 && paragraph
<= 0)
832 chap
= story
.getMeta().getResume();
835 chap
= story
.getChapters().get(chapter
- 1);
836 } catch (IndexOutOfBoundsException e
) {
837 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
838 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter not found");
842 String first
, previous
, next
, last
;
844 StringBuilder content
= new StringBuilder();
846 String disabledLeft
= "";
847 String disabledRight
= "";
848 String disabledZoomReal
= "";
849 String disabledZoomWidth
= "";
850 String disabledZoomHeight
= "";
852 if (paragraph
<= 0) {
853 first
= getViewUrl(luid
, 0, null);
854 previous
= getViewUrl(luid
, (Math
.max(chapter
- 1, 0)), null);
855 next
= getViewUrl(luid
,
856 (Math
.min(chapter
+ 1, story
.getChapters().size())),
858 last
= getViewUrl(luid
, story
.getChapters().size(), null);
860 StringBuilder desc
= new StringBuilder();
863 desc
.append("<h1 class='title'>");
864 desc
.append(story
.getMeta().getTitle());
865 desc
.append("</h1>\n");
866 desc
.append("<div class='desc'>\n");
867 desc
.append("\t<div class='cover'>\n");
868 desc
.append("\t\t<img src='/story/" + luid
+ "/cover'/>\n");
869 desc
.append("\t</div>\n");
870 desc
.append("\t<table class='details'>\n");
871 Map
<String
, String
> details
= BasicLibrary
872 .getMetaDesc(story
.getMeta());
873 for (String key
: details
.keySet()) {
874 appendTableRow(desc
, 2, key
, details
.get(key
));
876 desc
.append("\t</table>\n");
877 desc
.append("</div>\n");
878 desc
.append("<h1 class='title'>Description</h1>\n");
881 content
.append("<div class='viewer text'>\n");
882 content
.append(desc
);
883 String description
= new TextOutput(false).convert(chap
,
885 content
.append(chap
.getParagraphs().size() <= 0
886 ?
"No content provided."
888 content
.append("</div>\n");
891 disabledLeft
= " disabled='disbaled'";
892 if (chapter
>= story
.getChapters().size())
893 disabledRight
= " disabled='disbaled'";
895 first
= getViewUrl(luid
, chapter
, 1);
896 previous
= getViewUrl(luid
, chapter
,
897 (Math
.max(paragraph
- 1, 1)));
898 next
= getViewUrl(luid
, chapter
,
899 (Math
.min(paragraph
+ 1, chap
.getParagraphs().size())));
900 last
= getViewUrl(luid
, chapter
, chap
.getParagraphs().size());
903 disabledLeft
= " disabled='disbaled'";
904 if (paragraph
>= chap
.getParagraphs().size())
905 disabledRight
= " disabled='disbaled'";
907 // First -> previous *chapter*
910 first
= getViewUrl(luid
, (Math
.max(chapter
- 1, 0)), null);
911 if (paragraph
<= 1) {
915 Paragraph para
= null;
917 para
= chap
.getParagraphs().get(paragraph
- 1);
918 } catch (IndexOutOfBoundsException e
) {
919 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
920 NanoHTTPD
.MIME_PLAINTEXT
,
921 "Paragraph " + paragraph
+ " not found");
924 if (para
.getType() == ParagraphType
.IMAGE
) {
925 String zoomStyle
= "max-width: 100%;";
926 disabledZoomWidth
= " disabled='disabled'";
927 String zoomOption
= cookies
.get("zoom");
928 if (zoomOption
!= null && !zoomOption
.isEmpty()) {
929 if (zoomOption
.equals("real")) {
931 disabledZoomWidth
= "";
932 disabledZoomReal
= " disabled='disabled'";
933 } else if (zoomOption
.equals("width")) {
934 zoomStyle
= "max-width: 100%;";
935 } else if (zoomOption
.equals("height")) {
936 // see height of navbar + optionbar
937 zoomStyle
= "max-height: calc(100% - 128px);";
938 disabledZoomWidth
= "";
939 disabledZoomHeight
= " disabled='disabled'";
943 content
.append(String
.format("" //
944 + "<a class='viewer link' href='%s'>"
945 + "<img class='viewer img' style='%s' src='%s'/>"
949 getStoryUrl(luid
, chapter
, paragraph
)));
951 content
.append(String
.format("" //
952 + "<div class='viewer text'>%s</div>", //
957 builder
.append(String
.format("" //
958 + "<div class='bar navbar'>\n" //
959 + "\t<a%s class='button first' href='%s'><<</a>\n"//
960 + "\t<a%s class='button previous' href='%s'><</a>\n" //
961 + "\t<div class='gotobox itemsbox'>\n" //
962 + "\t\t<div class='button goto'>%d</div>\n" //
963 + "\t\t<div class='items goto'>\n", //
964 disabledLeft
, first
, //
965 disabledLeft
, previous
, //
966 paragraph
> 0 ? paragraph
: chapter
//
969 // List of chap/para links
971 String blink
= "/view/story/" + luid
+ "/";
972 appendItemA(builder
, 3, blink
+ "0", "Description",
973 paragraph
== 0 && chapter
== 0);
976 blink
= blink
+ chapter
+ "/";
977 for (int i
= 1; i
<= chap
.getParagraphs().size(); i
++) {
978 appendItemA(builder
, 3, blink
+ i
, "Image " + i
,
983 for (Chapter c
: story
.getChapters()) {
984 String chapName
= "Chapter " + c
.getNumber();
985 if (c
.getName() != null && !c
.getName().isEmpty()) {
986 chapName
+= ": " + c
.getName();
989 appendItemA(builder
, 3, blink
+ i
, chapName
, chapter
== i
);
995 builder
.append(String
.format("" //
998 + "\t<a%s class='button next' href='%s'>></a>\n" //
999 + "\t<a%s class='button last' href='%s'>>></a>\n"//
1001 disabledRight
, next
, //
1002 disabledRight
, last
//
1005 builder
.append(content
);
1007 builder
.append("<div class='bar optionbar ");
1008 if (paragraph
> 0) {
1009 builder
.append("s4");
1011 builder
.append("s1");
1013 builder
.append("'>\n");
1014 builder
.append(" <a class='button back' href='/'>BACK</a>\n");
1016 if (paragraph
> 0) {
1017 builder
.append(String
.format("" //
1018 + "\t<a%s class='button zoomreal' href='%s'>REAL</a>\n"//
1019 + "\t<a%s class='button zoomwidth' href='%s'>WIDTH</a>\n"//
1020 + "\t<a%s class='button zoomheight' href='%s'>HEIGHT</a>\n"//
1023 uri
+ "?optionName=zoom&optionValue=real", //
1025 uri
+ "?optionName=zoom&optionValue=width", //
1027 uri
+ "?optionName=zoom&optionValue=height" //
1031 appendPostHtml(builder
);
1032 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
1033 NanoHTTPD
.MIME_HTML
, builder
.toString());
1034 } catch (IOException e
) {
1035 Instance
.getInstance().getTraceHandler()
1036 .error(new IOException("Cannot get image: " + uri
, e
));
1037 return NanoHTTPD
.newFixedLengthResponse(Status
.INTERNAL_ERROR
,
1038 NanoHTTPD
.MIME_PLAINTEXT
, "Error when processing request");
1042 private Response
newInputStreamResponse(String mimeType
, InputStream in
) {
1044 return NanoHTTPD
.newFixedLengthResponse(Status
.NO_CONTENT
, "",
1047 return NanoHTTPD
.newChunkedResponse(Status
.OK
, mimeType
, in
);
1050 private String
getContentOf(String file
) {
1051 InputStream in
= IOUtils
.openResource(WebLibraryServerIndex
.class,
1055 return IOUtils
.readSmallStream(in
);
1056 } catch (IOException e
) {
1057 Instance
.getInstance().getTraceHandler().error(
1058 new IOException("Cannot get file: index.pre.html", e
));
1065 private String
getViewUrl(String luid
, int chap
, Integer para
) {
1066 return VIEWER_URL
//
1067 .replace("{luid}", luid
) //
1068 .replace("{chap}", Integer
.toString(chap
)) //
1070 para
== null ?
"" : "/" + Integer
.toString(para
));
1073 private String
getStoryUrl(String luid
, int chap
, Integer para
) {
1075 .replace("{luid}", luid
) //
1076 .replace("{chap}", Integer
.toString(chap
)) //
1077 .replace("{para}", para
== null ?
"" : Integer
.toString(para
));
1080 private String
getStoryUrlCover(String luid
) {
1081 return STORY_URL_COVER
//
1082 .replace("{luid}", luid
);
1085 private MetaData
meta(String luid
, List
<String
> whitelist
)
1086 throws IOException
{
1087 BasicLibrary lib
= Instance
.getInstance().getLibrary();
1088 MetaData meta
= lib
.getInfo(luid
);
1089 if (!whitelist
.isEmpty() && !whitelist
.contains(meta
.getSource())) {
1096 private Image
getCover(String luid
, List
<String
> whitelist
)
1097 throws IOException
{
1098 MetaData meta
= meta(luid
, whitelist
);
1100 BasicLibrary lib
= Instance
.getInstance().getLibrary();
1101 return lib
.getCover(meta
.getLuid());
1107 // NULL if not whitelist OK or if not found
1108 private Story
story(String luid
, List
<String
> whitelist
)
1109 throws IOException
{
1110 synchronized (storyCache
) {
1111 if (storyCache
.containsKey(luid
)) {
1112 Story story
= storyCache
.get(luid
);
1113 if (!whitelist
.isEmpty()
1114 && !whitelist
.contains(story
.getMeta().getSource())) {
1123 MetaData meta
= meta(luid
, whitelist
);
1125 BasicLibrary lib
= Instance
.getInstance().getLibrary();
1126 story
= lib
.getStory(luid
, null);
1127 long size
= sizeOf(story
);
1129 synchronized (storyCache
) {
1130 // Could have been added by another request
1131 if (!storyCache
.containsKey(luid
)) {
1132 while (!storyCacheOrder
.isEmpty()
1133 && storyCacheSize
+ size
> maxStoryCacheSize
) {
1134 String oldestLuid
= storyCacheOrder
.removeFirst();
1135 Story oldestStory
= storyCache
.remove(oldestLuid
);
1136 maxStoryCacheSize
-= sizeOf(oldestStory
);
1139 storyCacheOrder
.add(luid
);
1140 storyCache
.put(luid
, story
);
1148 private long sizeOf(Story story
) {
1150 for (Chapter chap
: story
) {
1151 for (Paragraph para
: chap
) {
1152 if (para
.getType() == ParagraphType
.IMAGE
) {
1153 size
+= para
.getContentImage().getSize();
1155 size
+= para
.getContent().length();
1163 private void appendPreHtml(StringBuilder builder
, boolean banner
) {
1164 String favicon
= "favicon.ico";
1165 String icon
= Instance
.getInstance().getUiConfig()
1166 .getString(UiConfig
.PROGRAM_ICON
);
1168 favicon
= "icon_" + icon
.replace("-", "_") + ".png";
1172 getContentOf("index.pre.html").replace("favicon.ico", favicon
));
1175 builder
.append("<div class='banner'>\n");
1176 builder
.append("\t<img class='ico' src='/") //
1179 builder
.append("\t<h1>Fanfix</h1>\n");
1180 builder
.append("\t<h2>") //
1181 .append(Version
.getCurrentVersion()) //
1183 builder
.append("</div>\n");
1187 private void appendPostHtml(StringBuilder builder
) {
1188 builder
.append(getContentOf("index.post.html"));
1191 private void appendOption(StringBuilder builder
, int depth
, String name
,
1192 String value
, String selected
) {
1193 for (int i
= 0; i
< depth
; i
++) {
1194 builder
.append("\t");
1196 builder
.append("<option value='").append(value
).append("'");
1197 if (value
.equals(selected
)) {
1198 builder
.append(" selected='selected'");
1200 builder
.append(">").append(name
).append("</option>\n");
1203 private void appendTableRow(StringBuilder builder
, int depth
,
1205 for (int i
= 0; i
< depth
; i
++) {
1206 builder
.append("\t");
1210 builder
.append("<tr>");
1211 for (String td
: tds
) {
1212 builder
.append("<td class='col");
1213 builder
.append(col
++);
1214 builder
.append("'>");
1216 builder
.append("</td>");
1218 builder
.append("</tr>\n");
1221 private void appendItemA(StringBuilder builder
, int depth
, String link
,
1222 String name
, boolean selected
) {
1223 for (int i
= 0; i
< depth
; i
++) {
1224 builder
.append("\t");
1227 builder
.append("<a href='");
1228 builder
.append(link
);
1229 builder
.append("' class='item goto");
1231 builder
.append(" selected");
1233 builder
.append("'>");
1234 builder
.append(name
);
1235 builder
.append("</a>\n");
1238 public static void main(String
[] args
) throws IOException
{
1240 WebLibraryServer web
= new WebLibraryServer(false);