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