42ebead137c05e8189fec17a9d57d4da91e4858a
[nikiroo-utils.git] / 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 List<Template> content = new ArrayList<Template>();
320
321 if (login.isBadLogin()) {
322 content.add(templates.message("Bad login or password", true));
323 } else if (login.isBadCookie()) {
324 content.add(templates.message("Your session timed out", true));
325 }
326
327 content.add(templates.login(uri));
328
329 return NanoHTTPD.newChunkedResponse(Status.FORBIDDEN,
330 NanoHTTPD.MIME_HTML, templates.index(true, content).read());
331 }
332
333 private Response root(IHTTPSession session, Map<String, String> cookies,
334 WLoginResult login) throws IOException {
335 BasicLibrary lib = Instance.getInstance().getLibrary();
336 MetaResultList result = new MetaResultList(metas(login));
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 List<Template> selects = new ArrayList<Template>();
361 boolean sourcesSel = false;
362 boolean authorsSel = false;
363 boolean tagsSel = false;
364
365 if (!browser.isEmpty()) {
366 List<Template> options = new ArrayList<Template>();
367
368 if (browser.equals("sources")) {
369 sourcesSel = true;
370 filterSource = browser2.isEmpty() ? filterSource : browser2;
371
372 // TODO: if 1 group -> no group
373 Map<String, List<String>> sources = result.getSourcesGrouped();
374 for (String source : sources.keySet()) {
375 options.add(
376 templates.browserOption(source, source, browser2));
377 }
378 } else if (browser.equals("authors")) {
379 authorsSel = true;
380 filterAuthor = browser2.isEmpty() ? filterAuthor : browser2;
381
382 // TODO: if 1 group -> no group
383 Map<String, List<String>> authors = result.getAuthorsGrouped();
384 for (String author : authors.keySet()) {
385 options.add(
386 templates.browserOption(author, author, browser2));
387 }
388 } else if (browser.equals("tags")) {
389 tagsSel = true;
390 filterTag = browser2.isEmpty() ? filterTag : browser2;
391
392 for (String tag : result.getTags()) {
393 options.add(templates.browserOption(tag, tag, browser2));
394 }
395 }
396
397 selects.add(templates.browserSelect("browser2", browser2, options));
398 }
399
400 if (!browser2.isEmpty()) {
401 List<Template> options = new ArrayList<Template>();
402
403 if (browser.equals("sources")) {
404 filterSource = browser3.isEmpty() ? filterSource : browser3;
405 Map<String, List<String>> sourcesGrouped = result
406 .getSourcesGrouped();
407 List<String> sources = sourcesGrouped.get(browser2);
408 if (sources != null && !sources.isEmpty()) {
409 // TODO: single empty value
410 for (String source : sources) {
411 options.add(templates.browserOption(source, source,
412 browser3));
413 }
414 }
415 } else if (browser.equals("authors")) {
416 filterAuthor = browser3.isEmpty() ? filterAuthor : browser3;
417 Map<String, List<String>> authorsGrouped = result
418 .getAuthorsGrouped();
419 List<String> authors = authorsGrouped.get(browser2);
420 if (authors != null && !authors.isEmpty()) {
421 // TODO: single empty value
422 for (String author : authors) {
423 options.add(templates.browserOption(author, author,
424 browser3));
425 }
426 }
427 }
428
429 selects.add(templates.browserSelect("browser3", browser3, options));
430 }
431
432 List<Template> booklines = new ArrayList<Template>();
433 for (MetaData meta : result.getMetas()) {
434 if (!filter.isEmpty() && !meta.getTitle().toLowerCase()
435 .contains(filter.toLowerCase())) {
436 continue;
437 }
438
439 // TODO Sub sources
440 if (filterSource != null
441 && !filterSource.equals(meta.getSource())) {
442 continue;
443 }
444
445 // TODO: sub authors
446 if (filterAuthor != null
447 && !filterAuthor.equals(meta.getAuthor())) {
448 continue;
449 }
450
451 if (filterTag != null && !meta.getTags().contains(filterTag)) {
452 continue;
453 }
454
455 String author = "";
456 if (meta.getAuthor() != null && !meta.getAuthor().isEmpty()) {
457 author = "(" + meta.getAuthor() + ")";
458 }
459
460 booklines.add(templates.bookline( //
461 meta.getLuid(), //
462 WebLibraryUrls.getViewUrl(meta.getLuid(), null, null), //
463 meta.getTitle(), //
464 author, //
465 lib.isCached(meta.getLuid()) //
466 ));
467 }
468
469 // Add the browser in front of the booklines
470 booklines.add(0, templates.browser(browser, filter, selects));
471
472 return newInputStreamResponse(NanoHTTPD.MIME_HTML,
473 templates.index(true, booklines).read());
474 }
475
476 private Response getViewer(Map<String, String> cookies, String uri,
477 WLoginResult login) {
478 String[] cover = uri.split("/");
479 int off = 2;
480
481 if (cover.length < off + 2) {
482 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
483 NanoHTTPD.MIME_PLAINTEXT, null);
484 }
485
486 String type = cover[off + 0];
487 String luid = cover[off + 1];
488 String chapterStr = cover.length < off + 3 ? null : cover[off + 2];
489 String paragraphStr = cover.length < off + 4 ? null : cover[off + 3];
490
491 // 1-based (0 = desc)
492 int chapter = 0;
493 if (chapterStr != null) {
494 try {
495 chapter = Integer.parseInt(chapterStr);
496 if (chapter < 0) {
497 throw new NumberFormatException();
498 }
499 } catch (NumberFormatException e) {
500 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
501 NanoHTTPD.MIME_PLAINTEXT, "Chapter is not valid");
502 }
503 }
504
505 // 1-based
506 int paragraph = 0;
507 if (paragraphStr != null) {
508 try {
509 paragraph = Integer.parseInt(paragraphStr);
510 if (paragraph <= 0) {
511 throw new NumberFormatException();
512 }
513 } catch (NumberFormatException e) {
514 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
515 NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid");
516 }
517 }
518
519 try {
520 Story story = story(luid, login);
521 if (story == null) {
522 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
523 NanoHTTPD.MIME_PLAINTEXT, "Story not found");
524 }
525
526 // For images documents, always go to the images if not chap 0 desc
527 if (story.getMeta().isImageDocument()) {
528 if (chapter > 0 && paragraph <= 0)
529 paragraph = 1;
530 }
531
532 Chapter chap = null;
533 if (chapter <= 0) {
534 chap = story.getMeta().getResume();
535 } else {
536 try {
537 chap = story.getChapters().get(chapter - 1);
538 } catch (IndexOutOfBoundsException e) {
539 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
540 NanoHTTPD.MIME_PLAINTEXT, "Chapter not found");
541 }
542 }
543
544 String first, previous, next, last;
545
546 String viewer = "";
547
548 String disabledLeft = "";
549 String disabledRight = "";
550 String disabledZoomReal = "";
551 String disabledZoomWidth = "";
552 String disabledZoomHeight = "";
553
554 if (paragraph <= 0) {
555 first = WebLibraryUrls.getViewUrl(luid, 0, null);
556 previous = WebLibraryUrls.getViewUrl(luid,
557 (Math.max(chapter - 1, 0)), null);
558 next = WebLibraryUrls.getViewUrl(luid,
559 (Math.min(chapter + 1, story.getChapters().size())),
560 null);
561 last = WebLibraryUrls.getViewUrl(luid,
562 story.getChapters().size(), null);
563
564 String desc = "";
565 if (chapter <= 0) {
566 StringBuilder desclines = new StringBuilder();
567 Map<String, String> details = BasicLibrary
568 .getMetaDesc(story.getMeta());
569 for (String key : details.keySet()) {
570 desclines.append(getTemplate("viewer.descline") //
571 .replace("${key}", key) //
572 .replace("${value}", details.get(key)) //
573 );
574 }
575
576 desc = getTemplate("viewer.desc") //
577 .replace("${title}", story.getMeta().getTitle()) //
578 .replace("${href}", next) //
579 .replace("${cover}",
580 WebLibraryUrls.getStoryUrlCover(luid)) //
581 .replace("${details}", desclines.toString()) //
582 ;
583 }
584
585 viewer = getTemplate("viewer.text") //
586 .replace("${desc}", desc) //
587 ;
588 if (chap.getParagraphs().size() <= 0) {
589 viewer = viewer.replace("${content}",
590 "No content provided.");
591 } else {
592 viewer = viewer.replace("${content}",
593 new TextOutput(false).convert(chap, chapter > 0));
594 }
595
596 if (chapter <= 0)
597 disabledLeft = " disabled='disbaled'";
598 if (chapter >= story.getChapters().size())
599 disabledRight = " disabled='disbaled'";
600 } else {
601 first = WebLibraryUrls.getViewUrl(luid, chapter, 1);
602 previous = WebLibraryUrls.getViewUrl(luid, chapter,
603 (Math.max(paragraph - 1, 1)));
604 next = WebLibraryUrls.getViewUrl(luid, chapter,
605 (Math.min(paragraph + 1, chap.getParagraphs().size())));
606 last = WebLibraryUrls.getViewUrl(luid, chapter,
607 chap.getParagraphs().size());
608
609 if (paragraph <= 1)
610 disabledLeft = " disabled='disbaled'";
611 if (paragraph >= chap.getParagraphs().size())
612 disabledRight = " disabled='disbaled'";
613
614 // First -> previous *chapter*
615 if (chapter > 0)
616 disabledLeft = "";
617 first = WebLibraryUrls.getViewUrl(luid,
618 (Math.max(chapter - 1, 0)), null);
619 if (paragraph <= 1) {
620 previous = first;
621 }
622
623 Paragraph para = null;
624 try {
625 para = chap.getParagraphs().get(paragraph - 1);
626 } catch (IndexOutOfBoundsException e) {
627 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
628 NanoHTTPD.MIME_PLAINTEXT,
629 "Paragraph " + paragraph + " not found");
630 }
631
632 if (para.getType() == ParagraphType.IMAGE) {
633 String zoomStyle = "max-width: 100%;";
634 disabledZoomWidth = " disabled='disabled'";
635 String zoomOption = cookies.get("zoom");
636 if (zoomOption != null && !zoomOption.isEmpty()) {
637 if (zoomOption.equals("real")) {
638 zoomStyle = "";
639 disabledZoomWidth = "";
640 disabledZoomReal = " disabled='disabled'";
641 } else if (zoomOption.equals("width")) {
642 zoomStyle = "max-width: 100%;";
643 } else if (zoomOption.equals("height")) {
644 // see height of navbar + optionbar
645 zoomStyle = "max-height: calc(100% - 128px);";
646 disabledZoomWidth = "";
647 disabledZoomHeight = " disabled='disabled'";
648 }
649 }
650
651 viewer = getTemplate("viewer.image") //
652 .replace("${href}", next) //
653 .replace("${zoomStyle}", zoomStyle) //
654 .replace("${src}", WebLibraryUrls.getStoryUrl(luid,
655 chapter, paragraph)) //
656 ;
657 } else {
658 viewer = getTemplate("viewer.text") //
659 .replace("${desc}", "") //
660 .replace("${content}",
661 new TextOutput(false).convert(para)) //
662 ;
663 }
664 }
665
666 // List of chap/para links
667 StringBuilder links = new StringBuilder();
668 links.append(getTemplate("viewer.link") //
669 .replace("${link}",
670 WebLibraryUrls.getViewUrl(luid, 0, null)) //
671 .replace("${class}",
672 paragraph == 0 && chapter == 0 ? "selected" : "") //
673 .replace("${name}", "Description") //
674 );
675 if (paragraph > 0) {
676 for (int i = 1; i <= chap.getParagraphs().size(); i++) {
677 links.append(getTemplate("viewer.link") //
678 .replace("${link}",
679 WebLibraryUrls.getViewUrl(luid, chapter, i)) //
680 .replace("${class}",
681 paragraph == i ? "selected" : "") //
682 .replace("${name}", "Image " + i) //
683 );
684 }
685 } else {
686 int i = 1;
687 for (Chapter c : story.getChapters()) {
688 String chapName = "Chapter " + c.getNumber();
689 if (c.getName() != null && !c.getName().isEmpty()) {
690 chapName += ": " + c.getName();
691 }
692
693 links.append(getTemplate("viewer.link") //
694 .replace("${link}",
695 WebLibraryUrls.getViewUrl(luid, i, null)) //
696 .replace("${class}", chapter == i ? "selected" : "") //
697 .replace("${name}", chapName) //
698 );
699
700 i++;
701 }
702 }
703
704 // Buttons on the optionbar
705
706 StringBuilder buttons = new StringBuilder();
707 buttons.append(getTemplate("viewer.optionbar.button") //
708 .replace("${disabled}", "") //
709 .replace("${class}", "back") //
710 .replace("${href}", "/") //
711 .replace("${value}", "Back") //
712 );
713 if (paragraph > 0) {
714 buttons.append(getTemplate("viewer.optionbar.button") //
715 .replace("${disabled}", disabledZoomReal) //
716 .replace("${class}", "zoomreal") //
717 .replace("${href}",
718 uri + "?optionName=zoom&optionValue=real") //
719 .replace("${value}", "1:1") //
720 );
721 buttons.append(getTemplate("viewer.optionbar.button") //
722 .replace("${disabled}", disabledZoomWidth) //
723 .replace("${class}", "zoomwidth") //
724 .replace("${href}",
725 uri + "?optionName=zoom&optionValue=width") //
726 .replace("${value}", "Width") //
727 );
728 buttons.append(getTemplate("viewer.optionbar.button") //
729 .replace("${disabled}", disabledZoomHeight) //
730 .replace("${class}", "zoomheight") //
731 .replace("${href}",
732 uri + "?optionName=zoom&optionValue=height") //
733 .replace("${value}", "Height") //
734 );
735 }
736
737 // Full content
738
739 StringBuilder builder = new StringBuilder();
740
741 builder.append(getTemplateIndexPreBanner(false));
742
743 builder.append(getTemplate("viewer.navbar") //
744 .replace("${disabledFirst}", disabledLeft) //
745 .replace("${disabledPrevious}", disabledLeft) //
746 .replace("${disabledNext}", disabledRight) //
747 .replace("${disabledLast}", disabledRight) //
748 .replace("${hrefFirst}", first) //
749 .replace("${hrefPrevious}", previous) //
750 .replace("${hrefNext}", next) //
751 .replace("${hrefLast}", last) //
752 .replace("${current}",
753 "" + (paragraph > 0 ? paragraph : chapter)) //
754 .replace("${links}", links.toString()) //
755 );
756
757 builder.append(viewer);
758
759 builder.append(getTemplate("viewer.optionbar") //
760 .replace("${classSize}", (paragraph > 0 ? "s4" : "s1")) //
761 .replace("${buttons}", buttons.toString()) //
762 );
763
764 builder.append(getTemplate("index.post"));
765
766 return NanoHTTPD.newFixedLengthResponse(Status.OK,
767 NanoHTTPD.MIME_HTML, builder.toString());
768 } catch (IOException e) {
769 Instance.getInstance().getTraceHandler()
770 .error(new IOException("Cannot get image: " + uri, e));
771 return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR,
772 NanoHTTPD.MIME_PLAINTEXT, "Error when processing request");
773 }
774 }
775
776 protected Response newInputStreamResponse(String mimeType, InputStream in) {
777 if (in == null) {
778 return NanoHTTPD.newFixedLengthResponse(Status.NO_CONTENT, "",
779 null);
780 }
781 return NanoHTTPD.newChunkedResponse(Status.OK, mimeType, in);
782 }
783
784 private String getTemplateIndexPreBanner(boolean banner)
785 throws IOException {
786 String favicon = "favicon.ico";
787 String icon = Instance.getInstance().getUiConfig()
788 .getString(UiConfig.PROGRAM_ICON);
789 if (icon != null) {
790 favicon = "icon_" + icon.replace("-", "_") + ".png";
791 }
792
793 String html = getTemplate("index.pre") //
794 .replace("${title}", "Fanfix") //
795 .replace("${favicon}", favicon) //
796 ;
797
798 if (banner) {
799 html += getTemplate("index.banner") //
800 .replace("${favicon}", favicon) //
801 .replace("${version}",
802 Version.getCurrentVersion().toString()) //
803 ;
804 }
805
806 return html;
807 }
808
809 private String getTemplate(String template) throws IOException {
810 // TODO: check if it is "slow" -> map cache
811 InputStream in = IOUtils.openResource(WebLibraryServerTemplates.class,
812 template + ".html");
813 try {
814 return IOUtils.readSmallStream(in);
815 } finally {
816 in.close();
817 }
818 }
819 }