weblib: use some templates
[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;
44f134a5
NR
176 if (!login.isSuccess()
177 && WebLibraryUrls.isSupportedUrl(uri, true)) {
33d40b9f
NR
178 rep = loginPage(login, uri);
179 }
180
181 if (rep == null) {
182 try {
44f134a5 183 if (WebLibraryUrls.isSupportedUrl(uri, false)) {
33d40b9f
NR
184 if (WebLibraryUrls.INDEX_URL.equals(uri)) {
185 rep = root(session, cookies, login);
186 } else if (WebLibraryUrls.VERSION_URL.equals(uri)) {
187 rep = newFixedLengthResponse(Status.OK,
188 MIME_PLAINTEXT,
189 Version.getCurrentVersion().toString());
6673ec59 190 } else if (WebLibraryUrls.isCoverUrl(uri)) {
e4b1b70c
NR
191 String luid = params.get("luid");
192 if (luid != null) {
193 rep = setCover(uri, luid, login);
194 } else {
195 rep = getCover(uri, login);
196 }
33d40b9f
NR
197 } else if (WebLibraryUrls.isListUrl(uri)) {
198 rep = getList(uri, login);
199 } else if (WebLibraryUrls.isStoryUrl(uri)) {
089e354e
NR
200 String value = params.get("value");
201 if (value != null) {
202 rep = setStoryPart(uri, value, login);
203 } else {
204 rep = getStoryPart(uri, login);
205 }
33d40b9f
NR
206 } else if (WebLibraryUrls.isViewUrl(uri)) {
207 rep = getViewer(cookies, uri, login);
208 } else if (WebLibraryUrls.LOGOUT_URL.equals(uri)) {
209 session.getCookies().delete("cookie");
210 cookies.remove("cookie");
211 rep = loginPage(login(false, false), uri);
f70bcacf
NR
212 } else if (WebLibraryUrls.isImprtUrl(uri)) {
213 String url = params.get("url");
214 if (url != null) {
215 rep = imprt(uri, url, login);
216 } else {
217 rep = imprtProgress(uri, login);
218 }
a1226ce0
NR
219 } else if (WebLibraryUrls.isDeleteUrl(uri)) {
220 rep = delete(uri, login);
09c2396e
NR
221 } else if (WebLibraryUrls.EXIT_URL.equals(uri)) {
222 rep = WebLibraryServerHtml.this.stop(login);
33d40b9f
NR
223 } else {
224 getTraceHandler().error(
225 "Supported URL was not processed: "
226 + uri);
227 rep = newFixedLengthResponse(
228 Status.INTERNAL_ERROR,
229 NanoHTTPD.MIME_PLAINTEXT,
230 "An error happened");
231 }
232 } else {
233 if (uri.startsWith("/"))
234 uri = uri.substring(1);
235 InputStream in = IOUtils.openResource(
236 WebLibraryServerIndex.class, uri);
237 if (in != null) {
238 String mimeType = MIME_PLAINTEXT;
239 if (uri.endsWith(".css")) {
240 mimeType = "text/css";
241 } else if (uri.endsWith(".html")) {
242 mimeType = "text/html";
243 } else if (uri.endsWith(".js")) {
244 mimeType = "text/javascript";
245 }
246 rep = newChunkedResponse(Status.OK, mimeType,
247 in);
248 }
249
250 if (rep == null) {
251 getTraceHandler().trace("404: " + uri);
252 rep = newFixedLengthResponse(Status.NOT_FOUND,
253 NanoHTTPD.MIME_PLAINTEXT, "Not Found");
254 }
255 }
256 } catch (Exception e) {
257 Instance.getInstance().getTraceHandler().error(
258 new IOException("Cannot process web request",
259 e));
260 rep = newFixedLengthResponse(Status.INTERNAL_ERROR,
261 NanoHTTPD.MIME_PLAINTEXT, "An error occured");
262 }
263 }
264
265 return rep;
266 }
267 };
268
acbec0d2 269 if (ssf != null) {
33d40b9f
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
09c2396e
NR
285 protected void doStop() {
286 server.stop();
287 }
288
33d40b9f
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
312 private Response loginPage(WLoginResult login, String uri) {
313 StringBuilder builder = new StringBuilder();
314
315 appendPreHtml(builder, true);
316
317 if (login.isBadLogin()) {
9a705a5a
NR
318 builder.append(
319 "\t\t<div class='error'>Bad login or password</div>");
33d40b9f 320 } else if (login.isBadCookie()) {
9a705a5a
NR
321 builder.append(
322 "\t\t<div class='error'>Your session timed out</div>");
33d40b9f
NR
323 }
324
325 if (WebLibraryUrls.LOGOUT_URL.equals(uri)) {
326 uri = WebLibraryUrls.INDEX_URL;
327 }
328
9a705a5a
NR
329 builder.append("\t\t<form method='POST' action='" + uri
330 + "' class='login'>\n");
33d40b9f 331 builder.append(
9a705a5a
NR
332 "\t\t\t<p>You must be logged into the system to see the stories.</p>");
333 builder.append("\t\t\t<input type='text' name='login' />\n");
334 builder.append("\t\t\t<input type='password' name='password' />\n");
335 builder.append("\t\t\t<input type='submit' value='Login' />\n");
336 builder.append("\t\t</form>\n");
33d40b9f
NR
337
338 appendPostHtml(builder);
339
340 return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
341 NanoHTTPD.MIME_HTML, builder.toString());
342 }
343
344 private Response root(IHTTPSession session, Map<String, String> cookies,
345 WLoginResult login) throws IOException {
346 BasicLibrary lib = Instance.getInstance().getLibrary();
347 MetaResultList result = new MetaResultList(metas(login));
348 StringBuilder builder = new StringBuilder();
349
350 appendPreHtml(builder, true);
351
352 Map<String, String> params = session.getParms();
353
354 String filter = cookies.get("filter");
355 if (params.get("optionNo") != null)
356 filter = null;
357 if (filter == null) {
358 filter = "";
359 }
360
361 String browser = params.get("browser") == null ? ""
362 : params.get("browser");
363 String browser2 = params.get("browser2") == null ? ""
364 : params.get("browser2");
365 String browser3 = params.get("browser3") == null ? ""
366 : params.get("browser3");
367
368 String filterSource = null;
369 String filterAuthor = null;
370 String filterTag = null;
371
372 // TODO: javascript in realtime, using visible=false + hide [submit]
373
c4361515
NR
374 StringBuilder selects = new StringBuilder();
375 boolean sourcesSel = false;
376 boolean authorsSel = false;
377 boolean tagsSel = false;
33d40b9f 378
c4361515 379 String selectTemplate = getTemplate("browser.select");
33d40b9f
NR
380
381 if (!browser.isEmpty()) {
c4361515
NR
382 StringBuilder options = new StringBuilder();
383
33d40b9f 384 if (browser.equals("sources")) {
c4361515 385 sourcesSel = true;
33d40b9f 386 filterSource = browser2.isEmpty() ? filterSource : browser2;
c4361515 387
33d40b9f 388 // TODO: if 1 group -> no group
33d40b9f
NR
389 Map<String, List<String>> sources = result.getSourcesGrouped();
390 for (String source : sources.keySet()) {
c4361515 391 appendOption(options, 5, source, source, browser2);
33d40b9f
NR
392 }
393 } else if (browser.equals("authors")) {
c4361515 394 authorsSel = true;
33d40b9f 395 filterAuthor = browser2.isEmpty() ? filterAuthor : browser2;
c4361515 396
33d40b9f 397 // TODO: if 1 group -> no group
33d40b9f
NR
398 Map<String, List<String>> authors = result.getAuthorsGrouped();
399 for (String author : authors.keySet()) {
c4361515 400 appendOption(options, 5, author, author, browser2);
33d40b9f
NR
401 }
402 } else if (browser.equals("tags")) {
c4361515 403 tagsSel = true;
33d40b9f 404 filterTag = browser2.isEmpty() ? filterTag : browser2;
c4361515 405
33d40b9f 406 for (String tag : result.getTags()) {
c4361515 407 appendOption(options, 5, tag, tag, browser2);
33d40b9f
NR
408 }
409 }
c4361515
NR
410
411 selects.append(selectTemplate //
412 .replace("${name}", "browser2") //
413 .replace("${value}", browser2) //
414 .replace("${options}", options.toString()) //
415 );
33d40b9f
NR
416 }
417
418 if (!browser2.isEmpty()) {
c4361515
NR
419 StringBuilder options = new StringBuilder();
420
33d40b9f
NR
421 if (browser.equals("sources")) {
422 filterSource = browser3.isEmpty() ? filterSource : browser3;
423 Map<String, List<String>> sourcesGrouped = result
424 .getSourcesGrouped();
425 List<String> sources = sourcesGrouped.get(browser2);
426 if (sources != null && !sources.isEmpty()) {
427 // TODO: single empty value
33d40b9f 428 for (String source : sources) {
c4361515 429 appendOption(options, 5, source, source, browser3);
33d40b9f 430 }
33d40b9f
NR
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
33d40b9f 439 for (String author : authors) {
c4361515 440 appendOption(options, 5, author, author, browser3);
33d40b9f 441 }
33d40b9f
NR
442 }
443 }
33d40b9f 444
c4361515
NR
445 selects.append(selectTemplate //
446 .replace("${name}", "browser3") //
447 .replace("${value}", browser3) //
448 .replace("${options}", options.toString()) //
449 );
450 }
33d40b9f 451
c4361515
NR
452 String sel = "selected='selected'";
453 builder.append(getTemplate("browser") //
454 .replace("${sourcesSelected}", sourcesSel ? sel : "") //
455 .replace("${authorsSelected}", authorsSel ? sel : "") //
456 .replace("${tagsSelected}", tagsSel ? sel : "") //
457 .replace("${filter}", filter) //
458 .replace("${selects}", selects.toString()) //
459 );
33d40b9f 460
9a705a5a 461 builder.append("\t\t<div class='books'>\n");
33d40b9f
NR
462 for (MetaData meta : result.getMetas()) {
463 if (!filter.isEmpty() && !meta.getTitle().toLowerCase()
464 .contains(filter.toLowerCase())) {
465 continue;
466 }
467
468 // TODO Sub sources
469 if (filterSource != null
470 && !filterSource.equals(meta.getSource())) {
471 continue;
472 }
473
474 // TODO: sub authors
475 if (filterAuthor != null
476 && !filterAuthor.equals(meta.getAuthor())) {
477 continue;
478 }
479
480 if (filterTag != null && !meta.getTags().contains(filterTag)) {
481 continue;
482 }
483
c4361515 484 String author = "";
33d40b9f 485 if (meta.getAuthor() != null && !meta.getAuthor().isEmpty()) {
c4361515
NR
486 author = "(" + meta.getAuthor() + ")";
487 }
488
489 String cachedClass = "cached";
490 String cached = "&#9673;";
491 if (!lib.isCached(meta.getLuid())) {
492 cachedClass = "uncached";
493 cached = "&#9675;";
33d40b9f 494 }
c4361515
NR
495
496 builder.append(getTemplate("bookline") //
497 .replace("${href}",
498 WebLibraryUrls.getViewUrl(meta.getLuid(), null,
499 null)) //
500 .replace("${luid}", meta.getLuid()) //
501 .replace("${title}", meta.getTitle()) //
502 .replace("${author}", author) //
503 .replace("${cachedClass}", cachedClass) //
504 .replace("${cached}", cached) //
505 );
33d40b9f 506 }
9a705a5a 507 builder.append("\t\t</div>\n");
33d40b9f
NR
508
509 appendPostHtml(builder);
510 return NanoHTTPD.newFixedLengthResponse(builder.toString());
511 }
512
513 private Response getViewer(Map<String, String> cookies, String uri,
514 WLoginResult login) {
515 String[] cover = uri.split("/");
516 int off = 2;
517
518 if (cover.length < off + 2) {
519 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
520 NanoHTTPD.MIME_PLAINTEXT, null);
521 }
522
523 String type = cover[off + 0];
524 String luid = cover[off + 1];
525 String chapterStr = cover.length < off + 3 ? null : cover[off + 2];
526 String paragraphStr = cover.length < off + 4 ? null : cover[off + 3];
527
528 // 1-based (0 = desc)
529 int chapter = 0;
530 if (chapterStr != null) {
531 try {
532 chapter = Integer.parseInt(chapterStr);
533 if (chapter < 0) {
534 throw new NumberFormatException();
535 }
536 } catch (NumberFormatException e) {
537 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
538 NanoHTTPD.MIME_PLAINTEXT, "Chapter is not valid");
539 }
540 }
541
542 // 1-based
543 int paragraph = 0;
544 if (paragraphStr != null) {
545 try {
546 paragraph = Integer.parseInt(paragraphStr);
547 if (paragraph <= 0) {
548 throw new NumberFormatException();
549 }
550 } catch (NumberFormatException e) {
551 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
552 NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid");
553 }
554 }
555
556 try {
557 Story story = story(luid, login);
558 if (story == null) {
559 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
560 NanoHTTPD.MIME_PLAINTEXT, "Story not found");
561 }
562
563 StringBuilder builder = new StringBuilder();
564 appendPreHtml(builder, false);
565
566 // For images documents, always go to the images if not chap 0 desc
567 if (story.getMeta().isImageDocument()) {
568 if (chapter > 0 && paragraph <= 0)
569 paragraph = 1;
570 }
571
572 Chapter chap = null;
573 if (chapter <= 0) {
574 chap = story.getMeta().getResume();
575 } else {
576 try {
577 chap = story.getChapters().get(chapter - 1);
578 } catch (IndexOutOfBoundsException e) {
579 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
580 NanoHTTPD.MIME_PLAINTEXT, "Chapter not found");
581 }
582 }
583
584 String first, previous, next, last;
585
586 StringBuilder content = new StringBuilder();
587
588 String disabledLeft = "";
589 String disabledRight = "";
590 String disabledZoomReal = "";
591 String disabledZoomWidth = "";
592 String disabledZoomHeight = "";
593
594 if (paragraph <= 0) {
595 first = WebLibraryUrls.getViewUrl(luid, 0, null);
596 previous = WebLibraryUrls.getViewUrl(luid,
597 (Math.max(chapter - 1, 0)), null);
598 next = WebLibraryUrls.getViewUrl(luid,
599 (Math.min(chapter + 1, story.getChapters().size())),
600 null);
601 last = WebLibraryUrls.getViewUrl(luid,
602 story.getChapters().size(), null);
603
604 StringBuilder desc = new StringBuilder();
605
606 if (chapter <= 0) {
9a705a5a
NR
607 desc.append("\t\t\t<h1 class='title'>")
608 .append(story.getMeta().getTitle())
609 .append("</h1>\n");
610 desc.append("\t\t\t<div class='desc'>\n");
611 desc.append(
612 "\t\t\t\t<a href='" + next + "' class='cover'>\n");
613 desc.append("\t\t\t\t\t<img src='/story/" + luid
614 + "/cover'/>\n");
615 desc.append("\t\t\t\t</a>\n");
616 desc.append("\t\t\t\t<table class='details'>\n");
33d40b9f
NR
617 Map<String, String> details = BasicLibrary
618 .getMetaDesc(story.getMeta());
619 for (String key : details.keySet()) {
9a705a5a 620 appendTableRow(desc, 5, key, details.get(key));
33d40b9f 621 }
9a705a5a
NR
622 desc.append("\t\t\t\t</table>\n");
623 desc.append("\t\t\t</div>\n");
624 desc.append("\t\t\t<h1 class='title'>Description</h1>\n");
33d40b9f
NR
625 }
626
9a705a5a 627 content.append("\t\t<div class='viewer text'>\n");
33d40b9f 628 content.append(desc);
9a705a5a
NR
629 if (chap.getParagraphs().size() <= 0) {
630 content.append("\t\t\tNo content provided.\n");
631 } else {
632 content.append(
633 "\t\t\t<!-- The text is in HTML 3.2, so it can also work in Java Swing: -->\n"); //
634 content.append("\t\t\t").append(
635 new TextOutput(false).convert(chap, chapter > 0))
636 .append("\n");
637 }
638 content.append("\t\t</div>\n");
33d40b9f
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
695 String javascript = "document.getElementById(\"previous\").click(); return false;";
696 content.append(String.format("" //
9a705a5a
NR
697 + "\t\t<a class='viewer link' oncontextmenu='%s' href='%s'>\n"
698 + "\t\t\t<img class='viewer img' style='%s' src='%s'/>\n"
699 + "\t\t</a>\n", //
33d40b9f
NR
700 javascript, //
701 next, //
702 zoomStyle, //
703 WebLibraryUrls.getStoryUrl(luid, chapter,
704 paragraph)));
705 } else {
706 content.append(String.format("" //
9a705a5a
NR
707 + "\t\t<div class='viewer text'>" //
708 + "\t\t\t%s\n" //
709 + "\t\t</div>\n", //
33d40b9f
NR
710 para.getContent()));
711 }
712 }
713
714 builder.append(String.format("" //
9a705a5a
NR
715 + "\t\t<div class='bar navbar'>\n" //
716 + "\t\t\t<a%s class='button first' href='%s'>&lt;&lt;</a>\n"//
717 + "\t\t\t<a%s id='previous' class='button previous' href='%s'>&lt;</a>\n" //
718 + "\t\t\t<div class='gotobox itemsbox'>\n" //
719 + "\t\t\t\t<div class='button goto'>%d</div>\n" //
720 + "\t\t\t\t<div class='items goto'>\n", //
33d40b9f
NR
721 disabledLeft, first, //
722 disabledLeft, previous, //
723 paragraph > 0 ? paragraph : chapter //
724 ));
725
726 // List of chap/para links
727
9a705a5a 728 appendItemA(builder, 5, WebLibraryUrls.getViewUrl(luid, 0, null),
33d40b9f
NR
729 "Description", paragraph == 0 && chapter == 0);
730 if (paragraph > 0) {
731 for (int i = 1; i <= chap.getParagraphs().size(); i++) {
9a705a5a 732 appendItemA(builder, 5,
33d40b9f
NR
733 WebLibraryUrls.getViewUrl(luid, chapter, i),
734 "Image " + i, paragraph == i);
735 }
736 } else {
737 int i = 1;
738 for (Chapter c : story.getChapters()) {
739 String chapName = "Chapter " + c.getNumber();
740 if (c.getName() != null && !c.getName().isEmpty()) {
741 chapName += ": " + c.getName();
742 }
743
9a705a5a 744 appendItemA(builder, 5,
33d40b9f
NR
745 WebLibraryUrls.getViewUrl(luid, i, null), chapName,
746 chapter == i);
747
748 i++;
749 }
750 }
751
752 builder.append(String.format("" //
9a705a5a
NR
753 + "\t\t\t\t</div>\n" //
754 + "\t\t\t</div>\n" //
755 + "\t\t\t<a%s class='button next' href='%s'>&gt;</a>\n" //
756 + "\t\t\t<a%s class='button last' href='%s'>&gt;&gt;</a>\n"//
757 + "\t\t</div>\n", //
33d40b9f
NR
758 disabledRight, next, //
759 disabledRight, last //
760 ));
761
762 builder.append(content);
763
9a705a5a 764 builder.append("\t\t<div class='bar optionbar");
33d40b9f 765 if (paragraph > 0) {
9a705a5a 766 builder.append(" s4");
33d40b9f 767 } else {
9a705a5a 768 builder.append(" s1");
33d40b9f
NR
769 }
770 builder.append("'>\n");
9a705a5a 771 builder.append("\t\t\t<a class='button back' href='/'>BACK</a>\n");
33d40b9f
NR
772
773 if (paragraph > 0) {
774 builder.append(String.format("" //
9a705a5a
NR
775 + "\t\t\t<a%s class='button zoomreal' href='%s'>REAL</a>\n"//
776 + "\t\t\t<a%s class='button zoomwidth' href='%s'>WIDTH</a>\n"//
777 + "\t\t\t<a%s class='button zoomheight' href='%s'>HEIGHT</a>\n", //
33d40b9f
NR
778 disabledZoomReal,
779 uri + "?optionName=zoom&optionValue=real", //
780 disabledZoomWidth,
781 uri + "?optionName=zoom&optionValue=width", //
782 disabledZoomHeight,
783 uri + "?optionName=zoom&optionValue=height" //
784 ));
785 }
786
9a705a5a
NR
787 builder.append("\t\t</div>\n");
788
33d40b9f
NR
789 appendPostHtml(builder);
790 return NanoHTTPD.newFixedLengthResponse(Status.OK,
791 NanoHTTPD.MIME_HTML, builder.toString());
792 } catch (IOException e) {
793 Instance.getInstance().getTraceHandler()
794 .error(new IOException("Cannot get image: " + uri, e));
795 return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR,
796 NanoHTTPD.MIME_PLAINTEXT, "Error when processing request");
797 }
798 }
799
800 protected Response newInputStreamResponse(String mimeType, InputStream in) {
801 if (in == null) {
802 return NanoHTTPD.newFixedLengthResponse(Status.NO_CONTENT, "",
803 null);
804 }
805 return NanoHTTPD.newChunkedResponse(Status.OK, mimeType, in);
806 }
807
808 private String getContentOf(String file) {
809 InputStream in = IOUtils.openResource(WebLibraryServerIndex.class,
810 file);
811 if (in != null) {
812 try {
813 return IOUtils.readSmallStream(in);
814 } catch (IOException e) {
815 Instance.getInstance().getTraceHandler().error(
816 new IOException("Cannot get file: index.pre.html", e));
817 }
818 }
819
820 return "";
821 }
822
823 private void appendPreHtml(StringBuilder builder, boolean banner) {
824 String favicon = "favicon.ico";
825 String icon = Instance.getInstance().getUiConfig()
826 .getString(UiConfig.PROGRAM_ICON);
827 if (icon != null) {
828 favicon = "icon_" + icon.replace("-", "_") + ".png";
829 }
830
831 builder.append(
832 getContentOf("index.pre.html").replace("favicon.ico", favicon));
833
834 if (banner) {
9a705a5a
NR
835 builder.append("\t\t<div class='banner'>\n");
836 builder.append("\t\t\t<img class='ico' src='/") //
33d40b9f
NR
837 .append(favicon) //
838 .append("'/>\n");
9a705a5a
NR
839 builder.append("\t\t\t<h1>Fanfix</h1>\n");
840 builder.append("\t\t\t<h2>") //
33d40b9f
NR
841 .append(Version.getCurrentVersion()) //
842 .append("</h2>\n");
9a705a5a 843 builder.append("\t\t</div>\n");
33d40b9f
NR
844 }
845 }
846
847 private void appendPostHtml(StringBuilder builder) {
848 builder.append(getContentOf("index.post.html"));
849 }
850
851 private void appendOption(StringBuilder builder, int depth, String name,
852 String value, String selected) {
853 for (int i = 0; i < depth; i++) {
854 builder.append("\t");
855 }
856 builder.append("<option value='").append(value).append("'");
857 if (value.equals(selected)) {
858 builder.append(" selected='selected'");
859 }
860 builder.append(">").append(name).append("</option>\n");
861 }
862
863 private void appendTableRow(StringBuilder builder, int depth,
864 String... tds) {
865 for (int i = 0; i < depth; i++) {
866 builder.append("\t");
867 }
868
869 int col = 1;
870 builder.append("<tr>");
871 for (String td : tds) {
872 builder.append("<td class='col");
873 builder.append(col++);
874 builder.append("'>");
875 builder.append(td);
876 builder.append("</td>");
877 }
878 builder.append("</tr>\n");
879 }
880
881 private void appendItemA(StringBuilder builder, int depth, String link,
882 String name, boolean selected) {
883 for (int i = 0; i < depth; i++) {
884 builder.append("\t");
885 }
886
887 builder.append("<a href='");
888 builder.append(link);
889 builder.append("' class='item goto");
890 if (selected) {
891 builder.append(" selected");
892 }
893 builder.append("'>");
894 builder.append(name);
895 builder.append("</a>\n");
896 }
c4361515
NR
897
898 private String getTemplate(String template) throws IOException {
899 InputStream in = IOUtils.openResource(WebLibraryServerIndex.class,
900 template + ".html");
901 try {
902 return IOUtils.readSmallStream(in);
903 } finally {
904 in.close();
905 }
906 }
33d40b9f 907}