1 package be
.nikiroo
.fanfix
.library
;
4 import java
.io
.FileInputStream
;
5 import java
.io
.IOException
;
6 import java
.io
.InputStream
;
7 import java
.security
.KeyStore
;
8 import java
.util
.HashMap
;
12 import javax
.net
.ssl
.KeyManagerFactory
;
13 import javax
.net
.ssl
.SSLServerSocketFactory
;
15 import be
.nikiroo
.fanfix
.Instance
;
16 import be
.nikiroo
.fanfix
.bundles
.Config
;
17 import be
.nikiroo
.fanfix
.bundles
.UiConfig
;
18 import be
.nikiroo
.fanfix
.data
.Chapter
;
19 import be
.nikiroo
.fanfix
.data
.MetaData
;
20 import be
.nikiroo
.fanfix
.data
.Paragraph
;
21 import be
.nikiroo
.fanfix
.data
.Paragraph
.ParagraphType
;
22 import be
.nikiroo
.fanfix
.data
.Story
;
23 import be
.nikiroo
.fanfix
.library
.WebLibraryServer
.WLoginResult
;
24 import be
.nikiroo
.fanfix
.library
.web
.WebLibraryServerIndex
;
25 import be
.nikiroo
.fanfix
.reader
.TextOutput
;
26 import be
.nikiroo
.utils
.IOUtils
;
27 import be
.nikiroo
.utils
.NanoHTTPD
;
28 import be
.nikiroo
.utils
.NanoHTTPD
.IHTTPSession
;
29 import be
.nikiroo
.utils
.NanoHTTPD
.Response
;
30 import be
.nikiroo
.utils
.NanoHTTPD
.Response
.Status
;
31 import be
.nikiroo
.utils
.TraceHandler
;
32 import be
.nikiroo
.utils
.Version
;
34 abstract class WebLibraryServerHtml
implements Runnable
{
35 private NanoHTTPD server
;
36 protected TraceHandler tracer
= new TraceHandler();
38 abstract protected WLoginResult
login(String who
, String cookie
);
40 abstract protected WLoginResult
login(String who
, String key
,
43 abstract protected WLoginResult
login(boolean badLogin
, boolean badCookie
);
45 abstract protected Response
getList(String uri
, WLoginResult login
)
48 abstract protected Response
getStoryPart(String uri
, WLoginResult login
);
50 abstract protected Response
setStoryPart(String uri
, String value
,
51 WLoginResult login
) throws IOException
;
53 abstract protected Response
getCover(String uri
, WLoginResult login
)
56 abstract protected Response
setCover(String uri
, String luid
,
57 WLoginResult login
) throws IOException
;
59 abstract protected List
<MetaData
> metas(WLoginResult login
)
62 abstract protected Story
story(String luid
, WLoginResult login
)
65 protected abstract Response
imprt(String uri
, String url
,
66 WLoginResult login
) throws IOException
;
68 protected abstract Response
imprtProgress(String uri
, WLoginResult login
);
70 protected abstract Response
delete(String uri
, WLoginResult login
)
74 * Wait until all operations are done and stop the server.
76 * All the new R/W operations will be refused after a call to stop.
78 protected abstract Response
stop(WLoginResult login
);
80 public WebLibraryServerHtml(boolean secure
) throws IOException
{
81 Integer port
= Instance
.getInstance().getConfig()
82 .getInteger(Config
.SERVER_PORT
);
84 throw new IOException(
85 "Cannot start web server: port not specified");
88 SSLServerSocketFactory ssf
= null;
90 String keystorePath
= Instance
.getInstance().getConfig()
91 .getString(Config
.SERVER_SSL_KEYSTORE
, "");
92 String keystorePass
= Instance
.getInstance().getConfig()
93 .getString(Config
.SERVER_SSL_KEYSTORE_PASS
);
95 if (secure
&& keystorePath
.isEmpty()) {
96 throw new IOException(
97 "Cannot start a secure web server: no keystore.jks file povided");
100 if (!keystorePath
.isEmpty()) {
101 File keystoreFile
= new File(keystorePath
);
103 KeyStore keystore
= KeyStore
104 .getInstance(KeyStore
.getDefaultType());
105 InputStream keystoreStream
= new FileInputStream(
108 keystore
.load(keystoreStream
,
109 keystorePass
.toCharArray());
110 KeyManagerFactory keyManagerFactory
= KeyManagerFactory
111 .getInstance(KeyManagerFactory
112 .getDefaultAlgorithm());
113 keyManagerFactory
.init(keystore
,
114 keystorePass
.toCharArray());
115 ssf
= NanoHTTPD
.makeSSLSocketFactory(keystore
,
118 keystoreStream
.close();
120 } catch (Exception e
) {
121 throw new IOException(e
.getMessage());
126 server
= new NanoHTTPD(port
) {
128 public Response
serve(final IHTTPSession session
) {
129 super.serve(session
);
131 String query
= session
.getQueryParameterString(); // a=a%20b&dd=2
132 Method method
= session
.getMethod(); // GET, POST..
133 String uri
= session
.getUri(); // /home.html
135 // need them in real time (not just those sent by the UA)
136 Map
<String
, String
> cookies
= new HashMap
<String
, String
>();
137 for (String cookie
: session
.getCookies()) {
138 cookies
.put(cookie
, session
.getCookies().read(cookie
));
141 WLoginResult login
= null;
142 Map
<String
, String
> params
= session
.getParms();
143 String who
= session
.getRemoteHostName()
144 + session
.getRemoteIpAddress();
145 if (params
.get("login") != null) {
146 login
= login(who
, params
.get("password"),
147 params
.get("login"));
149 String cookie
= cookies
.get("cookie");
150 login
= login(who
, cookie
);
153 if (login
.isSuccess()) {
155 session
.getCookies().set(new Cookie("cookie",
156 login
.getCookie(), "30; path=/"));
159 String optionName
= params
.get("optionName");
160 if (optionName
!= null && !optionName
.isEmpty()) {
161 String optionNo
= params
.get("optionNo");
162 String optionValue
= params
.get("optionValue");
163 if (optionNo
!= null || optionValue
== null
164 || optionValue
.isEmpty()) {
165 session
.getCookies().delete(optionName
);
166 cookies
.remove(optionName
);
168 session
.getCookies().set(new Cookie(optionName
,
169 optionValue
, "; path=/"));
170 cookies
.put(optionName
, optionValue
);
176 if (!login
.isSuccess()
177 && WebLibraryUrls
.isSupportedUrl(uri
, true)) {
178 rep
= loginPage(login
, uri
);
183 if (WebLibraryUrls
.isSupportedUrl(uri
, false)) {
184 if (WebLibraryUrls
.INDEX_URL
.equals(uri
)) {
185 rep
= root(session
, cookies
, login
);
186 } else if (WebLibraryUrls
.VERSION_URL
.equals(uri
)) {
187 rep
= newFixedLengthResponse(Status
.OK
,
189 Version
.getCurrentVersion().toString());
190 } else if (WebLibraryUrls
.isCoverUrl(uri
)) {
191 String luid
= params
.get("luid");
193 rep
= setCover(uri
, luid
, login
);
195 rep
= getCover(uri
, login
);
197 } else if (WebLibraryUrls
.isListUrl(uri
)) {
198 rep
= getList(uri
, login
);
199 } else if (WebLibraryUrls
.isStoryUrl(uri
)) {
200 String value
= params
.get("value");
202 rep
= setStoryPart(uri
, value
, login
);
204 rep
= getStoryPart(uri
, login
);
206 } else if (WebLibraryUrls
.isViewUrl(uri
)) {
207 rep
= getViewer(cookies
, uri
, login
);
208 } else if (WebLibraryUrls
.LOGOUT_URL
.equals(uri
)) {
209 session
.getCookies().delete("cookie");
210 cookies
.remove("cookie");
211 rep
= loginPage(login(false, false), uri
);
212 } else if (WebLibraryUrls
.isImprtUrl(uri
)) {
213 String url
= params
.get("url");
215 rep
= imprt(uri
, url
, login
);
217 rep
= imprtProgress(uri
, login
);
219 } else if (WebLibraryUrls
.isDeleteUrl(uri
)) {
220 rep
= delete(uri
, login
);
221 } else if (WebLibraryUrls
.EXIT_URL
.equals(uri
)) {
222 rep
= WebLibraryServerHtml
.this.stop(login
);
224 getTraceHandler().error(
225 "Supported URL was not processed: "
227 rep
= newFixedLengthResponse(
228 Status
.INTERNAL_ERROR
,
229 NanoHTTPD
.MIME_PLAINTEXT
,
230 "An error happened");
233 if (uri
.startsWith("/"))
234 uri
= uri
.substring(1);
235 InputStream in
= IOUtils
.openResource(
236 WebLibraryServerIndex
.class, uri
);
238 String mimeType
= MIME_PLAINTEXT
;
239 if (uri
.endsWith(".css")) {
240 mimeType
= "text/css";
241 } else if (uri
.endsWith(".html")) {
242 mimeType
= "text/html";
243 } else if (uri
.endsWith(".js")) {
244 mimeType
= "text/javascript";
246 rep
= newChunkedResponse(Status
.OK
, mimeType
,
251 getTraceHandler().trace("404: " + uri
);
252 rep
= newFixedLengthResponse(Status
.NOT_FOUND
,
253 NanoHTTPD
.MIME_PLAINTEXT
, "Not Found");
256 } catch (Exception e
) {
257 Instance
.getInstance().getTraceHandler().error(
258 new IOException("Cannot process web request",
260 rep
= newFixedLengthResponse(Status
.INTERNAL_ERROR
,
261 NanoHTTPD
.MIME_PLAINTEXT
, "An error occured");
270 getTraceHandler().trace("Install SSL on the web server...");
271 server
.makeSecure(ssf
, null);
272 getTraceHandler().trace("Done.");
279 server
.start(NanoHTTPD
.SOCKET_READ_TIMEOUT
, false);
280 } catch (IOException e
) {
281 tracer
.error(new IOException("Cannot start the web server", e
));
285 protected void doStop() {
290 * The traces handler for this {@link WebLibraryServerHtml}.
292 * @return the traces handler
294 public TraceHandler
getTraceHandler() {
299 * The traces handler for this {@link WebLibraryServerHtml}.
302 * the new traces handler
304 public void setTraceHandler(TraceHandler tracer
) {
305 if (tracer
== null) {
306 tracer
= new TraceHandler(false, false, false);
309 this.tracer
= tracer
;
312 private Response
loginPage(WLoginResult login
, String uri
) {
313 StringBuilder builder
= new StringBuilder();
315 appendPreHtml(builder
, true);
317 if (login
.isBadLogin()) {
319 "\t\t<div class='error'>Bad login or password</div>");
320 } else if (login
.isBadCookie()) {
322 "\t\t<div class='error'>Your session timed out</div>");
325 if (WebLibraryUrls
.LOGOUT_URL
.equals(uri
)) {
326 uri
= WebLibraryUrls
.INDEX_URL
;
329 builder
.append("\t\t<form method='POST' action='" + uri
330 + "' class='login'>\n");
332 "\t\t\t<p>You must be logged into the system to see the stories.</p>");
333 builder
.append("\t\t\t<input type='text' name='login' />\n");
334 builder
.append("\t\t\t<input type='password' name='password' />\n");
335 builder
.append("\t\t\t<input type='submit' value='Login' />\n");
336 builder
.append("\t\t</form>\n");
338 appendPostHtml(builder
);
340 return NanoHTTPD
.newFixedLengthResponse(Status
.FORBIDDEN
,
341 NanoHTTPD
.MIME_HTML
, builder
.toString());
344 private Response
root(IHTTPSession session
, Map
<String
, String
> cookies
,
345 WLoginResult login
) throws IOException
{
346 BasicLibrary lib
= Instance
.getInstance().getLibrary();
347 MetaResultList result
= new MetaResultList(metas(login
));
348 StringBuilder builder
= new StringBuilder();
350 appendPreHtml(builder
, true);
352 Map
<String
, String
> params
= session
.getParms();
354 String filter
= cookies
.get("filter");
355 if (params
.get("optionNo") != null)
357 if (filter
== null) {
361 String browser
= params
.get("browser") == null ?
""
362 : params
.get("browser");
363 String browser2
= params
.get("browser2") == null ?
""
364 : params
.get("browser2");
365 String browser3
= params
.get("browser3") == null ?
""
366 : params
.get("browser3");
368 String filterSource
= null;
369 String filterAuthor
= null;
370 String filterTag
= null;
372 // TODO: javascript in realtime, using visible=false + hide [submit]
374 StringBuilder selects
= new StringBuilder();
375 boolean sourcesSel
= false;
376 boolean authorsSel
= false;
377 boolean tagsSel
= false;
379 String selectTemplate
= getTemplate("browser.select");
381 if (!browser
.isEmpty()) {
382 StringBuilder options
= new StringBuilder();
384 if (browser
.equals("sources")) {
386 filterSource
= browser2
.isEmpty() ? filterSource
: browser2
;
388 // TODO: if 1 group -> no group
389 Map
<String
, List
<String
>> sources
= result
.getSourcesGrouped();
390 for (String source
: sources
.keySet()) {
391 appendOption(options
, 5, source
, source
, browser2
);
393 } else if (browser
.equals("authors")) {
395 filterAuthor
= browser2
.isEmpty() ? filterAuthor
: browser2
;
397 // TODO: if 1 group -> no group
398 Map
<String
, List
<String
>> authors
= result
.getAuthorsGrouped();
399 for (String author
: authors
.keySet()) {
400 appendOption(options
, 5, author
, author
, browser2
);
402 } else if (browser
.equals("tags")) {
404 filterTag
= browser2
.isEmpty() ? filterTag
: browser2
;
406 for (String tag
: result
.getTags()) {
407 appendOption(options
, 5, tag
, tag
, browser2
);
411 selects
.append(selectTemplate
//
412 .replace("${name}", "browser2") //
413 .replace("${value}", browser2
) //
414 .replace("${options}", options
.toString()) //
418 if (!browser2
.isEmpty()) {
419 StringBuilder options
= new StringBuilder();
421 if (browser
.equals("sources")) {
422 filterSource
= browser3
.isEmpty() ? filterSource
: browser3
;
423 Map
<String
, List
<String
>> sourcesGrouped
= result
424 .getSourcesGrouped();
425 List
<String
> sources
= sourcesGrouped
.get(browser2
);
426 if (sources
!= null && !sources
.isEmpty()) {
427 // TODO: single empty value
428 for (String source
: sources
) {
429 appendOption(options
, 5, source
, source
, browser3
);
432 } else if (browser
.equals("authors")) {
433 filterAuthor
= browser3
.isEmpty() ? filterAuthor
: browser3
;
434 Map
<String
, List
<String
>> authorsGrouped
= result
435 .getAuthorsGrouped();
436 List
<String
> authors
= authorsGrouped
.get(browser2
);
437 if (authors
!= null && !authors
.isEmpty()) {
438 // TODO: single empty value
439 for (String author
: authors
) {
440 appendOption(options
, 5, author
, author
, browser3
);
445 selects
.append(selectTemplate
//
446 .replace("${name}", "browser3") //
447 .replace("${value}", browser3
) //
448 .replace("${options}", options
.toString()) //
452 String sel
= "selected='selected'";
453 builder
.append(getTemplate("browser") //
454 .replace("${sourcesSelected}", sourcesSel ? sel
: "") //
455 .replace("${authorsSelected}", authorsSel ? sel
: "") //
456 .replace("${tagsSelected}", tagsSel ? sel
: "") //
457 .replace("${filter}", filter
) //
458 .replace("${selects}", selects
.toString()) //
461 builder
.append("\t\t<div class='books'>\n");
462 for (MetaData meta
: result
.getMetas()) {
463 if (!filter
.isEmpty() && !meta
.getTitle().toLowerCase()
464 .contains(filter
.toLowerCase())) {
469 if (filterSource
!= null
470 && !filterSource
.equals(meta
.getSource())) {
475 if (filterAuthor
!= null
476 && !filterAuthor
.equals(meta
.getAuthor())) {
480 if (filterTag
!= null && !meta
.getTags().contains(filterTag
)) {
485 if (meta
.getAuthor() != null && !meta
.getAuthor().isEmpty()) {
486 author
= "(" + meta
.getAuthor() + ")";
489 String cachedClass
= "cached";
490 String cached
= "◉";
491 if (!lib
.isCached(meta
.getLuid())) {
492 cachedClass
= "uncached";
496 builder
.append(getTemplate("bookline") //
498 WebLibraryUrls
.getViewUrl(meta
.getLuid(), null,
500 .replace("${luid}", meta
.getLuid()) //
501 .replace("${title}", meta
.getTitle()) //
502 .replace("${author}", author
) //
503 .replace("${cachedClass}", cachedClass
) //
504 .replace("${cached}", cached
) //
507 builder
.append("\t\t</div>\n");
509 appendPostHtml(builder
);
510 return NanoHTTPD
.newFixedLengthResponse(builder
.toString());
513 private Response
getViewer(Map
<String
, String
> cookies
, String uri
,
514 WLoginResult login
) {
515 String
[] cover
= uri
.split("/");
518 if (cover
.length
< off
+ 2) {
519 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
520 NanoHTTPD
.MIME_PLAINTEXT
, null);
523 String type
= cover
[off
+ 0];
524 String luid
= cover
[off
+ 1];
525 String chapterStr
= cover
.length
< off
+ 3 ?
null : cover
[off
+ 2];
526 String paragraphStr
= cover
.length
< off
+ 4 ?
null : cover
[off
+ 3];
528 // 1-based (0 = desc)
530 if (chapterStr
!= null) {
532 chapter
= Integer
.parseInt(chapterStr
);
534 throw new NumberFormatException();
536 } catch (NumberFormatException e
) {
537 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
538 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
544 if (paragraphStr
!= null) {
546 paragraph
= Integer
.parseInt(paragraphStr
);
547 if (paragraph
<= 0) {
548 throw new NumberFormatException();
550 } catch (NumberFormatException e
) {
551 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
552 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
557 Story story
= story(luid
, login
);
559 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
560 NanoHTTPD
.MIME_PLAINTEXT
, "Story not found");
563 StringBuilder builder
= new StringBuilder();
564 appendPreHtml(builder
, false);
566 // For images documents, always go to the images if not chap 0 desc
567 if (story
.getMeta().isImageDocument()) {
568 if (chapter
> 0 && paragraph
<= 0)
574 chap
= story
.getMeta().getResume();
577 chap
= story
.getChapters().get(chapter
- 1);
578 } catch (IndexOutOfBoundsException e
) {
579 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
580 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter not found");
584 String first
, previous
, next
, last
;
586 StringBuilder content
= new StringBuilder();
588 String disabledLeft
= "";
589 String disabledRight
= "";
590 String disabledZoomReal
= "";
591 String disabledZoomWidth
= "";
592 String disabledZoomHeight
= "";
594 if (paragraph
<= 0) {
595 first
= WebLibraryUrls
.getViewUrl(luid
, 0, null);
596 previous
= WebLibraryUrls
.getViewUrl(luid
,
597 (Math
.max(chapter
- 1, 0)), null);
598 next
= WebLibraryUrls
.getViewUrl(luid
,
599 (Math
.min(chapter
+ 1, story
.getChapters().size())),
601 last
= WebLibraryUrls
.getViewUrl(luid
,
602 story
.getChapters().size(), null);
604 StringBuilder desc
= new StringBuilder();
607 desc
.append("\t\t\t<h1 class='title'>")
608 .append(story
.getMeta().getTitle())
610 desc
.append("\t\t\t<div class='desc'>\n");
612 "\t\t\t\t<a href='" + next
+ "' class='cover'>\n");
613 desc
.append("\t\t\t\t\t<img src='/story/" + luid
615 desc
.append("\t\t\t\t</a>\n");
616 desc
.append("\t\t\t\t<table class='details'>\n");
617 Map
<String
, String
> details
= BasicLibrary
618 .getMetaDesc(story
.getMeta());
619 for (String key
: details
.keySet()) {
620 appendTableRow(desc
, 5, key
, details
.get(key
));
622 desc
.append("\t\t\t\t</table>\n");
623 desc
.append("\t\t\t</div>\n");
624 desc
.append("\t\t\t<h1 class='title'>Description</h1>\n");
627 content
.append("\t\t<div class='viewer text'>\n");
628 content
.append(desc
);
629 if (chap
.getParagraphs().size() <= 0) {
630 content
.append("\t\t\tNo content provided.\n");
633 "\t\t\t<!-- The text is in HTML 3.2, so it can also work in Java Swing: -->\n"); //
634 content
.append("\t\t\t").append(
635 new TextOutput(false).convert(chap
, chapter
> 0))
638 content
.append("\t\t</div>\n");
641 disabledLeft
= " disabled='disbaled'";
642 if (chapter
>= story
.getChapters().size())
643 disabledRight
= " disabled='disbaled'";
645 first
= WebLibraryUrls
.getViewUrl(luid
, chapter
, 1);
646 previous
= WebLibraryUrls
.getViewUrl(luid
, chapter
,
647 (Math
.max(paragraph
- 1, 1)));
648 next
= WebLibraryUrls
.getViewUrl(luid
, chapter
,
649 (Math
.min(paragraph
+ 1, chap
.getParagraphs().size())));
650 last
= WebLibraryUrls
.getViewUrl(luid
, chapter
,
651 chap
.getParagraphs().size());
654 disabledLeft
= " disabled='disbaled'";
655 if (paragraph
>= chap
.getParagraphs().size())
656 disabledRight
= " disabled='disbaled'";
658 // First -> previous *chapter*
661 first
= WebLibraryUrls
.getViewUrl(luid
,
662 (Math
.max(chapter
- 1, 0)), null);
663 if (paragraph
<= 1) {
667 Paragraph para
= null;
669 para
= chap
.getParagraphs().get(paragraph
- 1);
670 } catch (IndexOutOfBoundsException e
) {
671 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
672 NanoHTTPD
.MIME_PLAINTEXT
,
673 "Paragraph " + paragraph
+ " not found");
676 if (para
.getType() == ParagraphType
.IMAGE
) {
677 String zoomStyle
= "max-width: 100%;";
678 disabledZoomWidth
= " disabled='disabled'";
679 String zoomOption
= cookies
.get("zoom");
680 if (zoomOption
!= null && !zoomOption
.isEmpty()) {
681 if (zoomOption
.equals("real")) {
683 disabledZoomWidth
= "";
684 disabledZoomReal
= " disabled='disabled'";
685 } else if (zoomOption
.equals("width")) {
686 zoomStyle
= "max-width: 100%;";
687 } else if (zoomOption
.equals("height")) {
688 // see height of navbar + optionbar
689 zoomStyle
= "max-height: calc(100% - 128px);";
690 disabledZoomWidth
= "";
691 disabledZoomHeight
= " disabled='disabled'";
695 String javascript
= "document.getElementById(\"previous\").click(); return false;";
696 content
.append(String
.format("" //
697 + "\t\t<a class='viewer link' oncontextmenu='%s' href='%s'>\n"
698 + "\t\t\t<img class='viewer img' style='%s' src='%s'/>\n"
703 WebLibraryUrls
.getStoryUrl(luid
, chapter
,
706 content
.append(String
.format("" //
707 + "\t\t<div class='viewer text'>" //
714 builder
.append(String
.format("" //
715 + "\t\t<div class='bar navbar'>\n" //
716 + "\t\t\t<a%s class='button first' href='%s'><<</a>\n"//
717 + "\t\t\t<a%s id='previous' class='button previous' href='%s'><</a>\n" //
718 + "\t\t\t<div class='gotobox itemsbox'>\n" //
719 + "\t\t\t\t<div class='button goto'>%d</div>\n" //
720 + "\t\t\t\t<div class='items goto'>\n", //
721 disabledLeft
, first
, //
722 disabledLeft
, previous
, //
723 paragraph
> 0 ? paragraph
: chapter
//
726 // List of chap/para links
728 appendItemA(builder
, 5, WebLibraryUrls
.getViewUrl(luid
, 0, null),
729 "Description", paragraph
== 0 && chapter
== 0);
731 for (int i
= 1; i
<= chap
.getParagraphs().size(); i
++) {
732 appendItemA(builder
, 5,
733 WebLibraryUrls
.getViewUrl(luid
, chapter
, i
),
734 "Image " + i
, paragraph
== i
);
738 for (Chapter c
: story
.getChapters()) {
739 String chapName
= "Chapter " + c
.getNumber();
740 if (c
.getName() != null && !c
.getName().isEmpty()) {
741 chapName
+= ": " + c
.getName();
744 appendItemA(builder
, 5,
745 WebLibraryUrls
.getViewUrl(luid
, i
, null), chapName
,
752 builder
.append(String
.format("" //
753 + "\t\t\t\t</div>\n" //
754 + "\t\t\t</div>\n" //
755 + "\t\t\t<a%s class='button next' href='%s'>></a>\n" //
756 + "\t\t\t<a%s class='button last' href='%s'>>></a>\n"//
758 disabledRight
, next
, //
759 disabledRight
, last
//
762 builder
.append(content
);
764 builder
.append("\t\t<div class='bar optionbar");
766 builder
.append(" s4");
768 builder
.append(" s1");
770 builder
.append("'>\n");
771 builder
.append("\t\t\t<a class='button back' href='/'>BACK</a>\n");
774 builder
.append(String
.format("" //
775 + "\t\t\t<a%s class='button zoomreal' href='%s'>REAL</a>\n"//
776 + "\t\t\t<a%s class='button zoomwidth' href='%s'>WIDTH</a>\n"//
777 + "\t\t\t<a%s class='button zoomheight' href='%s'>HEIGHT</a>\n", //
779 uri
+ "?optionName=zoom&optionValue=real", //
781 uri
+ "?optionName=zoom&optionValue=width", //
783 uri
+ "?optionName=zoom&optionValue=height" //
787 builder
.append("\t\t</div>\n");
789 appendPostHtml(builder
);
790 return NanoHTTPD
.newFixedLengthResponse(Status
.OK
,
791 NanoHTTPD
.MIME_HTML
, builder
.toString());
792 } catch (IOException e
) {
793 Instance
.getInstance().getTraceHandler()
794 .error(new IOException("Cannot get image: " + uri
, e
));
795 return NanoHTTPD
.newFixedLengthResponse(Status
.INTERNAL_ERROR
,
796 NanoHTTPD
.MIME_PLAINTEXT
, "Error when processing request");
800 protected Response
newInputStreamResponse(String mimeType
, InputStream in
) {
802 return NanoHTTPD
.newFixedLengthResponse(Status
.NO_CONTENT
, "",
805 return NanoHTTPD
.newChunkedResponse(Status
.OK
, mimeType
, in
);
808 private String
getContentOf(String file
) {
809 InputStream in
= IOUtils
.openResource(WebLibraryServerIndex
.class,
813 return IOUtils
.readSmallStream(in
);
814 } catch (IOException e
) {
815 Instance
.getInstance().getTraceHandler().error(
816 new IOException("Cannot get file: index.pre.html", e
));
823 private void appendPreHtml(StringBuilder builder
, boolean banner
) {
824 String favicon
= "favicon.ico";
825 String icon
= Instance
.getInstance().getUiConfig()
826 .getString(UiConfig
.PROGRAM_ICON
);
828 favicon
= "icon_" + icon
.replace("-", "_") + ".png";
832 getContentOf("index.pre.html").replace("favicon.ico", favicon
));
835 builder
.append("\t\t<div class='banner'>\n");
836 builder
.append("\t\t\t<img class='ico' src='/") //
839 builder
.append("\t\t\t<h1>Fanfix</h1>\n");
840 builder
.append("\t\t\t<h2>") //
841 .append(Version
.getCurrentVersion()) //
843 builder
.append("\t\t</div>\n");
847 private void appendPostHtml(StringBuilder builder
) {
848 builder
.append(getContentOf("index.post.html"));
851 private void appendOption(StringBuilder builder
, int depth
, String name
,
852 String value
, String selected
) {
853 for (int i
= 0; i
< depth
; i
++) {
854 builder
.append("\t");
856 builder
.append("<option value='").append(value
).append("'");
857 if (value
.equals(selected
)) {
858 builder
.append(" selected='selected'");
860 builder
.append(">").append(name
).append("</option>\n");
863 private void appendTableRow(StringBuilder builder
, int depth
,
865 for (int i
= 0; i
< depth
; i
++) {
866 builder
.append("\t");
870 builder
.append("<tr>");
871 for (String td
: tds
) {
872 builder
.append("<td class='col");
873 builder
.append(col
++);
874 builder
.append("'>");
876 builder
.append("</td>");
878 builder
.append("</tr>\n");
881 private void appendItemA(StringBuilder builder
, int depth
, String link
,
882 String name
, boolean selected
) {
883 for (int i
= 0; i
< depth
; i
++) {
884 builder
.append("\t");
887 builder
.append("<a href='");
888 builder
.append(link
);
889 builder
.append("' class='item goto");
891 builder
.append(" selected");
893 builder
.append("'>");
894 builder
.append(name
);
895 builder
.append("</a>\n");
898 private String
getTemplate(String template
) throws IOException
{
899 InputStream in
= IOUtils
.openResource(WebLibraryServerIndex
.class,
902 return IOUtils
.readSmallStream(in
);