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