diff --git a/labyrinth-server.iml b/labyrinth-server.iml index 9a37eeb..ee5c0f7 100644 --- a/labyrinth-server.iml +++ b/labyrinth-server.iml @@ -13,15 +13,16 @@ - - + + - - + + + diff --git a/pom.xml b/pom.xml index 3a3f0b1..e5a2d62 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,10 @@ labyrinth-server 0.0.1-SNAPSHOT + + 1.7.35 + + ch.fritteli.labyrinth @@ -35,12 +39,12 @@ org.slf4j slf4j-api - 1.7.30 + ${slf4j.version} - org.slf4j - slf4j-simple - 1.7.30 + ch.qos.logback + logback-classic + 1.2.10 org.junit.jupiter @@ -95,12 +99,24 @@ - repo.gittr.ch - https://repo.gittr.ch/ + repo.gittr.ch.releases + https://repo.gittr.ch/releases/ true never + + false + never + + + + repo.gittr.ch.snapshots + https://repo.gittr.ch/snapshots/ + + false + never + true always diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java index 7ffb589..6360325 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java +++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java @@ -24,20 +24,24 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; @Slf4j public class LabyrinthServer { + @NonNull private final HttpServer httpServer; + @NonNull + private final ExecutorService executorService = Executors.newCachedThreadPool(); - public LabyrinthServer(@NonNull final ServerConfig config) throws IOException { + public LabyrinthServer(@NonNull final ServerConfig config) throws IOException, URISyntaxException { this.httpServer = HttpServer.create(new InetSocketAddress(config.getAddress(), config.getPort()), 5); - this.httpServer.createContext("/", exchange -> { - exchange.getResponseHeaders().add("Location", "/create"); - exchange.sendResponseHeaders(302, -1); - }); + this.httpServer.createContext("/", new StaticResourcesFileHandler(this.executorService)); this.httpServer.createContext("/create", this::handleCreate); this.httpServer.createContext("/render", this::handleRender); } @@ -94,97 +98,111 @@ public class LabyrinthServer { public void stop() { log.info("Stopping server ..."); this.httpServer.stop(5); + this.executorService.shutdown(); + try { + if (!this.executorService.awaitTermination(5, TimeUnit.SECONDS)) { + log.warn("Timeout occurred while awaiting termination of executor service"); + } + } catch (final InterruptedException e) { + log.error("Failed to await termination of executor service", e); + } log.info("Server stopped."); } private void handleCreate(HttpExchange exchange) throws IOException { - log.debug("Handling request to {}", exchange.getRequestURI()); - final String requestMethod = exchange.getRequestMethod(); - if (!requestMethod.equals("GET")) { - exchange.getResponseBody().close(); - exchange.sendResponseHeaders(405, -1); - exchange.close(); - return; - } - final Map requestParams = this.parseQueryString(exchange.getRequestURI().getQuery()); - final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 5); - final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 5); - final Option idOption = requestParams.get(RequestParameter.ID).toTry().map(Long::valueOf).toOption(); - final Option outputOption = requestParams.get(RequestParameter.OUTPUT).flatMap(OutputType::ofString); - final Headers responseHeaders = exchange.getResponseHeaders(); - final AtomicBoolean needsRedirect = new AtomicBoolean(false); - final long id = idOption.onEmpty(() -> needsRedirect.set(true)).getOrElse(System::nanoTime); - final OutputType output = outputOption.onEmpty(() -> needsRedirect.set(true)).getOrElse(OutputType.HTML); - if (needsRedirect.get()) { - responseHeaders.add("Location", "/create?width=" + width + "&height=" + height + "&output=" + output.toString() + "&id=" + id); - exchange.sendResponseHeaders(302, -1); - exchange.close(); - return; - } - final Labyrinth labyrinth = new Labyrinth(width, height, id); - final byte[] render; - try { - render = output.render(labyrinth); - } catch (Exception e) { - responseHeaders.add("Content-type", "text/plain; charset=UTF-8"); - exchange.sendResponseHeaders(500, 0); - final OutputStream responseBody = exchange.getResponseBody(); - responseBody.write(("Error: " + e).getBytes(StandardCharsets.UTF_8)); - responseBody.flush(); - exchange.close(); - return; - } - responseHeaders.add("Content-type", output.getContentType()); - exchange.sendResponseHeaders(200, 0); - final OutputStream responseBody = exchange.getResponseBody(); - responseBody.write(render); - responseBody.flush(); - exchange.close(); + this.executorService.submit(() -> { + log.debug("Handling request to {}", exchange.getRequestURI()); + try { + final String requestMethod = exchange.getRequestMethod(); + if (!requestMethod.equals("GET")) { + exchange.getResponseBody().close(); + exchange.sendResponseHeaders(405, -1); + return; + } + final Map requestParams = this.parseQueryString(exchange.getRequestURI().getQuery()); + final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 5); + final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 5); + final Option idOption = requestParams.get(RequestParameter.ID).toTry().map(Long::valueOf).toOption(); + final Option outputOption = requestParams.get(RequestParameter.OUTPUT).flatMap(OutputType::ofString); + final Headers responseHeaders = exchange.getResponseHeaders(); + final AtomicBoolean needsRedirect = new AtomicBoolean(false); + final long id = idOption.onEmpty(() -> needsRedirect.set(true)).getOrElse(System::nanoTime); + final OutputType output = outputOption.onEmpty(() -> needsRedirect.set(true)).getOrElse(OutputType.HTML); + if (needsRedirect.get()) { + responseHeaders.add("Location", "/create?width=" + width + "&height=" + height + "&output=" + output.toString() + "&id=" + id); + exchange.sendResponseHeaders(302, -1); + return; + } + final Labyrinth labyrinth = new Labyrinth(width, height, id); + final byte[] render; + try { + render = output.render(labyrinth); + } catch (Exception e) { + responseHeaders.add("Content-type", "text/plain; charset=UTF-8"); + exchange.sendResponseHeaders(500, 0); + final OutputStream responseBody = exchange.getResponseBody(); + responseBody.write(("Error: " + e).getBytes(StandardCharsets.UTF_8)); + responseBody.flush(); + return; + } + responseHeaders.add("Content-type", output.getContentType()); + if (output.equals(OutputType.BINARY)) { + responseHeaders.add("Content-disposition", "attachment; filename=\"labyrinth-" + width + "x" + height + "-" + id + ".laby\""); + } + exchange.sendResponseHeaders(200, 0); + final OutputStream responseBody = exchange.getResponseBody(); + responseBody.write(render); + responseBody.flush(); + } catch (Exception e) { + log.error("FSCK!", e); + } finally { + exchange.close(); + } + }); } private void handleRender(final HttpExchange exchange) throws IOException { - try { - log.debug("Handling request to {}", exchange.getRequestURI()); - final String requestMethod = exchange.getRequestMethod(); - if (!requestMethod.equals("POST")) { - exchange.getResponseBody().close(); - exchange.sendResponseHeaders(405, -1); - return; - } - final byte[] bytes = exchange.getRequestBody().readAllBytes(); - @NonNull final Labyrinth labyrinth; + this.executorService.submit(() -> { try { - labyrinth = SerializerDeserializer.deserialize(bytes); - } catch (Exception e) { - e.printStackTrace(); - throw e; - } + log.debug("Handling request to {}", exchange.getRequestURI()); + final String requestMethod = exchange.getRequestMethod(); + if (!requestMethod.equals("POST")) { + exchange.getResponseBody().close(); + exchange.sendResponseHeaders(405, -1); + return; + } + final byte[] bytes = exchange.getRequestBody().readAllBytes(); - OutputType output = exchange.getRequestHeaders() - .get("Accept") - .contains("text/html") ? - OutputType.HTML : - OutputType.TEXT_PLAIN; - final byte[] render; - final Headers responseHeaders = exchange.getResponseHeaders(); - try { - render = output.render(labyrinth); - } catch (Exception e) { - responseHeaders.add("Content-type", "text/plain; charset=UTF-8"); - exchange.sendResponseHeaders(500, 0); + final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); + + final OutputType output = exchange.getRequestHeaders() + .get("Accept") + .contains("text/html") ? + OutputType.HTML : + OutputType.TEXT_PLAIN; + final byte[] render; + final Headers responseHeaders = exchange.getResponseHeaders(); + try { + render = output.render(labyrinth); + } catch (Exception e) { + responseHeaders.add("Content-type", "text/plain; charset=UTF-8"); + exchange.sendResponseHeaders(500, 0); + final OutputStream responseBody = exchange.getResponseBody(); + responseBody.write(("Error: " + e).getBytes(StandardCharsets.UTF_8)); + responseBody.flush(); + return; + } + responseHeaders.add("Content-type", output.getContentType()); + exchange.sendResponseHeaders(200, 0); final OutputStream responseBody = exchange.getResponseBody(); - responseBody.write(("Error: " + e).getBytes(StandardCharsets.UTF_8)); + responseBody.write(render); responseBody.flush(); - return; + } catch (Exception e) { + log.error("FSCK!", e); + } finally { + exchange.close(); } - responseHeaders.add("Content-type", output.getContentType()); - exchange.sendResponseHeaders(200, 0); - final OutputStream responseBody = exchange.getResponseBody(); - responseBody.write(render); - responseBody.flush(); - } finally { - exchange.close(); - } + }); } private enum RequestParameter { diff --git a/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java b/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java new file mode 100644 index 0000000..d15f1fd --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java @@ -0,0 +1,98 @@ +package ch.fritteli.labyrinth.server; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.pdfbox.io.IOUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; + +@Slf4j +public class StaticResourcesFileHandler implements HttpHandler { + + public static final String WEBASSETS_DIRECTORY = "webassets"; + private final ExecutorService executorService; + + public StaticResourcesFileHandler(final ExecutorService executorService) { + this.executorService = executorService; + } + + private static void redirect(@NonNull final HttpExchange exchange, @NonNull final String target) throws IOException { + log.debug("Sending redirect to {}", target); + exchange.getResponseHeaders().add("Location", target); + exchange.sendResponseHeaders(302, -1); + } + + private static void notFound(@NonNull final HttpExchange exchange, @NonNull final String path) throws IOException { + log.debug("Resource '{}' not found, replying with HTTP 404", path); + exchange.getResponseHeaders().add("Content-type", "text/plain; charset=utf-8"); + exchange.sendResponseHeaders(404, 0); + exchange.getResponseBody().write("404 - Not found".getBytes(StandardCharsets.UTF_8)); + exchange.getResponseBody().flush(); + } + + @NonNull + private static byte[] getBytes(@NonNull final String path) throws IOException { + final InputStream stream = StaticResourcesFileHandler.class.getClassLoader().getResourceAsStream(WEBASSETS_DIRECTORY + path); + if (stream == null) { + log.debug("Resource '{}' not found in classpath.", path); + return new byte[0]; + } + return IOUtils.toByteArray(stream); + } + + @Nullable + private static String getMimeType(@NonNull final String path) { + if (path.endsWith(".html")) { + return "text/html"; + } + if (path.endsWith(".css")) { + return "text/css"; + } + return null; + } + + @Override + public void handle(@NonNull final HttpExchange exchange) throws IOException { + this.executorService.submit(() -> { + try { + final URI requestURI = exchange.getRequestURI(); + final String path = requestURI.getPath(); + log.debug("Handling request to {}", path); + if ("/".equals(path)) { + redirect(exchange, "/index.html"); + return; + } + if (!path.startsWith("/")) { + notFound(exchange, path); + return; + } + final String mimeType = getMimeType(path); + if (mimeType == null) { + notFound(exchange, path); + return; + } + final byte[] responseBytes = getBytes(path); + if (responseBytes.length == 0) { + notFound(exchange, path); + return; + } + log.debug("Serving {}{} with mimetype {}: {} bytes", WEBASSETS_DIRECTORY, path, mimeType, responseBytes.length); + exchange.getResponseHeaders().add("Content-type", mimeType); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().write(responseBytes); + exchange.getResponseBody().flush(); + } catch (Exception e) { + log.error("FSCK!", e); + } finally { + exchange.close(); + } + }); + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..08ae395 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/src/main/resources/webassets/index.html b/src/main/resources/webassets/index.html new file mode 100644 index 0000000..8248e96 --- /dev/null +++ b/src/main/resources/webassets/index.html @@ -0,0 +1,32 @@ + + + + + Labyrinth Generator + + + +
+

Labyrinth Generator

+

Enter some values, click the "Create!" button and see what happens!

+
+
+ + + + + +
+
+ + +
+
+
+ + diff --git a/src/main/resources/webassets/style.css b/src/main/resources/webassets/style.css new file mode 100644 index 0000000..153f8e8 --- /dev/null +++ b/src/main/resources/webassets/style.css @@ -0,0 +1,51 @@ +:root { + --color-background: #181a1b; + --color-foreground: #e8e6e3; + --color-border: #5d6164; + --color-background-highlight: #292b2c; + --color-foreground-highlight: #f8f7f4; + --color-border-highlight: #6e7275; +} + +body { + display: flex; + font-family: sans-serif; + justify-content: center; +} + +body, button, input, select { + background-color: var(--color-background); + color: var(--color-foreground); +} + +button, input, select { + border: 1px solid var(--color-border); +} + +button:active, input:active, select:active, +button:focus, input:focus, select:focus, +button:hover, input:hover, select:hover { + background-color: var(--color-background-highlight); + border-color: var(--color-border-highlight); + color: var(--color-foreground-highlight); +} + +button.primary { + background-color: #ffcc00; + border-color: #eebb00; + color: var(--color-background); +} + +.content { + width: 75%; +} + +.content h1 { + text-align: center; +} + +.inputs-wrapper { + display: grid; + grid-row-gap: 1em; + grid-template-columns: 0.5fr 1fr; +}