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