fix html indentation
[nikiroo-utils.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
9a705a5a
NR
374 builder.append("\t\t<form class='browser'>\n");
375 builder.append("\t\t\t<div class='breadcrumbs'>\n");
33d40b9f 376
9a705a5a
NR
377 builder.append("\t\t\t\t<select name='browser'>\n");
378 appendOption(builder, 5, "", "", browser);
379 appendOption(builder, 5, "Sources", "sources", browser);
380 appendOption(builder, 5, "Authors", "authors", browser);
381 appendOption(builder, 5, "Tags", "tags", browser);
382 builder.append("\t\t\t\t</select>\n");
33d40b9f
NR
383
384 if (!browser.isEmpty()) {
9a705a5a 385 builder.append("\t\t\t\t<select name='browser2'>\n");
33d40b9f
NR
386 if (browser.equals("sources")) {
387 filterSource = browser2.isEmpty() ? filterSource : browser2;
388 // TODO: if 1 group -> no group
9a705a5a 389 appendOption(builder, 5, "", "", browser2);
33d40b9f
NR
390 Map<String, List<String>> sources = result.getSourcesGrouped();
391 for (String source : sources.keySet()) {
9a705a5a 392 appendOption(builder, 5, source, source, browser2);
33d40b9f
NR
393 }
394 } else if (browser.equals("authors")) {
395 filterAuthor = browser2.isEmpty() ? filterAuthor : browser2;
396 // TODO: if 1 group -> no group
9a705a5a 397 appendOption(builder, 5, "", "", browser2);
33d40b9f
NR
398 Map<String, List<String>> authors = result.getAuthorsGrouped();
399 for (String author : authors.keySet()) {
9a705a5a 400 appendOption(builder, 5, author, author, browser2);
33d40b9f
NR
401 }
402 } else if (browser.equals("tags")) {
403 filterTag = browser2.isEmpty() ? filterTag : browser2;
9a705a5a 404 appendOption(builder, 5, "", "", browser2);
33d40b9f 405 for (String tag : result.getTags()) {
9a705a5a 406 appendOption(builder, 5, tag, tag, browser2);
33d40b9f
NR
407 }
408 }
9a705a5a 409 builder.append("\t\t\t\t</select>\n");
33d40b9f
NR
410 }
411
412 if (!browser2.isEmpty()) {
413 if (browser.equals("sources")) {
414 filterSource = browser3.isEmpty() ? filterSource : browser3;
415 Map<String, List<String>> sourcesGrouped = result
416 .getSourcesGrouped();
417 List<String> sources = sourcesGrouped.get(browser2);
418 if (sources != null && !sources.isEmpty()) {
419 // TODO: single empty value
9a705a5a
NR
420 builder.append("\t\t\t\t<select name='browser3'>\n");
421 appendOption(builder, 5, "", "", browser3);
33d40b9f 422 for (String source : sources) {
9a705a5a 423 appendOption(builder, 5, source, source, browser3);
33d40b9f 424 }
9a705a5a 425 builder.append("\t\t\t\t</select>\n");
33d40b9f
NR
426 }
427 } else if (browser.equals("authors")) {
428 filterAuthor = browser3.isEmpty() ? filterAuthor : browser3;
429 Map<String, List<String>> authorsGrouped = result
430 .getAuthorsGrouped();
431 List<String> authors = authorsGrouped.get(browser2);
432 if (authors != null && !authors.isEmpty()) {
433 // TODO: single empty value
9a705a5a
NR
434 builder.append("\t\t\t\t<select name='browser3'>\n");
435 appendOption(builder, 5, "", "", browser3);
33d40b9f 436 for (String author : authors) {
9a705a5a 437 appendOption(builder, 5, author, author, browser3);
33d40b9f 438 }
9a705a5a 439 builder.append("\t\t\t\t</select>\n");
33d40b9f
NR
440 }
441 }
442 }
443
9a705a5a
NR
444 builder.append("\t\t\t\t<input type='submit' value='Select'/>\n");
445 builder.append("\t\t\t</div>\n");
33d40b9f
NR
446
447 // TODO: javascript in realtime, using visible=false + hide [submit]
9a705a5a
NR
448 builder.append("\t\t\t<div class='filter'>\n");
449 builder.append("\t\t\t\t<span class='label'>Filter: </span>\n");
33d40b9f 450 builder.append(
9a705a5a
NR
451 "\t\t\t\t<input name='optionName' type='hidden' value='filter' />\n");
452 builder.append("\t\t\t\t<input name='optionValue' type='text' value='"
33d40b9f 453 + filter + "' place-holder='...' />\n");
33d40b9f 454 builder.append(
9a705a5a
NR
455 "\t\t\t\t<input name='optionNo' type='submit' value='x' />\n");
456 builder.append(
457 "\t\t\t\t<input name='submit' type='submit' value='Filter' />\n");
458 builder.append("\t\t\t</div>\n");
459 builder.append("\t\t</form>\n");
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
9a705a5a 484 builder.append("\t\t\t<div class='book_line'>");
33d40b9f
NR
485 builder.append("<a href='");
486 builder.append(
487 WebLibraryUrls.getViewUrl(meta.getLuid(), null, null));
488 builder.append("'");
489 builder.append(" class='link'>");
490
491 if (lib.isCached(meta.getLuid())) {
492 // â—‰ = &#9673;
493 builder.append(
494 "<span class='cache_icon cached'>&#9673;</span>");
495 } else {
496 // â—‹ = &#9675;
497 builder.append(
498 "<span class='cache_icon uncached'>&#9675;</span>");
499 }
500 builder.append("<span class='luid'>");
501 builder.append(meta.getLuid());
502 builder.append("</span>");
503 builder.append("<span class='title'>");
504 builder.append(meta.getTitle());
505 builder.append("</span>");
506 builder.append("<span class='author'>");
507 if (meta.getAuthor() != null && !meta.getAuthor().isEmpty()) {
508 builder.append("(").append(meta.getAuthor()).append(")");
509 }
510 builder.append("</span>");
511 builder.append("</a></div>\n");
512 }
9a705a5a 513 builder.append("\t\t</div>\n");
33d40b9f
NR
514
515 appendPostHtml(builder);
516 return NanoHTTPD.newFixedLengthResponse(builder.toString());
517 }
518
519 private Response getViewer(Map<String, String> cookies, String uri,
520 WLoginResult login) {
521 String[] cover = uri.split("/");
522 int off = 2;
523
524 if (cover.length < off + 2) {
525 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
526 NanoHTTPD.MIME_PLAINTEXT, null);
527 }
528
529 String type = cover[off + 0];
530 String luid = cover[off + 1];
531 String chapterStr = cover.length < off + 3 ? null : cover[off + 2];
532 String paragraphStr = cover.length < off + 4 ? null : cover[off + 3];
533
534 // 1-based (0 = desc)
535 int chapter = 0;
536 if (chapterStr != null) {
537 try {
538 chapter = Integer.parseInt(chapterStr);
539 if (chapter < 0) {
540 throw new NumberFormatException();
541 }
542 } catch (NumberFormatException e) {
543 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
544 NanoHTTPD.MIME_PLAINTEXT, "Chapter is not valid");
545 }
546 }
547
548 // 1-based
549 int paragraph = 0;
550 if (paragraphStr != null) {
551 try {
552 paragraph = Integer.parseInt(paragraphStr);
553 if (paragraph <= 0) {
554 throw new NumberFormatException();
555 }
556 } catch (NumberFormatException e) {
557 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
558 NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid");
559 }
560 }
561
562 try {
563 Story story = story(luid, login);
564 if (story == null) {
565 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
566 NanoHTTPD.MIME_PLAINTEXT, "Story not found");
567 }
568
569 StringBuilder builder = new StringBuilder();
570 appendPreHtml(builder, false);
571
572 // For images documents, always go to the images if not chap 0 desc
573 if (story.getMeta().isImageDocument()) {
574 if (chapter > 0 && paragraph <= 0)
575 paragraph = 1;
576 }
577
578 Chapter chap = null;
579 if (chapter <= 0) {
580 chap = story.getMeta().getResume();
581 } else {
582 try {
583 chap = story.getChapters().get(chapter - 1);
584 } catch (IndexOutOfBoundsException e) {
585 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
586 NanoHTTPD.MIME_PLAINTEXT, "Chapter not found");
587 }
588 }
589
590 String first, previous, next, last;
591
592 StringBuilder content = new StringBuilder();
593
594 String disabledLeft = "";
595 String disabledRight = "";
596 String disabledZoomReal = "";
597 String disabledZoomWidth = "";
598 String disabledZoomHeight = "";
599
600 if (paragraph <= 0) {
601 first = WebLibraryUrls.getViewUrl(luid, 0, null);
602 previous = WebLibraryUrls.getViewUrl(luid,
603 (Math.max(chapter - 1, 0)), null);
604 next = WebLibraryUrls.getViewUrl(luid,
605 (Math.min(chapter + 1, story.getChapters().size())),
606 null);
607 last = WebLibraryUrls.getViewUrl(luid,
608 story.getChapters().size(), null);
609
610 StringBuilder desc = new StringBuilder();
611
612 if (chapter <= 0) {
9a705a5a
NR
613 desc.append("\t\t\t<h1 class='title'>")
614 .append(story.getMeta().getTitle())
615 .append("</h1>\n");
616 desc.append("\t\t\t<div class='desc'>\n");
617 desc.append(
618 "\t\t\t\t<a href='" + next + "' class='cover'>\n");
619 desc.append("\t\t\t\t\t<img src='/story/" + luid
620 + "/cover'/>\n");
621 desc.append("\t\t\t\t</a>\n");
622 desc.append("\t\t\t\t<table class='details'>\n");
33d40b9f
NR
623 Map<String, String> details = BasicLibrary
624 .getMetaDesc(story.getMeta());
625 for (String key : details.keySet()) {
9a705a5a 626 appendTableRow(desc, 5, key, details.get(key));
33d40b9f 627 }
9a705a5a
NR
628 desc.append("\t\t\t\t</table>\n");
629 desc.append("\t\t\t</div>\n");
630 desc.append("\t\t\t<h1 class='title'>Description</h1>\n");
33d40b9f
NR
631 }
632
9a705a5a 633 content.append("\t\t<div class='viewer text'>\n");
33d40b9f 634 content.append(desc);
9a705a5a
NR
635 if (chap.getParagraphs().size() <= 0) {
636 content.append("\t\t\tNo content provided.\n");
637 } else {
638 content.append(
639 "\t\t\t<!-- The text is in HTML 3.2, so it can also work in Java Swing: -->\n"); //
640 content.append("\t\t\t").append(
641 new TextOutput(false).convert(chap, chapter > 0))
642 .append("\n");
643 }
644 content.append("\t\t</div>\n");
33d40b9f
NR
645
646 if (chapter <= 0)
647 disabledLeft = " disabled='disbaled'";
648 if (chapter >= story.getChapters().size())
649 disabledRight = " disabled='disbaled'";
650 } else {
651 first = WebLibraryUrls.getViewUrl(luid, chapter, 1);
652 previous = WebLibraryUrls.getViewUrl(luid, chapter,
653 (Math.max(paragraph - 1, 1)));
654 next = WebLibraryUrls.getViewUrl(luid, chapter,
655 (Math.min(paragraph + 1, chap.getParagraphs().size())));
656 last = WebLibraryUrls.getViewUrl(luid, chapter,
657 chap.getParagraphs().size());
658
659 if (paragraph <= 1)
660 disabledLeft = " disabled='disbaled'";
661 if (paragraph >= chap.getParagraphs().size())
662 disabledRight = " disabled='disbaled'";
663
664 // First -> previous *chapter*
665 if (chapter > 0)
666 disabledLeft = "";
667 first = WebLibraryUrls.getViewUrl(luid,
668 (Math.max(chapter - 1, 0)), null);
669 if (paragraph <= 1) {
670 previous = first;
671 }
672
673 Paragraph para = null;
674 try {
675 para = chap.getParagraphs().get(paragraph - 1);
676 } catch (IndexOutOfBoundsException e) {
677 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
678 NanoHTTPD.MIME_PLAINTEXT,
679 "Paragraph " + paragraph + " not found");
680 }
681
682 if (para.getType() == ParagraphType.IMAGE) {
683 String zoomStyle = "max-width: 100%;";
684 disabledZoomWidth = " disabled='disabled'";
685 String zoomOption = cookies.get("zoom");
686 if (zoomOption != null && !zoomOption.isEmpty()) {
687 if (zoomOption.equals("real")) {
688 zoomStyle = "";
689 disabledZoomWidth = "";
690 disabledZoomReal = " disabled='disabled'";
691 } else if (zoomOption.equals("width")) {
692 zoomStyle = "max-width: 100%;";
693 } else if (zoomOption.equals("height")) {
694 // see height of navbar + optionbar
695 zoomStyle = "max-height: calc(100% - 128px);";
696 disabledZoomWidth = "";
697 disabledZoomHeight = " disabled='disabled'";
698 }
699 }
700
701 String javascript = "document.getElementById(\"previous\").click(); return false;";
702 content.append(String.format("" //
9a705a5a
NR
703 + "\t\t<a class='viewer link' oncontextmenu='%s' href='%s'>\n"
704 + "\t\t\t<img class='viewer img' style='%s' src='%s'/>\n"
705 + "\t\t</a>\n", //
33d40b9f
NR
706 javascript, //
707 next, //
708 zoomStyle, //
709 WebLibraryUrls.getStoryUrl(luid, chapter,
710 paragraph)));
711 } else {
712 content.append(String.format("" //
9a705a5a
NR
713 + "\t\t<div class='viewer text'>" //
714 + "\t\t\t%s\n" //
715 + "\t\t</div>\n", //
33d40b9f
NR
716 para.getContent()));
717 }
718 }
719
720 builder.append(String.format("" //
9a705a5a
NR
721 + "\t\t<div class='bar navbar'>\n" //
722 + "\t\t\t<a%s class='button first' href='%s'>&lt;&lt;</a>\n"//
723 + "\t\t\t<a%s id='previous' class='button previous' href='%s'>&lt;</a>\n" //
724 + "\t\t\t<div class='gotobox itemsbox'>\n" //
725 + "\t\t\t\t<div class='button goto'>%d</div>\n" //
726 + "\t\t\t\t<div class='items goto'>\n", //
33d40b9f
NR
727 disabledLeft, first, //
728 disabledLeft, previous, //
729 paragraph > 0 ? paragraph : chapter //
730 ));
731
732 // List of chap/para links
733
9a705a5a 734 appendItemA(builder, 5, WebLibraryUrls.getViewUrl(luid, 0, null),
33d40b9f
NR
735 "Description", paragraph == 0 && chapter == 0);
736 if (paragraph > 0) {
737 for (int i = 1; i <= chap.getParagraphs().size(); i++) {
9a705a5a 738 appendItemA(builder, 5,
33d40b9f
NR
739 WebLibraryUrls.getViewUrl(luid, chapter, i),
740 "Image " + i, paragraph == i);
741 }
742 } else {
743 int i = 1;
744 for (Chapter c : story.getChapters()) {
745 String chapName = "Chapter " + c.getNumber();
746 if (c.getName() != null && !c.getName().isEmpty()) {
747 chapName += ": " + c.getName();
748 }
749
9a705a5a 750 appendItemA(builder, 5,
33d40b9f
NR
751 WebLibraryUrls.getViewUrl(luid, i, null), chapName,
752 chapter == i);
753
754 i++;
755 }
756 }
757
758 builder.append(String.format("" //
9a705a5a
NR
759 + "\t\t\t\t</div>\n" //
760 + "\t\t\t</div>\n" //
761 + "\t\t\t<a%s class='button next' href='%s'>&gt;</a>\n" //
762 + "\t\t\t<a%s class='button last' href='%s'>&gt;&gt;</a>\n"//
763 + "\t\t</div>\n", //
33d40b9f
NR
764 disabledRight, next, //
765 disabledRight, last //
766 ));
767
768 builder.append(content);
769
9a705a5a 770 builder.append("\t\t<div class='bar optionbar");
33d40b9f 771 if (paragraph > 0) {
9a705a5a 772 builder.append(" s4");
33d40b9f 773 } else {
9a705a5a 774 builder.append(" s1");
33d40b9f
NR
775 }
776 builder.append("'>\n");
9a705a5a 777 builder.append("\t\t\t<a class='button back' href='/'>BACK</a>\n");
33d40b9f
NR
778
779 if (paragraph > 0) {
780 builder.append(String.format("" //
9a705a5a
NR
781 + "\t\t\t<a%s class='button zoomreal' href='%s'>REAL</a>\n"//
782 + "\t\t\t<a%s class='button zoomwidth' href='%s'>WIDTH</a>\n"//
783 + "\t\t\t<a%s class='button zoomheight' href='%s'>HEIGHT</a>\n", //
33d40b9f
NR
784 disabledZoomReal,
785 uri + "?optionName=zoom&optionValue=real", //
786 disabledZoomWidth,
787 uri + "?optionName=zoom&optionValue=width", //
788 disabledZoomHeight,
789 uri + "?optionName=zoom&optionValue=height" //
790 ));
791 }
792
9a705a5a
NR
793 builder.append("\t\t</div>\n");
794
33d40b9f
NR
795 appendPostHtml(builder);
796 return NanoHTTPD.newFixedLengthResponse(Status.OK,
797 NanoHTTPD.MIME_HTML, builder.toString());
798 } catch (IOException e) {
799 Instance.getInstance().getTraceHandler()
800 .error(new IOException("Cannot get image: " + uri, e));
801 return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR,
802 NanoHTTPD.MIME_PLAINTEXT, "Error when processing request");
803 }
804 }
805
806 protected Response newInputStreamResponse(String mimeType, InputStream in) {
807 if (in == null) {
808 return NanoHTTPD.newFixedLengthResponse(Status.NO_CONTENT, "",
809 null);
810 }
811 return NanoHTTPD.newChunkedResponse(Status.OK, mimeType, in);
812 }
813
814 private String getContentOf(String file) {
815 InputStream in = IOUtils.openResource(WebLibraryServerIndex.class,
816 file);
817 if (in != null) {
818 try {
819 return IOUtils.readSmallStream(in);
820 } catch (IOException e) {
821 Instance.getInstance().getTraceHandler().error(
822 new IOException("Cannot get file: index.pre.html", e));
823 }
824 }
825
826 return "";
827 }
828
829 private void appendPreHtml(StringBuilder builder, boolean banner) {
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
837 builder.append(
838 getContentOf("index.pre.html").replace("favicon.ico", favicon));
839
840 if (banner) {
9a705a5a
NR
841 builder.append("\t\t<div class='banner'>\n");
842 builder.append("\t\t\t<img class='ico' src='/") //
33d40b9f
NR
843 .append(favicon) //
844 .append("'/>\n");
9a705a5a
NR
845 builder.append("\t\t\t<h1>Fanfix</h1>\n");
846 builder.append("\t\t\t<h2>") //
33d40b9f
NR
847 .append(Version.getCurrentVersion()) //
848 .append("</h2>\n");
9a705a5a 849 builder.append("\t\t</div>\n");
33d40b9f
NR
850 }
851 }
852
853 private void appendPostHtml(StringBuilder builder) {
854 builder.append(getContentOf("index.post.html"));
855 }
856
857 private void appendOption(StringBuilder builder, int depth, String name,
858 String value, String selected) {
859 for (int i = 0; i < depth; i++) {
860 builder.append("\t");
861 }
862 builder.append("<option value='").append(value).append("'");
863 if (value.equals(selected)) {
864 builder.append(" selected='selected'");
865 }
866 builder.append(">").append(name).append("</option>\n");
867 }
868
869 private void appendTableRow(StringBuilder builder, int depth,
870 String... tds) {
871 for (int i = 0; i < depth; i++) {
872 builder.append("\t");
873 }
874
875 int col = 1;
876 builder.append("<tr>");
877 for (String td : tds) {
878 builder.append("<td class='col");
879 builder.append(col++);
880 builder.append("'>");
881 builder.append(td);
882 builder.append("</td>");
883 }
884 builder.append("</tr>\n");
885 }
886
887 private void appendItemA(StringBuilder builder, int depth, String link,
888 String name, boolean selected) {
889 for (int i = 0; i < depth; i++) {
890 builder.append("\t");
891 }
892
893 builder.append("<a href='");
894 builder.append(link);
895 builder.append("' class='item goto");
896 if (selected) {
897 builder.append(" selected");
898 }
899 builder.append("'>");
900 builder.append(name);
901 builder.append("</a>\n");
902 }
903}