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