Merge from master
[nikiroo-utils.git] / library / WebLibraryServerHtml.java
1 package be.nikiroo.fanfix.library;
2
3 import java.io.File;
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;
9 import java.util.List;
10 import java.util.Map;
11
12 import javax.net.ssl.KeyManagerFactory;
13 import javax.net.ssl.SSLServerSocketFactory;
14
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;
33
34 abstract class WebLibraryServerHtml implements Runnable {
35 private NanoHTTPD server;
36 protected TraceHandler tracer = new TraceHandler();
37
38 abstract protected WLoginResult login(String who, String cookie);
39
40 abstract protected WLoginResult login(String who, String key,
41 String subkey);
42
43 abstract protected WLoginResult login(boolean badLogin, boolean badCookie);
44
45 abstract protected Response getList(String uri, WLoginResult login)
46 throws IOException;
47
48 abstract protected Response getStoryPart(String uri, WLoginResult login);
49
50 abstract protected Response setStoryPart(String uri, String value,
51 WLoginResult login) throws IOException;
52
53 abstract protected Response getCover(String uri, WLoginResult login)
54 throws IOException;
55
56 abstract protected Response setCover(String uri, String luid,
57 WLoginResult login) throws IOException;
58
59 abstract protected List<MetaData> metas(WLoginResult login)
60 throws IOException;
61
62 abstract protected Story story(String luid, WLoginResult login)
63 throws IOException;
64
65 protected abstract Response imprt(String uri, String url,
66 WLoginResult login) throws IOException;
67
68 protected abstract Response imprtProgress(String uri, WLoginResult login);
69
70 public WebLibraryServerHtml(boolean secure) throws IOException {
71 Integer port = Instance.getInstance().getConfig()
72 .getInteger(Config.SERVER_PORT);
73 if (port == null) {
74 throw new IOException(
75 "Cannot start web server: port not specified");
76 }
77
78 SSLServerSocketFactory ssf = null;
79 if (secure) {
80 String keystorePath = Instance.getInstance().getConfig()
81 .getString(Config.SERVER_SSL_KEYSTORE, "");
82 String keystorePass = Instance.getInstance().getConfig()
83 .getString(Config.SERVER_SSL_KEYSTORE_PASS);
84
85 if (secure && keystorePath.isEmpty()) {
86 throw new IOException(
87 "Cannot start a secure web server: no keystore.jks file povided");
88 }
89
90 if (!keystorePath.isEmpty()) {
91 File keystoreFile = new File(keystorePath);
92 try {
93 KeyStore keystore = KeyStore
94 .getInstance(KeyStore.getDefaultType());
95 InputStream keystoreStream = new FileInputStream(
96 keystoreFile);
97 try {
98 keystore.load(keystoreStream,
99 keystorePass.toCharArray());
100 KeyManagerFactory keyManagerFactory = KeyManagerFactory
101 .getInstance(KeyManagerFactory
102 .getDefaultAlgorithm());
103 keyManagerFactory.init(keystore,
104 keystorePass.toCharArray());
105 ssf = NanoHTTPD.makeSSLSocketFactory(keystore,
106 keyManagerFactory);
107 } finally {
108 keystoreStream.close();
109 }
110 } catch (Exception e) {
111 throw new IOException(e.getMessage());
112 }
113 }
114 }
115
116 server = new NanoHTTPD(port) {
117 @Override
118 public Response serve(final IHTTPSession session) {
119 super.serve(session);
120
121 String query = session.getQueryParameterString(); // a=a%20b&dd=2
122 Method method = session.getMethod(); // GET, POST..
123 String uri = session.getUri(); // /home.html
124
125 // need them in real time (not just those sent by the UA)
126 Map<String, String> cookies = new HashMap<String, String>();
127 for (String cookie : session.getCookies()) {
128 cookies.put(cookie, session.getCookies().read(cookie));
129 }
130
131 WLoginResult login = null;
132 Map<String, String> params = session.getParms();
133 String who = session.getRemoteHostName()
134 + session.getRemoteIpAddress();
135 if (params.get("login") != null) {
136 login = login(who, params.get("password"),
137 params.get("login"));
138 } else {
139 String cookie = cookies.get("cookie");
140 login = login(who, cookie);
141 }
142
143 if (login.isSuccess()) {
144 // refresh cookie
145 session.getCookies().set(new Cookie("cookie",
146 login.getCookie(), "30; path=/"));
147
148 // set options
149 String optionName = params.get("optionName");
150 if (optionName != null && !optionName.isEmpty()) {
151 String optionNo = params.get("optionNo");
152 String optionValue = params.get("optionValue");
153 if (optionNo != null || optionValue == null
154 || optionValue.isEmpty()) {
155 session.getCookies().delete(optionName);
156 cookies.remove(optionName);
157 } else {
158 session.getCookies().set(new Cookie(optionName,
159 optionValue, "; path=/"));
160 cookies.put(optionName, optionValue);
161 }
162 }
163 }
164
165 Response rep = null;
166 if (!login.isSuccess() && WebLibraryUrls.isSupportedUrl(uri)) {
167 rep = loginPage(login, uri);
168 }
169
170 if (rep == null) {
171 try {
172 if (WebLibraryUrls.isSupportedUrl(uri)) {
173 if (WebLibraryUrls.INDEX_URL.equals(uri)) {
174 rep = root(session, cookies, login);
175 } else if (WebLibraryUrls.VERSION_URL.equals(uri)) {
176 rep = newFixedLengthResponse(Status.OK,
177 MIME_PLAINTEXT,
178 Version.getCurrentVersion().toString());
179 } else if (WebLibraryUrls.isCoverUrl(uri)) {
180 String luid = params.get("luid");
181 if (luid != null) {
182 rep = setCover(uri, luid, login);
183 } else {
184 rep = getCover(uri, login);
185 }
186 } else if (WebLibraryUrls.isListUrl(uri)) {
187 rep = getList(uri, login);
188 } else if (WebLibraryUrls.isStoryUrl(uri)) {
189 String value = params.get("value");
190 if (value != null) {
191 rep = setStoryPart(uri, value, login);
192 } else {
193 rep = getStoryPart(uri, login);
194 }
195 } else if (WebLibraryUrls.isViewUrl(uri)) {
196 rep = getViewer(cookies, uri, login);
197 } else if (WebLibraryUrls.LOGOUT_URL.equals(uri)) {
198 session.getCookies().delete("cookie");
199 cookies.remove("cookie");
200 rep = loginPage(login(false, false), uri);
201 } else if (WebLibraryUrls.isImprtUrl(uri)) {
202 String url = params.get("url");
203 if (url != null) {
204 rep = imprt(uri, url, login);
205 } else {
206 rep = imprtProgress(uri, login);
207 }
208 } else {
209 getTraceHandler().error(
210 "Supported URL was not processed: "
211 + uri);
212 rep = newFixedLengthResponse(
213 Status.INTERNAL_ERROR,
214 NanoHTTPD.MIME_PLAINTEXT,
215 "An error happened");
216 }
217 } else {
218 if (uri.startsWith("/"))
219 uri = uri.substring(1);
220 InputStream in = IOUtils.openResource(
221 WebLibraryServerIndex.class, uri);
222 if (in != null) {
223 String mimeType = MIME_PLAINTEXT;
224 if (uri.endsWith(".css")) {
225 mimeType = "text/css";
226 } else if (uri.endsWith(".html")) {
227 mimeType = "text/html";
228 } else if (uri.endsWith(".js")) {
229 mimeType = "text/javascript";
230 }
231 rep = newChunkedResponse(Status.OK, mimeType,
232 in);
233 }
234
235 if (rep == null) {
236 getTraceHandler().trace("404: " + uri);
237 rep = newFixedLengthResponse(Status.NOT_FOUND,
238 NanoHTTPD.MIME_PLAINTEXT, "Not Found");
239 }
240 }
241 } catch (Exception e) {
242 Instance.getInstance().getTraceHandler().error(
243 new IOException("Cannot process web request",
244 e));
245 rep = newFixedLengthResponse(Status.INTERNAL_ERROR,
246 NanoHTTPD.MIME_PLAINTEXT, "An error occured");
247 }
248 }
249
250 return rep;
251 }
252 };
253
254 if (ssf != null)
255
256 {
257 getTraceHandler().trace("Install SSL on the web server...");
258 server.makeSecure(ssf, null);
259 getTraceHandler().trace("Done.");
260 }
261 }
262
263 @Override
264 public void run() {
265 try {
266 server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
267 } catch (IOException e) {
268 tracer.error(new IOException("Cannot start the web server", e));
269 }
270 }
271
272 /**
273 * The traces handler for this {@link WebLibraryServerHtml}.
274 *
275 * @return the traces handler
276 */
277 public TraceHandler getTraceHandler() {
278 return tracer;
279 }
280
281 /**
282 * The traces handler for this {@link WebLibraryServerHtml}.
283 *
284 * @param tracer
285 * the new traces handler
286 */
287 public void setTraceHandler(TraceHandler tracer) {
288 if (tracer == null) {
289 tracer = new TraceHandler(false, false, false);
290 }
291
292 this.tracer = tracer;
293 }
294
295 private Response loginPage(WLoginResult login, String uri) {
296 StringBuilder builder = new StringBuilder();
297
298 appendPreHtml(builder, true);
299
300 if (login.isBadLogin()) {
301 builder.append("<div class='error'>Bad login or password</div>");
302 } else if (login.isBadCookie()) {
303 builder.append("<div class='error'>Your session timed out</div>");
304 }
305
306 if (WebLibraryUrls.LOGOUT_URL.equals(uri)) {
307 uri = WebLibraryUrls.INDEX_URL;
308 }
309
310 builder.append(
311 "<form method='POST' action='" + uri + "' class='login'>\n");
312 builder.append(
313 "<p>You must be logged into the system to see the stories.</p>");
314 builder.append("\t<input type='text' name='login' />\n");
315 builder.append("\t<input type='password' name='password' />\n");
316 builder.append("\t<input type='submit' value='Login' />\n");
317 builder.append("</form>\n");
318
319 appendPostHtml(builder);
320
321 return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
322 NanoHTTPD.MIME_HTML, builder.toString());
323 }
324
325 private Response root(IHTTPSession session, Map<String, String> cookies,
326 WLoginResult login) throws IOException {
327 BasicLibrary lib = Instance.getInstance().getLibrary();
328 MetaResultList result = new MetaResultList(metas(login));
329 StringBuilder builder = new StringBuilder();
330
331 appendPreHtml(builder, true);
332
333 Map<String, String> params = session.getParms();
334
335 String filter = cookies.get("filter");
336 if (params.get("optionNo") != null)
337 filter = null;
338 if (filter == null) {
339 filter = "";
340 }
341
342 String browser = params.get("browser") == null ? ""
343 : params.get("browser");
344 String browser2 = params.get("browser2") == null ? ""
345 : params.get("browser2");
346 String browser3 = params.get("browser3") == null ? ""
347 : params.get("browser3");
348
349 String filterSource = null;
350 String filterAuthor = null;
351 String filterTag = null;
352
353 // TODO: javascript in realtime, using visible=false + hide [submit]
354
355 builder.append("<form class='browser'>\n");
356 builder.append("<div class='breadcrumbs'>\n");
357
358 builder.append("\t<select name='browser'>");
359 appendOption(builder, 2, "", "", browser);
360 appendOption(builder, 2, "Sources", "sources", browser);
361 appendOption(builder, 2, "Authors", "authors", browser);
362 appendOption(builder, 2, "Tags", "tags", browser);
363 builder.append("\t</select>\n");
364
365 if (!browser.isEmpty()) {
366 builder.append("\t<select name='browser2'>");
367 if (browser.equals("sources")) {
368 filterSource = browser2.isEmpty() ? filterSource : browser2;
369 // TODO: if 1 group -> no group
370 appendOption(builder, 2, "", "", browser2);
371 Map<String, List<String>> sources = result.getSourcesGrouped();
372 for (String source : sources.keySet()) {
373 appendOption(builder, 2, source, source, browser2);
374 }
375 } else if (browser.equals("authors")) {
376 filterAuthor = browser2.isEmpty() ? filterAuthor : browser2;
377 // TODO: if 1 group -> no group
378 appendOption(builder, 2, "", "", browser2);
379 Map<String, List<String>> authors = result.getAuthorsGrouped();
380 for (String author : authors.keySet()) {
381 appendOption(builder, 2, author, author, browser2);
382 }
383 } else if (browser.equals("tags")) {
384 filterTag = browser2.isEmpty() ? filterTag : browser2;
385 appendOption(builder, 2, "", "", browser2);
386 for (String tag : result.getTags()) {
387 appendOption(builder, 2, tag, tag, browser2);
388 }
389 }
390 builder.append("\t</select>\n");
391 }
392
393 if (!browser2.isEmpty()) {
394 if (browser.equals("sources")) {
395 filterSource = browser3.isEmpty() ? filterSource : browser3;
396 Map<String, List<String>> sourcesGrouped = result
397 .getSourcesGrouped();
398 List<String> sources = sourcesGrouped.get(browser2);
399 if (sources != null && !sources.isEmpty()) {
400 // TODO: single empty value
401 builder.append("\t<select name='browser3'>");
402 appendOption(builder, 2, "", "", browser3);
403 for (String source : sources) {
404 appendOption(builder, 2, source, source, browser3);
405 }
406 builder.append("\t</select>\n");
407 }
408 } else if (browser.equals("authors")) {
409 filterAuthor = browser3.isEmpty() ? filterAuthor : browser3;
410 Map<String, List<String>> authorsGrouped = result
411 .getAuthorsGrouped();
412 List<String> authors = authorsGrouped.get(browser2);
413 if (authors != null && !authors.isEmpty()) {
414 // TODO: single empty value
415 builder.append("\t<select name='browser3'>");
416 appendOption(builder, 2, "", "", browser3);
417 for (String author : authors) {
418 appendOption(builder, 2, author, author, browser3);
419 }
420 builder.append("\t</select>\n");
421 }
422 }
423 }
424
425 builder.append("\t<input type='submit' value='Select'/>\n");
426 builder.append("</div>\n");
427
428 // TODO: javascript in realtime, using visible=false + hide [submit]
429 builder.append("<div class='filter'>\n");
430 builder.append("\t<span class='label'>Filter: </span>\n");
431 builder.append(
432 "\t<input name='optionName' type='hidden' value='filter' />\n");
433 builder.append("\t<input name='optionValue' type='text' value='"
434 + filter + "' place-holder='...' />\n");
435 builder.append("\t<input name='optionNo' type='submit' value='x' />");
436 builder.append(
437 "\t<input name='submit' type='submit' value='Filter' />\n");
438 builder.append("</div>\n");
439 builder.append("</form>\n");
440
441 builder.append("\t<div class='books'>");
442 for (MetaData meta : result.getMetas()) {
443 if (!filter.isEmpty() && !meta.getTitle().toLowerCase()
444 .contains(filter.toLowerCase())) {
445 continue;
446 }
447
448 // TODO Sub sources
449 if (filterSource != null
450 && !filterSource.equals(meta.getSource())) {
451 continue;
452 }
453
454 // TODO: sub authors
455 if (filterAuthor != null
456 && !filterAuthor.equals(meta.getAuthor())) {
457 continue;
458 }
459
460 if (filterTag != null && !meta.getTags().contains(filterTag)) {
461 continue;
462 }
463
464 builder.append("<div class='book_line'>");
465 builder.append("<a href='");
466 builder.append(
467 WebLibraryUrls.getViewUrl(meta.getLuid(), null, null));
468 builder.append("'");
469 builder.append(" class='link'>");
470
471 if (lib.isCached(meta.getLuid())) {
472 // â—‰ = &#9673;
473 builder.append(
474 "<span class='cache_icon cached'>&#9673;</span>");
475 } else {
476 // â—‹ = &#9675;
477 builder.append(
478 "<span class='cache_icon uncached'>&#9675;</span>");
479 }
480 builder.append("<span class='luid'>");
481 builder.append(meta.getLuid());
482 builder.append("</span>");
483 builder.append("<span class='title'>");
484 builder.append(meta.getTitle());
485 builder.append("</span>");
486 builder.append("<span class='author'>");
487 if (meta.getAuthor() != null && !meta.getAuthor().isEmpty()) {
488 builder.append("(").append(meta.getAuthor()).append(")");
489 }
490 builder.append("</span>");
491 builder.append("</a></div>\n");
492 }
493 builder.append("</div>");
494
495 appendPostHtml(builder);
496 return NanoHTTPD.newFixedLengthResponse(builder.toString());
497 }
498
499 private Response getViewer(Map<String, String> cookies, String uri,
500 WLoginResult login) {
501 String[] cover = uri.split("/");
502 int off = 2;
503
504 if (cover.length < off + 2) {
505 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
506 NanoHTTPD.MIME_PLAINTEXT, null);
507 }
508
509 String type = cover[off + 0];
510 String luid = cover[off + 1];
511 String chapterStr = cover.length < off + 3 ? null : cover[off + 2];
512 String paragraphStr = cover.length < off + 4 ? null : cover[off + 3];
513
514 // 1-based (0 = desc)
515 int chapter = 0;
516 if (chapterStr != null) {
517 try {
518 chapter = Integer.parseInt(chapterStr);
519 if (chapter < 0) {
520 throw new NumberFormatException();
521 }
522 } catch (NumberFormatException e) {
523 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
524 NanoHTTPD.MIME_PLAINTEXT, "Chapter is not valid");
525 }
526 }
527
528 // 1-based
529 int paragraph = 0;
530 if (paragraphStr != null) {
531 try {
532 paragraph = Integer.parseInt(paragraphStr);
533 if (paragraph <= 0) {
534 throw new NumberFormatException();
535 }
536 } catch (NumberFormatException e) {
537 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
538 NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid");
539 }
540 }
541
542 try {
543 Story story = story(luid, login);
544 if (story == null) {
545 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
546 NanoHTTPD.MIME_PLAINTEXT, "Story not found");
547 }
548
549 StringBuilder builder = new StringBuilder();
550 appendPreHtml(builder, false);
551
552 // For images documents, always go to the images if not chap 0 desc
553 if (story.getMeta().isImageDocument()) {
554 if (chapter > 0 && paragraph <= 0)
555 paragraph = 1;
556 }
557
558 Chapter chap = null;
559 if (chapter <= 0) {
560 chap = story.getMeta().getResume();
561 } else {
562 try {
563 chap = story.getChapters().get(chapter - 1);
564 } catch (IndexOutOfBoundsException e) {
565 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
566 NanoHTTPD.MIME_PLAINTEXT, "Chapter not found");
567 }
568 }
569
570 String first, previous, next, last;
571
572 StringBuilder content = new StringBuilder();
573
574 String disabledLeft = "";
575 String disabledRight = "";
576 String disabledZoomReal = "";
577 String disabledZoomWidth = "";
578 String disabledZoomHeight = "";
579
580 if (paragraph <= 0) {
581 first = WebLibraryUrls.getViewUrl(luid, 0, null);
582 previous = WebLibraryUrls.getViewUrl(luid,
583 (Math.max(chapter - 1, 0)), null);
584 next = WebLibraryUrls.getViewUrl(luid,
585 (Math.min(chapter + 1, story.getChapters().size())),
586 null);
587 last = WebLibraryUrls.getViewUrl(luid,
588 story.getChapters().size(), null);
589
590 StringBuilder desc = new StringBuilder();
591
592 if (chapter <= 0) {
593 desc.append("<h1 class='title'>");
594 desc.append(story.getMeta().getTitle());
595 desc.append("</h1>\n");
596 desc.append("<div class='desc'>\n");
597 desc.append("\t<a href='" + next + "' class='cover'>\n");
598 desc.append("\t\t<img src='/story/" + luid + "/cover'/>\n");
599 desc.append("\t</a>\n");
600 desc.append("\t<table class='details'>\n");
601 Map<String, String> details = BasicLibrary
602 .getMetaDesc(story.getMeta());
603 for (String key : details.keySet()) {
604 appendTableRow(desc, 2, key, details.get(key));
605 }
606 desc.append("\t</table>\n");
607 desc.append("</div>\n");
608 desc.append("<h1 class='title'>Description</h1>\n");
609 }
610
611 content.append("<div class='viewer text'>\n");
612 content.append(desc);
613 String description = new TextOutput(false).convert(chap,
614 chapter > 0);
615 content.append(chap.getParagraphs().size() <= 0
616 ? "No content provided."
617 : description);
618 content.append("</div>\n");
619
620 if (chapter <= 0)
621 disabledLeft = " disabled='disbaled'";
622 if (chapter >= story.getChapters().size())
623 disabledRight = " disabled='disbaled'";
624 } else {
625 first = WebLibraryUrls.getViewUrl(luid, chapter, 1);
626 previous = WebLibraryUrls.getViewUrl(luid, chapter,
627 (Math.max(paragraph - 1, 1)));
628 next = WebLibraryUrls.getViewUrl(luid, chapter,
629 (Math.min(paragraph + 1, chap.getParagraphs().size())));
630 last = WebLibraryUrls.getViewUrl(luid, chapter,
631 chap.getParagraphs().size());
632
633 if (paragraph <= 1)
634 disabledLeft = " disabled='disbaled'";
635 if (paragraph >= chap.getParagraphs().size())
636 disabledRight = " disabled='disbaled'";
637
638 // First -> previous *chapter*
639 if (chapter > 0)
640 disabledLeft = "";
641 first = WebLibraryUrls.getViewUrl(luid,
642 (Math.max(chapter - 1, 0)), null);
643 if (paragraph <= 1) {
644 previous = first;
645 }
646
647 Paragraph para = null;
648 try {
649 para = chap.getParagraphs().get(paragraph - 1);
650 } catch (IndexOutOfBoundsException e) {
651 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
652 NanoHTTPD.MIME_PLAINTEXT,
653 "Paragraph " + paragraph + " not found");
654 }
655
656 if (para.getType() == ParagraphType.IMAGE) {
657 String zoomStyle = "max-width: 100%;";
658 disabledZoomWidth = " disabled='disabled'";
659 String zoomOption = cookies.get("zoom");
660 if (zoomOption != null && !zoomOption.isEmpty()) {
661 if (zoomOption.equals("real")) {
662 zoomStyle = "";
663 disabledZoomWidth = "";
664 disabledZoomReal = " disabled='disabled'";
665 } else if (zoomOption.equals("width")) {
666 zoomStyle = "max-width: 100%;";
667 } else if (zoomOption.equals("height")) {
668 // see height of navbar + optionbar
669 zoomStyle = "max-height: calc(100% - 128px);";
670 disabledZoomWidth = "";
671 disabledZoomHeight = " disabled='disabled'";
672 }
673 }
674
675 String javascript = "document.getElementById(\"previous\").click(); return false;";
676 content.append(String.format("" //
677 + "<a class='viewer link' oncontextmenu='%s' href='%s'>"
678 + "<img class='viewer img' style='%s' src='%s'/>"
679 + "</a>", //
680 javascript, //
681 next, //
682 zoomStyle, //
683 WebLibraryUrls.getStoryUrl(luid, chapter,
684 paragraph)));
685 } else {
686 content.append(String.format("" //
687 + "<div class='viewer text'>%s</div>", //
688 para.getContent()));
689 }
690 }
691
692 builder.append(String.format("" //
693 + "<div class='bar navbar'>\n" //
694 + "\t<a%s class='button first' href='%s'>&lt;&lt;</a>\n"//
695 + "\t<a%s id='previous' class='button previous' href='%s'>&lt;</a>\n" //
696 + "\t<div class='gotobox itemsbox'>\n" //
697 + "\t\t<div class='button goto'>%d</div>\n" //
698 + "\t\t<div class='items goto'>\n", //
699 disabledLeft, first, //
700 disabledLeft, previous, //
701 paragraph > 0 ? paragraph : chapter //
702 ));
703
704 // List of chap/para links
705
706 appendItemA(builder, 3, WebLibraryUrls.getViewUrl(luid, 0, null),
707 "Description", paragraph == 0 && chapter == 0);
708 if (paragraph > 0) {
709 for (int i = 1; i <= chap.getParagraphs().size(); i++) {
710 appendItemA(builder, 3,
711 WebLibraryUrls.getViewUrl(luid, chapter, i),
712 "Image " + i, paragraph == i);
713 }
714 } else {
715 int i = 1;
716 for (Chapter c : story.getChapters()) {
717 String chapName = "Chapter " + c.getNumber();
718 if (c.getName() != null && !c.getName().isEmpty()) {
719 chapName += ": " + c.getName();
720 }
721
722 appendItemA(builder, 3,
723 WebLibraryUrls.getViewUrl(luid, i, null), chapName,
724 chapter == i);
725
726 i++;
727 }
728 }
729
730 builder.append(String.format("" //
731 + "\t\t</div>\n" //
732 + "\t</div>\n" //
733 + "\t<a%s class='button next' href='%s'>&gt;</a>\n" //
734 + "\t<a%s class='button last' href='%s'>&gt;&gt;</a>\n"//
735 + "</div>\n", //
736 disabledRight, next, //
737 disabledRight, last //
738 ));
739
740 builder.append(content);
741
742 builder.append("<div class='bar optionbar ");
743 if (paragraph > 0) {
744 builder.append("s4");
745 } else {
746 builder.append("s1");
747 }
748 builder.append("'>\n");
749 builder.append(" <a class='button back' href='/'>BACK</a>\n");
750
751 if (paragraph > 0) {
752 builder.append(String.format("" //
753 + "\t<a%s class='button zoomreal' href='%s'>REAL</a>\n"//
754 + "\t<a%s class='button zoomwidth' href='%s'>WIDTH</a>\n"//
755 + "\t<a%s class='button zoomheight' href='%s'>HEIGHT</a>\n"//
756 + "</div>\n", //
757 disabledZoomReal,
758 uri + "?optionName=zoom&optionValue=real", //
759 disabledZoomWidth,
760 uri + "?optionName=zoom&optionValue=width", //
761 disabledZoomHeight,
762 uri + "?optionName=zoom&optionValue=height" //
763 ));
764 }
765
766 appendPostHtml(builder);
767 return NanoHTTPD.newFixedLengthResponse(Status.OK,
768 NanoHTTPD.MIME_HTML, builder.toString());
769 } catch (IOException e) {
770 Instance.getInstance().getTraceHandler()
771 .error(new IOException("Cannot get image: " + uri, e));
772 return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR,
773 NanoHTTPD.MIME_PLAINTEXT, "Error when processing request");
774 }
775 }
776
777 protected Response newInputStreamResponse(String mimeType, InputStream in) {
778 if (in == null) {
779 return NanoHTTPD.newFixedLengthResponse(Status.NO_CONTENT, "",
780 null);
781 }
782 return NanoHTTPD.newChunkedResponse(Status.OK, mimeType, in);
783 }
784
785 private String getContentOf(String file) {
786 InputStream in = IOUtils.openResource(WebLibraryServerIndex.class,
787 file);
788 if (in != null) {
789 try {
790 return IOUtils.readSmallStream(in);
791 } catch (IOException e) {
792 Instance.getInstance().getTraceHandler().error(
793 new IOException("Cannot get file: index.pre.html", e));
794 }
795 }
796
797 return "";
798 }
799
800 private void appendPreHtml(StringBuilder builder, boolean banner) {
801 String favicon = "favicon.ico";
802 String icon = Instance.getInstance().getUiConfig()
803 .getString(UiConfig.PROGRAM_ICON);
804 if (icon != null) {
805 favicon = "icon_" + icon.replace("-", "_") + ".png";
806 }
807
808 builder.append(
809 getContentOf("index.pre.html").replace("favicon.ico", favicon));
810
811 if (banner) {
812 builder.append("<div class='banner'>\n");
813 builder.append("\t<img class='ico' src='/") //
814 .append(favicon) //
815 .append("'/>\n");
816 builder.append("\t<h1>Fanfix</h1>\n");
817 builder.append("\t<h2>") //
818 .append(Version.getCurrentVersion()) //
819 .append("</h2>\n");
820 builder.append("</div>\n");
821 }
822 }
823
824 private void appendPostHtml(StringBuilder builder) {
825 builder.append(getContentOf("index.post.html"));
826 }
827
828 private void appendOption(StringBuilder builder, int depth, String name,
829 String value, String selected) {
830 for (int i = 0; i < depth; i++) {
831 builder.append("\t");
832 }
833 builder.append("<option value='").append(value).append("'");
834 if (value.equals(selected)) {
835 builder.append(" selected='selected'");
836 }
837 builder.append(">").append(name).append("</option>\n");
838 }
839
840 private void appendTableRow(StringBuilder builder, int depth,
841 String... tds) {
842 for (int i = 0; i < depth; i++) {
843 builder.append("\t");
844 }
845
846 int col = 1;
847 builder.append("<tr>");
848 for (String td : tds) {
849 builder.append("<td class='col");
850 builder.append(col++);
851 builder.append("'>");
852 builder.append(td);
853 builder.append("</td>");
854 }
855 builder.append("</tr>\n");
856 }
857
858 private void appendItemA(StringBuilder builder, int depth, String link,
859 String name, boolean selected) {
860 for (int i = 0; i < depth; i++) {
861 builder.append("\t");
862 }
863
864 builder.append("<a href='");
865 builder.append(link);
866 builder.append("' class='item goto");
867 if (selected) {
868 builder.append(" selected");
869 }
870 builder.append("'>");
871 builder.append(name);
872 builder.append("</a>\n");
873 }
874 }