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