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