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