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