From 682e0d0f25d26e28abe8afbc56f1e5c417aefdb3 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sun, 14 Feb 2021 17:28:36 +0100 Subject: [PATCH 01/15] Intermediate commit; to be amended. --- pom.xml | 8 +- .../ch/fritteli/labyrinth/server/Main.java | 14 +++- .../UndertowPlayground.java | 78 +++++++++++++++++++ 3 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java diff --git a/pom.xml b/pom.xml index 5035e90..8259a2d 100644 --- a/pom.xml +++ b/pom.xml @@ -48,10 +48,14 @@ logback-classic ${logback.version} + + io.undertow + undertow-core + 2.2.22.Final + org.junit.jupiter junit-jupiter-api - test @@ -83,7 +87,7 @@ scm:git:git://gittr.ch/java/labyrinth-server.git scm:git:ssh://git@gittr.ch/java/labyrinth-server.git https://gittr.ch/java/labyrinth-server - v0.0.1 + HEAD diff --git a/src/main/java/ch/fritteli/labyrinth/server/Main.java b/src/main/java/ch/fritteli/labyrinth/server/Main.java index ced13bd..21e0d6c 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/Main.java +++ b/src/main/java/ch/fritteli/labyrinth/server/Main.java @@ -1,11 +1,21 @@ package ch.fritteli.labyrinth.server; +import ch.fritteli.labyrinth.server.undertow_playground.UndertowPlayground; +import io.undertow.Undertow; import lombok.extern.slf4j.Slf4j; @Slf4j public class Main { public static void main(String[] args) { - LabyrinthServer.createAndStartServer() - .onEmpty(() -> log.error("Failed to create server. Stopping.")); +// LabyrinthServer.createAndStartServer() +// .onEmpty(() -> log.error("Failed to create server. Stopping.")); + + final ServerConfig config = ServerConfig.init(); + log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort()); + Undertow.builder() + .addHttpListener(config.getPort(), config.getAddress().getHostAddress()) + .setHandler(UndertowPlayground.r) + .build() + .start(); } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java new file mode 100644 index 0000000..b64a343 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java @@ -0,0 +1,78 @@ +package ch.fritteli.labyrinth.server.undertow_playground; + +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.RoutingHandler; +import io.undertow.util.HeaderValues; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.vavr.control.Option; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +import java.util.Deque; +import java.util.Map; + +@Slf4j +public class UndertowPlayground { + public static final RoutingHandler r = new RoutingHandler() + .get("/create/{output}", new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + if (exchange.isInIoThread()) { + exchange.dispatch(this); + return; + } + final Map> queryParameters = exchange.getQueryParameters(); + final Option output = getFirstOption(queryParameters, "output"); + final Option width = getIntOption(queryParameters, "width"); + final Option height = getIntOption(queryParameters, "height"); + final Option id = getIntOption(queryParameters, "id"); + + log.info("Output: {}", output); + log.info("Width: {}", width); + log.info("Height: {}", height); + log.info("Id: {}", id); + exchange.getResponseSender().send( + "Output: " + output + + ", Width: " + width + + ", Height: " + height + + ", Id: " + id + ); + } + }) + .post("/render", new HttpHandler() { + @Override + public void handleRequest(final HttpServerExchange exchange) { + if (exchange.isInIoThread()) { + exchange.dispatch(this); + return; + } + exchange.getResponseSender().send("TODO: read body, render stuff"); + } + }) + .setFallbackHandler(new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.getResponseSender().send("Request: " + exchange.getRequestURI()); + final HeaderValues strings = exchange.getRequestHeaders().get(Headers.ACCEPT); + strings.peekFirst(); + } + } + ); + + @NonNull + private static Option getFirstOption(@NonNull final Map> queryParams, @NonNull final String paramName) { + return Option.of(queryParams.get(paramName)) + .map(Deque::peek) + .flatMap(Option::of); + } + + @NonNull + private static Option getIntOption(@NonNull final Map> queryParams, @NonNull final String paramName) { + return getFirstOption(queryParams, paramName) + .toTry() + .map(Integer::parseInt) + .toOption(); + } +} From 0bc1114aec2e71ac67fc73c0cdf923fff24f9371 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sun, 2 Apr 2023 10:15:22 +0200 Subject: [PATCH 02/15] Reformat everything. First prototype of runnign with untertow. --- maven-settings.xml | 20 +- pom.xml | 248 +++++++++--------- .../labyrinth/server/LabyrinthServer.java | 22 +- .../labyrinth/server/ServerConfig.java | 37 +-- .../server/StaticResourcesFileHandler.java | 84 +++--- .../UndertowPlayground.java | 108 ++++---- src/main/resources/logback.xml | 24 +- src/main/resources/webassets/index.html | 8 +- 8 files changed, 281 insertions(+), 270 deletions(-) diff --git a/maven-settings.xml b/maven-settings.xml index adce7ab..18a15cb 100644 --- a/maven-settings.xml +++ b/maven-settings.xml @@ -1,12 +1,12 @@ - - - - repo.gittr.ch - ci - ${env.REPO_TOKEN} - - + + + + repo.gittr.ch + ci + ${env.REPO_TOKEN} + + diff --git a/pom.xml b/pom.xml index 8259a2d..f99aeac 100644 --- a/pom.xml +++ b/pom.xml @@ -1,130 +1,132 @@ - - 4.0.0 + + 4.0.0 - - ch.fritteli - fritteli-build-parent - 2.0.4 - + + ch.fritteli + fritteli-build-parent + 2.0.4 + - ch.fritteli.labyrinth - labyrinth-server - 0.0.2-SNAPSHOT + ch.fritteli.labyrinth + labyrinth-server + 0.0.2-SNAPSHOT - - 1.4.6 - 1.18.26 - 2.0.5 - 17 - 17 - + + 1.4.6 + 1.18.26 + 2.0.5 + 17 + 17 + - - - ch.fritteli.labyrinth - labyrinth-generator - 0.0.2 - - - io.vavr - vavr - - - org.projectlombok - lombok - - - org.jetbrains - annotations - - - org.slf4j - slf4j-api - ${slf4j.version} - - - ch.qos.logback - logback-classic - ${logback.version} - - - io.undertow - undertow-core - 2.2.22.Final - - - org.junit.jupiter - junit-jupiter-api - - + + + ch.fritteli.labyrinth + labyrinth-generator + 0.0.2 + + + io.vavr + vavr + + + org.projectlombok + lombok + + + org.jetbrains + annotations + + + org.slf4j + slf4j-api + ${slf4j.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + io.undertow + undertow-core + 2.2.22.Final + + + org.junit.jupiter + junit-jupiter-api + + - - - - org.apache.maven.plugins - maven-shade-plugin - 3.2.4 - - - - ch.fritteli.labyrinth.server.Main - - - - - - package - - shade - - - - - - - - scm:git:git://gittr.ch/java/labyrinth-server.git - scm:git:ssh://git@gittr.ch/java/labyrinth-server.git - https://gittr.ch/java/labyrinth-server - HEAD - - - - repo.gittr.ch - gittr.ch - https://repo.gittr.ch/releases/ - - - repo.gittr.ch - gittr.ch - https://repo.gittr.ch/snapshots/ - - - - - 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 - - - + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + + ch.fritteli.labyrinth.server.Main + + + + + + package + + shade + + + + + + + + scm:git:git://gittr.ch/java/labyrinth-server.git + scm:git:ssh://git@gittr.ch/java/labyrinth-server.git + https://gittr.ch/java/labyrinth-server + HEAD + + + + repo.gittr.ch + gittr.ch + https://repo.gittr.ch/releases/ + + + repo.gittr.ch + gittr.ch + https://repo.gittr.ch/snapshots/ + + + + + 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 aab20f6..f082742 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java +++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java @@ -119,15 +119,13 @@ public class LabyrinthServer { } responseHeaders.add("Content-type", output.getContentType()); if (output.isAttachment()) { - responseHeaders.add( - "Content-disposition", - String.format("attachment; filename=\"labyrinth-%dx%d-%d.%s\"", - width, - height, - id, - output.getFileExtension() - ) - ); + responseHeaders.add("Content-disposition", String.format( + "attachment; filename=\"labyrinth-%dx%d-%d.%s\"", + width, + height, + id, + output.getFileExtension() + )); } exchange.sendResponseHeaders(200, 0); final OutputStream responseBody = exchange.getResponseBody(); @@ -254,7 +252,7 @@ public class LabyrinthServer { } } - private enum OutputType { + public enum OutputType { TEXT_PLAIN("text/plain; charset=UTF-8", labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), "txt", @@ -303,7 +301,7 @@ public class LabyrinthServer { this.names = List.of(names); } - static Option ofString(@Nullable final String name) { + public static Option ofString(@Nullable final String name) { if (name == null) { return Option.none(); } @@ -316,7 +314,7 @@ public class LabyrinthServer { return this.names.last(); } - byte[] render(@NonNull final Labyrinth labyrinth) { + public byte[] render(@NonNull final Labyrinth labyrinth) { return this.render.apply(labyrinth); } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java b/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java index 58cee13..fc161dd 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java +++ b/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java @@ -10,7 +10,6 @@ import org.jetbrains.annotations.Nullable; import java.net.InetAddress; - @AllArgsConstructor(access = AccessLevel.PRIVATE) @Slf4j @Value @@ -18,8 +17,7 @@ public class ServerConfig { public static final String SYSPROP_HOST = "fritteli.labyrinth.server.host"; public static final String SYSPROP_PORT = "fritteli.labyrinth.server.port"; - @NonNull - InetAddress address; + @NonNull InetAddress address; int port; public ServerConfig(@Nullable final String address, final int port) throws ConfigurationException { @@ -28,6 +26,19 @@ public class ServerConfig { log.debug("host={}, port={}", this.address, this.port); } + @NonNull + private static InetAddress validateAddress(@Nullable final String address) { + return Try.of(() -> InetAddress.getByName(address)) + .getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause)); + } + + private static int validatePort(final int port) { + if (port < 0 || port > 0xFFFF) { + throw new ConfigurationException("Port out of range (0..65535): " + port); + } + return port; + } + @NonNull public static ServerConfig init() throws ConfigurationException { final String host = System.getProperty(SYSPROP_HOST); @@ -36,26 +47,16 @@ public class ServerConfig { return new ServerConfig(host, port); } - @NonNull - private static InetAddress validateAddress(@Nullable final String address) { - return Try.of(() -> InetAddress.getByName(address)) - .getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause)); - } - private static int validatePort(@Nullable final String portString) { if (portString == null) { log.info("No port configured; using default."); return 0; } return Try.of(() -> Integer.valueOf(portString)) - .map(ServerConfig::validatePort) - .getOrElseThrow(cause -> new ConfigurationException("Failed to parse port specified in system property '" + SYSPROP_PORT + "': " + portString, cause)); - } - - private static int validatePort(final int port) { - if (port < 0 || port > 0xFFFF) { - throw new ConfigurationException("Port out of range (0..65535): " + port); - } - return port; + .map(ServerConfig::validatePort) + .getOrElseThrow(cause -> new ConfigurationException( + "Failed to parse port specified in system property '" + SYSPROP_PORT + "': " + portString, + cause + )); } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java b/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java index d1cabfc..e6911e8 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java @@ -24,43 +24,6 @@ public class StaticResourcesFileHandler implements HttpHandler { log.debug("Created {}", this.getClass().getSimpleName()); } - 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]; - } - final byte[] response = IOUtils.toByteArray(stream); - log.debug("Sending reply; {} bytes", response.length); - return response; - } - - @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(() -> { @@ -86,7 +49,13 @@ public class StaticResourcesFileHandler implements HttpHandler { notFound(exchange, path); return; } - log.debug("Serving {}{} with mimetype {}: {} bytes", WEBASSETS_DIRECTORY, path, mimeType, responseBytes.length); + 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); @@ -98,4 +67,43 @@ public class StaticResourcesFileHandler implements HttpHandler { } }); } + + 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(); + } + + @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; + } + + @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]; + } + final byte[] response = IOUtils.toByteArray(stream); + log.debug("Sending reply; {} bytes", response.length); + return response; + } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java index b64a343..8bb037f 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java +++ b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java @@ -1,5 +1,7 @@ package ch.fritteli.labyrinth.server.undertow_playground; +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.server.LabyrinthServer; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.server.RoutingHandler; @@ -10,69 +12,69 @@ import io.vavr.control.Option; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; +import java.nio.ByteBuffer; import java.util.Deque; import java.util.Map; +import java.util.Random; @Slf4j public class UndertowPlayground { - public static final RoutingHandler r = new RoutingHandler() - .get("/create/{output}", new HttpHandler() { - @Override - public void handleRequest(HttpServerExchange exchange) throws Exception { - if (exchange.isInIoThread()) { - exchange.dispatch(this); - return; - } - final Map> queryParameters = exchange.getQueryParameters(); - final Option output = getFirstOption(queryParameters, "output"); - final Option width = getIntOption(queryParameters, "width"); - final Option height = getIntOption(queryParameters, "height"); - final Option id = getIntOption(queryParameters, "id"); + public static final RoutingHandler r = new RoutingHandler().get("/create/{output}", new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + if (exchange.isInIoThread()) { + exchange.dispatch(this); + return; + } + final Map> queryParameters = exchange.getQueryParameters(); + final Option output = getFirstOption(queryParameters, "output"); + final Option width = getIntOption(queryParameters, "width"); + final Option height = getIntOption(queryParameters, "height"); + final Option id = getIntOption(queryParameters, "id"); - log.info("Output: {}", output); - log.info("Width: {}", width); - log.info("Height: {}", height); - log.info("Id: {}", id); - exchange.getResponseSender().send( - "Output: " + output + - ", Width: " + width + - ", Height: " + height + - ", Id: " + id - ); - } - }) - .post("/render", new HttpHandler() { - @Override - public void handleRequest(final HttpServerExchange exchange) { - if (exchange.isInIoThread()) { - exchange.dispatch(this); - return; - } - exchange.getResponseSender().send("TODO: read body, render stuff"); - } - }) - .setFallbackHandler(new HttpHandler() { - @Override - public void handleRequest(HttpServerExchange exchange) throws Exception { - exchange.getResponseSender().send("Request: " + exchange.getRequestURI()); - final HeaderValues strings = exchange.getRequestHeaders().get(Headers.ACCEPT); - strings.peekFirst(); - } - } - ); + log.info("Output: {}", output); + log.info("Width: {}", width); + log.info("Height: {}", height); + log.info("Id: {}", id); + final Integer theId = id.getOrElse(() -> new Random().nextInt()); + final Labyrinth labyrinth = new Labyrinth(width.get(), height.get(), theId); + final LabyrinthServer.OutputType outputType = output.flatMap(LabyrinthServer.OutputType::ofString).get(); + final byte[] result = outputType.render(labyrinth); + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, outputType.getContentType()); + exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(theId)); + exchange.getResponseHeaders() + .put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(width.get())); + exchange.getResponseHeaders() + .put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(height.get())); + exchange.getResponseSender().send(ByteBuffer.wrap(result)); + } + }).post("/render", new HttpHandler() { + @Override + public void handleRequest(final HttpServerExchange exchange) { + if (exchange.isInIoThread()) { + exchange.dispatch(this); + return; + } + exchange.getResponseSender().send("TODO: read body, render stuff"); + } + }).setFallbackHandler(new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.getResponseSender().send("Request: " + exchange.getRequestURI()); + final HeaderValues strings = exchange.getRequestHeaders().get(Headers.ACCEPT); + strings.peekFirst(); + } + }); @NonNull - private static Option getFirstOption(@NonNull final Map> queryParams, @NonNull final String paramName) { - return Option.of(queryParams.get(paramName)) - .map(Deque::peek) - .flatMap(Option::of); + private static Option getIntOption(@NonNull final Map> queryParams, + @NonNull final String paramName) { + return getFirstOption(queryParams, paramName).toTry().map(Integer::parseInt).toOption(); } @NonNull - private static Option getIntOption(@NonNull final Map> queryParams, @NonNull final String paramName) { - return getFirstOption(queryParams, paramName) - .toTry() - .map(Integer::parseInt) - .toOption(); + private static Option getFirstOption(@NonNull final Map> queryParams, + @NonNull final String paramName) { + return Option.of(queryParams.get(paramName)).map(Deque::peek).flatMap(Option::of); } } diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 4bba3c1..f484fc8 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,17 +1,17 @@ - + - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - + + + + %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 index 3444b42..b97b934 100644 --- a/src/main/resources/webassets/index.html +++ b/src/main/resources/webassets/index.html @@ -3,7 +3,7 @@ Labyrinth Generator - +
@@ -11,8 +11,8 @@

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

- - + +
- +
From 65421caf85b3e9732dc1318fa7bd90055c8ac741 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sun, 2 Apr 2023 10:17:50 +0200 Subject: [PATCH 03/15] refactoring --- .../labyrinth/server/LabyrinthServer.java | 72 ----------------- .../fritteli/labyrinth/server/OutputType.java | 77 +++++++++++++++++++ .../LabyrinthHttpHandler.java | 45 +++++++++++ .../UndertowPlayground.java | 40 +--------- 4 files changed, 125 insertions(+), 109 deletions(-) create mode 100644 src/main/java/ch/fritteli/labyrinth/server/OutputType.java create mode 100644 src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LabyrinthHttpHandler.java diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java index f082742..32bb27c 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java +++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java @@ -1,22 +1,17 @@ package ch.fritteli.labyrinth.server; import ch.fritteli.labyrinth.generator.model.Labyrinth; -import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer; -import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer; -import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer; import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; import io.vavr.collection.HashMap; import io.vavr.collection.HashSet; -import io.vavr.collection.List; import io.vavr.collection.Map; import io.vavr.collection.Set; import io.vavr.collection.Stream; import io.vavr.control.Option; import io.vavr.control.Try; -import lombok.Getter; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; @@ -251,71 +246,4 @@ public class LabyrinthServer { return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); } } - - public enum OutputType { - TEXT_PLAIN("text/plain; charset=UTF-8", - labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), - "txt", - false, - "t", - "text" - ), - HTML("text/html", - labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), - "html", - false, - "h", - "html" - ), - PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", false, "p", "pdf"), - PDFFILE("application/pdf", - labyrinth -> PDFRenderer.newInstance().render(labyrinth), - "pdf", - true, - "f", - "pdffile" - ), - BINARY("application/octet-stream", SerializerDeserializer::serialize, "laby", true, "b", "binary"); - @Getter - @NonNull - private final String contentType; - @NonNull - private final List names; - @NonNull - private final Function render; - @Getter - private final boolean attachment; - @Getter - @NonNull - private final String fileExtension; - - OutputType(@NonNull final String contentType, - @NonNull final Function render, - @NonNull final String fileExtension, - final boolean attachment, - @NonNull final String... names) { - this.contentType = contentType; - this.render = render; - this.fileExtension = fileExtension; - this.attachment = attachment; - this.names = List.of(names); - } - - public static Option ofString(@Nullable final String name) { - if (name == null) { - return Option.none(); - } - final String nameLC = name.toLowerCase(); - return Stream.of(values()).find(param -> param.names.contains(nameLC)); - } - - @Override - public String toString() { - return this.names.last(); - } - - public byte[] render(@NonNull final Labyrinth labyrinth) { - return this.render.apply(labyrinth); - } - } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java new file mode 100644 index 0000000..e627968 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java @@ -0,0 +1,77 @@ +package ch.fritteli.labyrinth.server; + +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer; +import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer; +import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer; +import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer; +import io.vavr.collection.List; +import io.vavr.collection.Stream; +import io.vavr.control.Option; +import lombok.Getter; +import lombok.NonNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.charset.StandardCharsets; +import java.util.function.Function; + +public enum OutputType { + TEXT_PLAIN("text/plain; charset=UTF-8", + labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), + "txt", + false, + "t", + "text" + ), + HTML("text/html", + labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), + "html", + false, + "h", + "html" + ), + PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", false, "p", "pdf"), + PDFFILE("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", true, "f", "pdffile"), + BINARY("application/octet-stream", SerializerDeserializer::serialize, "laby", true, "b", "binary"); + @Getter + @NonNull + private final String contentType; + @NonNull + private final List names; + @NonNull + private final Function render; + @Getter + private final boolean attachment; + @Getter + @NonNull + private final String fileExtension; + + OutputType(@NonNull final String contentType, + @NonNull final Function render, + @NonNull final String fileExtension, + final boolean attachment, + @NonNull final String... names) { + this.contentType = contentType; + this.render = render; + this.fileExtension = fileExtension; + this.attachment = attachment; + this.names = List.of(names); + } + + public static Option ofString(@Nullable final String name) { + if (name == null) { + return Option.none(); + } + final String nameLC = name.toLowerCase(); + return Stream.of(values()).find(param -> param.names.contains(nameLC)); + } + + @Override + public String toString() { + return this.names.last(); + } + + public byte[] render(@NonNull final Labyrinth labyrinth) { + return this.render.apply(labyrinth); + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LabyrinthHttpHandler.java b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LabyrinthHttpHandler.java new file mode 100644 index 0000000..4cc6658 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LabyrinthHttpHandler.java @@ -0,0 +1,45 @@ +package ch.fritteli.labyrinth.server.undertow_playground; + +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.server.OutputType; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.vavr.control.Option; +import lombok.extern.slf4j.Slf4j; + +import java.nio.ByteBuffer; +import java.util.Deque; +import java.util.Map; +import java.util.Random; + +@Slf4j +class LabyrinthHttpHandler implements HttpHandler { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + if (exchange.isInIoThread()) { + exchange.dispatch(this); + return; + } + final Map> queryParameters = exchange.getQueryParameters(); + final Option output = UndertowPlayground.getFirstOption(queryParameters, "output"); + final Option width = UndertowPlayground.getIntOption(queryParameters, "width"); + final Option height = UndertowPlayground.getIntOption(queryParameters, "height"); + final Option id = UndertowPlayground.getIntOption(queryParameters, "id"); + + log.info("Output: {}", output); + log.info("Width: {}", width); + log.info("Height: {}", height); + log.info("Id: {}", id); + final Integer theId = id.getOrElse(() -> new Random().nextInt()); + final Labyrinth labyrinth = new Labyrinth(width.get(), height.get(), theId); + final OutputType outputType = output.flatMap(OutputType::ofString).get(); + final byte[] result = outputType.render(labyrinth); + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, outputType.getContentType()); + exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(theId)); + exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(width.get())); + exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(height.get())); + exchange.getResponseSender().send(ByteBuffer.wrap(result)); + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java index 8bb037f..1ef547c 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java +++ b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java @@ -1,54 +1,20 @@ package ch.fritteli.labyrinth.server.undertow_playground; -import ch.fritteli.labyrinth.generator.model.Labyrinth; -import ch.fritteli.labyrinth.server.LabyrinthServer; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.server.RoutingHandler; import io.undertow.util.HeaderValues; import io.undertow.util.Headers; -import io.undertow.util.HttpString; import io.vavr.control.Option; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; -import java.nio.ByteBuffer; import java.util.Deque; import java.util.Map; -import java.util.Random; @Slf4j public class UndertowPlayground { - public static final RoutingHandler r = new RoutingHandler().get("/create/{output}", new HttpHandler() { - @Override - public void handleRequest(HttpServerExchange exchange) throws Exception { - if (exchange.isInIoThread()) { - exchange.dispatch(this); - return; - } - final Map> queryParameters = exchange.getQueryParameters(); - final Option output = getFirstOption(queryParameters, "output"); - final Option width = getIntOption(queryParameters, "width"); - final Option height = getIntOption(queryParameters, "height"); - final Option id = getIntOption(queryParameters, "id"); - - log.info("Output: {}", output); - log.info("Width: {}", width); - log.info("Height: {}", height); - log.info("Id: {}", id); - final Integer theId = id.getOrElse(() -> new Random().nextInt()); - final Labyrinth labyrinth = new Labyrinth(width.get(), height.get(), theId); - final LabyrinthServer.OutputType outputType = output.flatMap(LabyrinthServer.OutputType::ofString).get(); - final byte[] result = outputType.render(labyrinth); - exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, outputType.getContentType()); - exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(theId)); - exchange.getResponseHeaders() - .put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(width.get())); - exchange.getResponseHeaders() - .put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(height.get())); - exchange.getResponseSender().send(ByteBuffer.wrap(result)); - } - }).post("/render", new HttpHandler() { + public static final RoutingHandler r = new RoutingHandler().get("/create/{output}", new LabyrinthHttpHandler()).post("/render", new HttpHandler() { @Override public void handleRequest(final HttpServerExchange exchange) { if (exchange.isInIoThread()) { @@ -67,13 +33,13 @@ public class UndertowPlayground { }); @NonNull - private static Option getIntOption(@NonNull final Map> queryParams, + public static Option getIntOption(@NonNull final Map> queryParams, @NonNull final String paramName) { return getFirstOption(queryParams, paramName).toTry().map(Integer::parseInt).toOption(); } @NonNull - private static Option getFirstOption(@NonNull final Map> queryParams, + public static Option getFirstOption(@NonNull final Map> queryParams, @NonNull final String paramName) { return Option.of(queryParams.get(paramName)).map(Deque::peek).flatMap(Option::of); } From dd571a190ad10c9e59e554c99f32401688794ee2 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sun, 2 Apr 2023 20:16:34 +0200 Subject: [PATCH 04/15] Refactoring --- .../labyrinth/server/LabyrinthServer.java | 297 +++++++++--------- .../ch/fritteli/labyrinth/server/Main.java | 19 +- .../server/handler/AbstractHttpHandler.java | 28 ++ .../CreateHandler.java} | 14 +- .../LanyrinthRenderHandler.java | 17 + .../UndertowPlayground.java | 18 -- 6 files changed, 202 insertions(+), 191 deletions(-) create mode 100644 src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java rename src/main/java/ch/fritteli/labyrinth/server/{undertow_playground/LabyrinthHttpHandler.java => handler/CreateHandler.java} (83%) create mode 100644 src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LanyrinthRenderHandler.java diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java index 32bb27c..f6cdbd0 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java +++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java @@ -1,10 +1,11 @@ package ch.fritteli.labyrinth.server; -import ch.fritteli.labyrinth.generator.model.Labyrinth; -import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer; -import com.sun.net.httpserver.Headers; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpServer; +import ch.fritteli.labyrinth.server.handler.CreateHandler; +import ch.fritteli.labyrinth.server.undertow_playground.LanyrinthRenderHandler; +import io.undertow.Undertow; +import io.undertow.server.RoutingHandler; +import io.undertow.server.handlers.RedirectHandler; +import io.undertow.util.StatusCodes; import io.vavr.collection.HashMap; import io.vavr.collection.HashSet; import io.vavr.collection.Map; @@ -16,28 +17,13 @@ import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; -import java.io.IOException; -import java.io.OutputStream; import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.SynchronousQueue; -import java.util.concurrent.ThreadPoolExecutor; -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 = new ThreadPoolExecutor(0, - 1_000, - 5, - TimeUnit.SECONDS, - new SynchronousQueue<>() - ); + private final Undertow undertow; public static Option createAndStartServer() { final Option serverOption = Try.of(ServerConfig::init) @@ -51,147 +37,106 @@ public class LabyrinthServer { return serverOption; } - public LabyrinthServer(@NonNull final ServerConfig config) throws IOException { - this.httpServer = HttpServer.create(new InetSocketAddress(config.getAddress(), config.getPort()), 5); - this.httpServer.createContext("/", new StaticResourcesFileHandler(this.executorService)); - this.httpServer.createContext("/create", this::handleCreate); - this.httpServer.createContext("/render", this::handleRender); + public LabyrinthServer(@NonNull final ServerConfig config) { + log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort()); + final RoutingHandler routingHandler = new RoutingHandler().get("/", new RedirectHandler("/create/text")) + .get("/create", new RedirectHandler("/create/text")) + .get("/create/{output}", new CreateHandler()) + .post("/render", new LanyrinthRenderHandler()) + .setFallbackHandler(exchange -> { + exchange.setStatusCode(StatusCodes.NOT_FOUND) + .getResponseSender() + .send("Resource %s not found".formatted( + exchange.getRequestURI())); + }); + this.undertow = Undertow.builder() + .addHttpListener(config.getPort(), config.getAddress().getHostAddress()) + .setHandler(routingHandler) + .build(); } public void start() { Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); - this.httpServer.start(); - log.info("Listening on http://{}:{}", - this.httpServer.getAddress().getHostString(), - this.httpServer.getAddress().getPort() - ); - } - - private void handleCreate(HttpExchange exchange) { - 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, 7); - 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 byte[] render; - try { - final Labyrinth labyrinth = new Labyrinth(width, height, id); - 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.getMessage()).getBytes(StandardCharsets.UTF_8)); - responseBody.flush(); - return; - } - responseHeaders.add("Content-type", output.getContentType()); - if (output.isAttachment()) { - responseHeaders.add("Content-disposition", String.format( - "attachment; filename=\"labyrinth-%dx%d-%d.%s\"", - width, - height, - id, - output.getFileExtension() - )); - } - 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) { - this.executorService.submit(() -> { - 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(); - - final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); - - final OutputType output = exchange.getRequestHeaders() - .get("Accept") - .contains(OutputType.HTML.getContentType()) ? - 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(render); - responseBody.flush(); - } catch (Exception e) { - log.error("FSCK!", e); - } finally { - exchange.close(); - } - }); + this.undertow.start(); + Try.of(() -> (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress()) + .onFailure(e -> log.warn("Started server, unable to determine listeing address/port.")) + .forEach(address -> log.info("Listening on http://{}:{}", address.getHostString(), address.getPort())); } 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); - } + this.undertow.stop(); log.info("Server stopped."); } +// private void handleCreate(HttpExchange exchange) { +// 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, 7); +// 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 byte[] render; +// try { +// final Labyrinth labyrinth = new Labyrinth(width, height, id); +// 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.getMessage()).getBytes(StandardCharsets.UTF_8)); +// responseBody.flush(); +// return; +// } +// responseHeaders.add("Content-type", output.getContentType()); +// if (output.isAttachment()) { +// responseHeaders.add("Content-disposition", String.format( +// "attachment; filename=\"labyrinth-%dx%d-%d.%s\"", +// width, +// height, +// id, +// output.getFileExtension() +// )); +// } +// exchange.sendResponseHeaders(200, 0); +// final OutputStream responseBody = exchange.getResponseBody(); +// responseBody.write(render); +// responseBody.flush(); +// } catch (Exception e) { +// log.error("FSCK!", e); +// } finally { +// exchange.close(); +// } +// }); +// } + @NonNull private Map parseQueryString(@Nullable final String query) { if (query == null) { @@ -218,15 +163,59 @@ public class LabyrinthServer { return result; } + private RequestParameter normalizeParameterName(final String paramName) { + return RequestParameter.parseName(paramName).get(); + } + private T getOrDefault(@NonNull final Option input, @NonNull final Function mapper, @Nullable final T defaultValue) { return input.toTry().map(mapper).getOrElse(defaultValue); } - private RequestParameter normalizeParameterName(final String paramName) { - return RequestParameter.parseName(paramName).get(); - } +// private void handleRender(final HttpExchange exchange) { +// this.executorService.submit(() -> { +// 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(); +// +// final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); +// +// final OutputType output = exchange.getRequestHeaders() +// .get("Accept") +// .contains(OutputType.HTML.getContentType()) ? +// 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(render); +// responseBody.flush(); +// } catch (Exception e) { +// log.error("FSCK!", e); +// } finally { +// exchange.close(); +// } +// }); +// } private enum RequestParameter { WIDTH("w", "width"), diff --git a/src/main/java/ch/fritteli/labyrinth/server/Main.java b/src/main/java/ch/fritteli/labyrinth/server/Main.java index 21e0d6c..e11f627 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/Main.java +++ b/src/main/java/ch/fritteli/labyrinth/server/Main.java @@ -1,21 +1,18 @@ package ch.fritteli.labyrinth.server; -import ch.fritteli.labyrinth.server.undertow_playground.UndertowPlayground; -import io.undertow.Undertow; import lombok.extern.slf4j.Slf4j; @Slf4j public class Main { public static void main(String[] args) { -// LabyrinthServer.createAndStartServer() -// .onEmpty(() -> log.error("Failed to create server. Stopping.")); + LabyrinthServer.createAndStartServer().onEmpty(() -> log.error("Failed to create server. Stopping.")); - final ServerConfig config = ServerConfig.init(); - log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort()); - Undertow.builder() - .addHttpListener(config.getPort(), config.getAddress().getHostAddress()) - .setHandler(UndertowPlayground.r) - .build() - .start(); +// final ServerConfig config = ServerConfig.init(); +// log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort()); +// Undertow.builder() +// .addHttpListener(config.getPort(), config.getAddress().getHostAddress()) +// .setHandler(UndertowPlayground.r) +// .build() +// .start(); } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java new file mode 100644 index 0000000..5d44423 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java @@ -0,0 +1,28 @@ +package ch.fritteli.labyrinth.server.handler; + +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.StatusCodes; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class AbstractHttpHandler implements HttpHandler { + @Override + public final void handleRequest(final HttpServerExchange exchange) { + if (exchange.isInIoThread()) { + exchange.dispatch(this); + return; + } + try { + this.handle(exchange); + } catch (@NonNull final Exception e) { + log.error("Error handling request", e); + exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) + .getResponseSender() + .send(StatusCodes.INTERNAL_SERVER_ERROR_STRING); + } + } + + protected abstract void handle(@NonNull final HttpServerExchange exchange) throws Exception; +} diff --git a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LabyrinthHttpHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java similarity index 83% rename from src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LabyrinthHttpHandler.java rename to src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java index 4cc6658..c1a023f 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LabyrinthHttpHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java @@ -1,12 +1,14 @@ -package ch.fritteli.labyrinth.server.undertow_playground; +package ch.fritteli.labyrinth.server.handler; import ch.fritteli.labyrinth.generator.model.Labyrinth; import ch.fritteli.labyrinth.server.OutputType; -import io.undertow.server.HttpHandler; +import ch.fritteli.labyrinth.server.handler.AbstractHttpHandler; +import ch.fritteli.labyrinth.server.undertow_playground.UndertowPlayground; import io.undertow.server.HttpServerExchange; import io.undertow.util.Headers; import io.undertow.util.HttpString; import io.vavr.control.Option; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import java.nio.ByteBuffer; @@ -15,13 +17,9 @@ import java.util.Map; import java.util.Random; @Slf4j -class LabyrinthHttpHandler implements HttpHandler { +public class CreateHandler extends AbstractHttpHandler { @Override - public void handleRequest(HttpServerExchange exchange) throws Exception { - if (exchange.isInIoThread()) { - exchange.dispatch(this); - return; - } + protected void handle(@NonNull final HttpServerExchange exchange) throws Exception { final Map> queryParameters = exchange.getQueryParameters(); final Option output = UndertowPlayground.getFirstOption(queryParameters, "output"); final Option width = UndertowPlayground.getIntOption(queryParameters, "width"); diff --git a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LanyrinthRenderHandler.java b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LanyrinthRenderHandler.java new file mode 100644 index 0000000..b176f09 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LanyrinthRenderHandler.java @@ -0,0 +1,17 @@ +package ch.fritteli.labyrinth.server.undertow_playground; + +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.StatusCodes; + +public class LanyrinthRenderHandler implements HttpHandler { + @Override + public void handleRequest(final HttpServerExchange exchange) { + if (exchange.isInIoThread()) { + exchange.dispatch(this); + return; + } + exchange.setStatusCode(StatusCodes.NOT_IMPLEMENTED); + exchange.getResponseSender().send("Rendering binary data is not implemented yet."); + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java index 1ef547c..1caf698 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java +++ b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java @@ -14,24 +14,6 @@ import java.util.Map; @Slf4j public class UndertowPlayground { - public static final RoutingHandler r = new RoutingHandler().get("/create/{output}", new LabyrinthHttpHandler()).post("/render", new HttpHandler() { - @Override - public void handleRequest(final HttpServerExchange exchange) { - if (exchange.isInIoThread()) { - exchange.dispatch(this); - return; - } - exchange.getResponseSender().send("TODO: read body, render stuff"); - } - }).setFallbackHandler(new HttpHandler() { - @Override - public void handleRequest(HttpServerExchange exchange) throws Exception { - exchange.getResponseSender().send("Request: " + exchange.getRequestURI()); - final HeaderValues strings = exchange.getRequestHeaders().get(Headers.ACCEPT); - strings.peekFirst(); - } - }); - @NonNull public static Option getIntOption(@NonNull final Map> queryParams, @NonNull final String paramName) { From 17afb42715c639d0cbdb8d72849a6a5a6638036f Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sun, 2 Apr 2023 20:18:21 +0200 Subject: [PATCH 05/15] Refactoring --- .../ch/fritteli/labyrinth/server/LabyrinthServer.java | 10 ++-------- src/main/java/ch/fritteli/labyrinth/server/Main.java | 10 +--------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java index f6cdbd0..caa07b9 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java +++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java @@ -25,14 +25,8 @@ public class LabyrinthServer { @NonNull private final Undertow undertow; - public static Option createAndStartServer() { - final Option serverOption = Try.of(ServerConfig::init) - .mapTry(LabyrinthServer::new) - .onFailure(cause -> log.error( - "Failed to create LabyrinthServer.", - cause - )) - .toOption(); + public static Try createAndStartServer() { + final Try serverOption = Try.of(ServerConfig::init).mapTry(LabyrinthServer::new); serverOption.forEach(LabyrinthServer::start); return serverOption; } diff --git a/src/main/java/ch/fritteli/labyrinth/server/Main.java b/src/main/java/ch/fritteli/labyrinth/server/Main.java index e11f627..fbdc645 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/Main.java +++ b/src/main/java/ch/fritteli/labyrinth/server/Main.java @@ -5,14 +5,6 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class Main { public static void main(String[] args) { - LabyrinthServer.createAndStartServer().onEmpty(() -> log.error("Failed to create server. Stopping.")); - -// final ServerConfig config = ServerConfig.init(); -// log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort()); -// Undertow.builder() -// .addHttpListener(config.getPort(), config.getAddress().getHostAddress()) -// .setHandler(UndertowPlayground.r) -// .build() -// .start(); + LabyrinthServer.createAndStartServer().onFailure(e -> log.error("Failed to create server. Stopping.", e)); } } From f99efcdad7c96347527d68f424cae5c42864d751 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sun, 2 Apr 2023 20:19:37 +0200 Subject: [PATCH 06/15] Refactoring --- .../labyrinth/server/LabyrinthServer.java | 116 ++++++++---------- 1 file changed, 54 insertions(+), 62 deletions(-) diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java index caa07b9..8ed349f 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java +++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java @@ -6,19 +6,11 @@ import io.undertow.Undertow; import io.undertow.server.RoutingHandler; import io.undertow.server.handlers.RedirectHandler; import io.undertow.util.StatusCodes; -import io.vavr.collection.HashMap; -import io.vavr.collection.HashSet; -import io.vavr.collection.Map; -import io.vavr.collection.Set; -import io.vavr.collection.Stream; -import io.vavr.control.Option; import io.vavr.control.Try; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.Nullable; import java.net.InetSocketAddress; -import java.util.function.Function; @Slf4j public class LabyrinthServer { @@ -31,7 +23,7 @@ public class LabyrinthServer { return serverOption; } - public LabyrinthServer(@NonNull final ServerConfig config) { + private LabyrinthServer(@NonNull final ServerConfig config) { log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort()); final RoutingHandler routingHandler = new RoutingHandler().get("/", new RedirectHandler("/create/text")) .get("/create", new RedirectHandler("/create/text")) @@ -49,7 +41,7 @@ public class LabyrinthServer { .build(); } - public void start() { + private void start() { Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); this.undertow.start(); Try.of(() -> (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress()) @@ -57,7 +49,7 @@ public class LabyrinthServer { .forEach(address -> log.info("Listening on http://{}:{}", address.getHostString(), address.getPort())); } - public void stop() { + private void stop() { log.info("Stopping server ..."); this.undertow.stop(); log.info("Server stopped."); @@ -131,41 +123,41 @@ public class LabyrinthServer { // }); // } - @NonNull - private Map parseQueryString(@Nullable final String query) { - if (query == null) { - return HashMap.empty(); - } - HashMap result = HashMap.empty(); - final String[] parts = query.split("&"); - for (final String part : parts) { - final int split = part.indexOf('='); - if (split == -1) { - final Try tryKey = Try.of(() -> this.normalizeParameterName(part)); - if (tryKey.isSuccess()) { - result = result.put(tryKey.get(), null); - } - } else { - final String key = part.substring(0, split); - final String value = part.substring(split + 1); - final Try tryKey = Try.of(() -> this.normalizeParameterName(key)); - if (tryKey.isSuccess()) { - result = result.put(tryKey.get(), value); - } - } - } - return result; - } +// @NonNull +// private Map parseQueryString(@Nullable final String query) { +// if (query == null) { +// return HashMap.empty(); +// } +// HashMap result = HashMap.empty(); +// final String[] parts = query.split("&"); +// for (final String part : parts) { +// final int split = part.indexOf('='); +// if (split == -1) { +// final Try tryKey = Try.of(() -> this.normalizeParameterName(part)); +// if (tryKey.isSuccess()) { +// result = result.put(tryKey.get(), null); +// } +// } else { +// final String key = part.substring(0, split); +// final String value = part.substring(split + 1); +// final Try tryKey = Try.of(() -> this.normalizeParameterName(key)); +// if (tryKey.isSuccess()) { +// result = result.put(tryKey.get(), value); +// } +// } +// } +// return result; +// } - private RequestParameter normalizeParameterName(final String paramName) { - return RequestParameter.parseName(paramName).get(); - } +// private RequestParameter normalizeParameterName(final String paramName) { +// return RequestParameter.parseName(paramName).get(); +// } - private T getOrDefault(@NonNull final Option input, - @NonNull final Function mapper, - @Nullable final T defaultValue) { - return input.toTry().map(mapper).getOrElse(defaultValue); - } +// private T getOrDefault(@NonNull final Option input, +// @NonNull final Function mapper, +// @Nullable final T defaultValue) { +// return input.toTry().map(mapper).getOrElse(defaultValue); +// } // private void handleRender(final HttpExchange exchange) { // this.executorService.submit(() -> { @@ -211,22 +203,22 @@ public class LabyrinthServer { // }); // } - private enum RequestParameter { - WIDTH("w", "width"), - HEIGHT("h", "height"), - ID("i", "id"), - OUTPUT("o", "output"); - private final Set names; - - RequestParameter(@NonNull final String... names) { - this.names = HashSet.of(names); - } - - static Option parseName(@Nullable final String name) { - if (name == null) { - return Option.none(); - } - return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); - } - } +// private enum RequestParameter { +// WIDTH("w", "width"), +// HEIGHT("h", "height"), +// ID("i", "id"), +// OUTPUT("o", "output"); +// private final Set names; +// +// RequestParameter(@NonNull final String... names) { +// this.names = HashSet.of(names); +// } +// +// static Option parseName(@Nullable final String name) { +// if (name == null) { +// return Option.none(); +// } +// return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); +// } +// } } From 2e3e11fbb02a6622e41e93ae3e112bab44469ba2 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Mon, 3 Apr 2023 22:49:23 +0200 Subject: [PATCH 07/15] Cleanup, refactoring. --- .../labyrinth/server/LabyrinthServer.java | 40 +++---- .../fritteli/labyrinth/server/OutputType.java | 20 ++-- .../labyrinth/server/ServerConfig.java | 12 +- .../server/StaticResourcesFileHandler.java | 109 ------------------ .../server/handler/CreateHandler.java | 91 +++++++++++---- .../LanyrinthRenderHandler.java | 2 +- .../UndertowPlayground.java | 28 ----- src/main/resources/logback.xml | 2 +- 8 files changed, 105 insertions(+), 199 deletions(-) delete mode 100644 src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java rename src/main/java/ch/fritteli/labyrinth/server/{undertow_playground => handler}/LanyrinthRenderHandler.java (90%) delete mode 100644 src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java index 8ed349f..14d162d 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java +++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java @@ -1,10 +1,9 @@ package ch.fritteli.labyrinth.server; import ch.fritteli.labyrinth.server.handler.CreateHandler; -import ch.fritteli.labyrinth.server.undertow_playground.LanyrinthRenderHandler; +import ch.fritteli.labyrinth.server.handler.LanyrinthRenderHandler; import io.undertow.Undertow; import io.undertow.server.RoutingHandler; -import io.undertow.server.handlers.RedirectHandler; import io.undertow.util.StatusCodes; import io.vavr.control.Try; import lombok.NonNull; @@ -17,36 +16,33 @@ public class LabyrinthServer { @NonNull private final Undertow undertow; + private LabyrinthServer(@NonNull final ServerConfig config) { + log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort()); + final RoutingHandler routingHandler = new RoutingHandler().get(CreateHandler.PATH_TEMPLATE, new CreateHandler()) + .post("/render", new LanyrinthRenderHandler()) + .setFallbackHandler(exchange -> exchange.setStatusCode( + StatusCodes.NOT_FOUND) + .getResponseSender() + .send("Resource %s not found".formatted( + exchange.getRequestURI()))); + this.undertow = Undertow.builder() + .addHttpListener(config.getPort(), config.getAddress().getHostAddress()) + .setHandler(routingHandler) + .build(); + } + public static Try createAndStartServer() { final Try serverOption = Try.of(ServerConfig::init).mapTry(LabyrinthServer::new); serverOption.forEach(LabyrinthServer::start); return serverOption; } - private LabyrinthServer(@NonNull final ServerConfig config) { - log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort()); - final RoutingHandler routingHandler = new RoutingHandler().get("/", new RedirectHandler("/create/text")) - .get("/create", new RedirectHandler("/create/text")) - .get("/create/{output}", new CreateHandler()) - .post("/render", new LanyrinthRenderHandler()) - .setFallbackHandler(exchange -> { - exchange.setStatusCode(StatusCodes.NOT_FOUND) - .getResponseSender() - .send("Resource %s not found".formatted( - exchange.getRequestURI())); - }); - this.undertow = Undertow.builder() - .addHttpListener(config.getPort(), config.getAddress().getHostAddress()) - .setHandler(routingHandler) - .build(); - } - private void start() { Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); this.undertow.start(); Try.of(() -> (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress()) - .onFailure(e -> log.warn("Started server, unable to determine listeing address/port.")) - .forEach(address -> log.info("Listening on http://{}:{}", address.getHostString(), address.getPort())); + .onFailure(e -> log.warn("Started server, unable to determine listeing address/port.")) + .forEach(address -> log.info("Listening on http://{}:{}", address.getHostString(), address.getPort())); } private void stop() { diff --git a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java index e627968..29acf8c 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java +++ b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java @@ -17,18 +17,18 @@ import java.util.function.Function; public enum OutputType { TEXT_PLAIN("text/plain; charset=UTF-8", - labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), - "txt", - false, - "t", - "text" + labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), + "txt", + false, + "t", + "text" ), HTML("text/html", - labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), - "html", - false, - "h", - "html" + labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), + "html", + false, + "h", + "html" ), PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", false, "p", "pdf"), PDFFILE("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", true, "f", "pdffile"), diff --git a/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java b/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java index fc161dd..2ac4426 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java +++ b/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java @@ -29,7 +29,7 @@ public class ServerConfig { @NonNull private static InetAddress validateAddress(@Nullable final String address) { return Try.of(() -> InetAddress.getByName(address)) - .getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause)); + .getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause)); } private static int validatePort(final int port) { @@ -53,10 +53,10 @@ public class ServerConfig { return 0; } return Try.of(() -> Integer.valueOf(portString)) - .map(ServerConfig::validatePort) - .getOrElseThrow(cause -> new ConfigurationException( - "Failed to parse port specified in system property '" + SYSPROP_PORT + "': " + portString, - cause - )); + .map(ServerConfig::validatePort) + .getOrElseThrow(cause -> new ConfigurationException( + "Failed to parse port specified in system property '" + SYSPROP_PORT + "': " + portString, + cause + )); } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java b/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java deleted file mode 100644 index e6911e8..0000000 --- a/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java +++ /dev/null @@ -1,109 +0,0 @@ -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; - log.debug("Created {}", this.getClass().getSimpleName()); - } - - @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(); - } - }); - } - - 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(); - } - - @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; - } - - @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]; - } - final byte[] response = IOUtils.toByteArray(stream); - log.debug("Sending reply; {} bytes", response.length); - return response; - } -} diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java index c1a023f..8525918 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java @@ -2,14 +2,18 @@ package ch.fritteli.labyrinth.server.handler; import ch.fritteli.labyrinth.generator.model.Labyrinth; import ch.fritteli.labyrinth.server.OutputType; -import ch.fritteli.labyrinth.server.handler.AbstractHttpHandler; -import ch.fritteli.labyrinth.server.undertow_playground.UndertowPlayground; import io.undertow.server.HttpServerExchange; import io.undertow.util.Headers; import io.undertow.util.HttpString; +import io.undertow.util.StatusCodes; +import io.vavr.Tuple; +import io.vavr.Tuple2; +import io.vavr.collection.*; import io.vavr.control.Option; +import io.vavr.control.Try; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.Nullable; import java.nio.ByteBuffer; import java.util.Deque; @@ -18,26 +22,69 @@ import java.util.Random; @Slf4j public class CreateHandler extends AbstractHttpHandler { - @Override - protected void handle(@NonNull final HttpServerExchange exchange) throws Exception { - final Map> queryParameters = exchange.getQueryParameters(); - final Option output = UndertowPlayground.getFirstOption(queryParameters, "output"); - final Option width = UndertowPlayground.getIntOption(queryParameters, "width"); - final Option height = UndertowPlayground.getIntOption(queryParameters, "height"); - final Option id = UndertowPlayground.getIntOption(queryParameters, "id"); + public static final String PATH_TEMPLATE = "/create/{output}"; - log.info("Output: {}", output); - log.info("Width: {}", width); - log.info("Height: {}", height); - log.info("Id: {}", id); - final Integer theId = id.getOrElse(() -> new Random().nextInt()); - final Labyrinth labyrinth = new Labyrinth(width.get(), height.get(), theId); - final OutputType outputType = output.flatMap(OutputType::ofString).get(); - final byte[] result = outputType.render(labyrinth); - exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, outputType.getContentType()); - exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(theId)); - exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(width.get())); - exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(height.get())); - exchange.getResponseSender().send(ByteBuffer.wrap(result)); + @Override + protected void handle(@NonNull final HttpServerExchange exchange) { + this.createLabyrinthFromRequestParameters(exchange.getQueryParameters()) + .onFailure(e -> { + exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); + exchange.setReasonPhrase(e.getMessage()); + }).forEach(tuple -> { + final OutputType outputType = tuple._1(); + final Labyrinth labyrinth = tuple._2(); + final byte[] bytes = outputType.render(labyrinth); + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, outputType.getContentType()); + exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(labyrinth.getRandomSeed())); + exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth())); + exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(labyrinth.getHeight())); + exchange.getResponseSender().send(ByteBuffer.wrap(bytes)); + }); + } + + private @NonNull Try> createLabyrinthFromRequestParameters(final Map> queryParameters) { + return new ParametersToLabyrinthExtractor(queryParameters).createLabyrinth(); + } + + private enum RequestParameter { + WIDTH("w", "width"), HEIGHT("h", "height"), ID("i", "id"), OUTPUT("o", "output"); + private final Set names; + + RequestParameter(@NonNull final String... names) { + this.names = HashSet.of(names); + } + + static Option parseName(@Nullable final String name) { + if (name == null) { + return Option.none(); + } + return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); + } + } + + private static class ParametersToLabyrinthExtractor { + @NonNull + private final Multimap queryParameters; + + ParametersToLabyrinthExtractor(@NonNull final Map> queryParameters) { + this.queryParameters = HashMap.ofAll(queryParameters).foldLeft(HashMultimap.withSet().empty(), (map, tuple) -> { + final String key = tuple._1(); + return Stream.ofAll(tuple._2()).foldLeft(map, (m, value) -> m.put(key, value)); + }); + } + + @NonNull Try> createLabyrinth() { + final Option output = getParameterValues(RequestParameter.OUTPUT).foldLeft(Option.none(), (type, param) -> type.orElse(() -> OutputType.ofString(param))); + final Option width = getParameterValues(RequestParameter.WIDTH).foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption())); + final Option height = getParameterValues(RequestParameter.HEIGHT).foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption())); + final Option id = getParameterValues(RequestParameter.ID).foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Long.parseLong(param)).toOption())); + + return Try.of(() -> Tuple.of(output.get(), new Labyrinth(width.get(), height.get(), id.getOrElse(() -> new Random().nextLong())))); + } + + @NonNull + private Stream getParameterValues(@NonNull final RequestParameter parameter) { + return parameter.names.toStream().flatMap(name -> Stream.ofAll(this.queryParameters.getOrElse(name, List.empty()))); + } } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LanyrinthRenderHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/LanyrinthRenderHandler.java similarity index 90% rename from src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LanyrinthRenderHandler.java rename to src/main/java/ch/fritteli/labyrinth/server/handler/LanyrinthRenderHandler.java index b176f09..e1b17fd 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LanyrinthRenderHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/LanyrinthRenderHandler.java @@ -1,4 +1,4 @@ -package ch.fritteli.labyrinth.server.undertow_playground; +package ch.fritteli.labyrinth.server.handler; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; diff --git a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java deleted file mode 100644 index 1caf698..0000000 --- a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java +++ /dev/null @@ -1,28 +0,0 @@ -package ch.fritteli.labyrinth.server.undertow_playground; - -import io.undertow.server.HttpHandler; -import io.undertow.server.HttpServerExchange; -import io.undertow.server.RoutingHandler; -import io.undertow.util.HeaderValues; -import io.undertow.util.Headers; -import io.vavr.control.Option; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; - -import java.util.Deque; -import java.util.Map; - -@Slf4j -public class UndertowPlayground { - @NonNull - public static Option getIntOption(@NonNull final Map> queryParams, - @NonNull final String paramName) { - return getFirstOption(queryParams, paramName).toTry().map(Integer::parseInt).toOption(); - } - - @NonNull - public static Option getFirstOption(@NonNull final Map> queryParams, - @NonNull final String paramName) { - return Option.of(queryParams.get(paramName)).map(Deque::peek).flatMap(Option::of); - } -} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index f484fc8..b2ea4fb 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -13,5 +13,5 @@ - + From 4dc15acf0cf1219cec8c528014cca8e9a6fa55e5 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Tue, 4 Apr 2023 00:33:32 +0200 Subject: [PATCH 08/15] Cleanup, refactoring. --- pom.xml | 2 +- .../labyrinth/server/LabyrinthServer.java | 164 +++--------------- .../ch/fritteli/labyrinth/server/Main.java | 3 +- .../fritteli/labyrinth/server/OutputType.java | 54 ++++-- .../labyrinth/server/ServerConfig.java | 35 ++-- .../server/handler/AbstractHttpHandler.java | 30 ++-- .../server/handler/CreateHandler.java | 56 ++++-- ...hRenderHandler.java => RenderHandler.java} | 2 +- src/main/resources/logback.xml | 2 +- 9 files changed, 142 insertions(+), 206 deletions(-) rename src/main/java/ch/fritteli/labyrinth/server/handler/{LanyrinthRenderHandler.java => RenderHandler.java} (89%) diff --git a/pom.xml b/pom.xml index f99aeac..bf06567 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,7 @@ io.undertow undertow-core - 2.2.22.Final + 2.3.5.Final org.junit.jupiter diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java index 14d162d..09ed5fc 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java +++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java @@ -1,48 +1,51 @@ package ch.fritteli.labyrinth.server; import ch.fritteli.labyrinth.server.handler.CreateHandler; -import ch.fritteli.labyrinth.server.handler.LanyrinthRenderHandler; +import ch.fritteli.labyrinth.server.handler.RenderHandler; import io.undertow.Undertow; import io.undertow.server.RoutingHandler; -import io.undertow.util.StatusCodes; import io.vavr.control.Try; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; -import java.net.InetSocketAddress; - @Slf4j public class LabyrinthServer { + @NonNull + private final ServerConfig config; @NonNull private final Undertow undertow; private LabyrinthServer(@NonNull final ServerConfig config) { - log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort()); - final RoutingHandler routingHandler = new RoutingHandler().get(CreateHandler.PATH_TEMPLATE, new CreateHandler()) - .post("/render", new LanyrinthRenderHandler()) - .setFallbackHandler(exchange -> exchange.setStatusCode( - StatusCodes.NOT_FOUND) - .getResponseSender() - .send("Resource %s not found".formatted( - exchange.getRequestURI()))); + this.config = config; + final String hostAddress = config.getAddress().getHostAddress(); + final int port = config.getPort(); + log.info("Starting Server at http://{}:{}/", hostAddress, port); + final RoutingHandler routingHandler = new RoutingHandler() + .get(CreateHandler.PATH_TEMPLATE, new CreateHandler()) + .post("/render", new RenderHandler()); + this.undertow = Undertow.builder() - .addHttpListener(config.getPort(), config.getAddress().getHostAddress()) + .addHttpListener(port, hostAddress) .setHandler(routingHandler) .build(); } + @NonNull public static Try createAndStartServer() { - final Try serverOption = Try.of(ServerConfig::init).mapTry(LabyrinthServer::new); - serverOption.forEach(LabyrinthServer::start); - return serverOption; + return Try.of(ServerConfig::init) + .flatMapTry(LabyrinthServer::createAndStartServer); + } + + @NonNull + public static Try createAndStartServer(@NonNull final ServerConfig config) { + return Try.of(() -> new LabyrinthServer(config)) + .peek(LabyrinthServer::start); } private void start() { Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); this.undertow.start(); - Try.of(() -> (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress()) - .onFailure(e -> log.warn("Started server, unable to determine listeing address/port.")) - .forEach(address -> log.info("Listening on http://{}:{}", address.getHostString(), address.getPort())); + log.info("Listeing on http://{}:{}", this.config.getAddress().getHostAddress(), this.config.getPort()); } private void stop() { @@ -51,110 +54,6 @@ public class LabyrinthServer { log.info("Server stopped."); } -// private void handleCreate(HttpExchange exchange) { -// 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, 7); -// 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 byte[] render; -// try { -// final Labyrinth labyrinth = new Labyrinth(width, height, id); -// 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.getMessage()).getBytes(StandardCharsets.UTF_8)); -// responseBody.flush(); -// return; -// } -// responseHeaders.add("Content-type", output.getContentType()); -// if (output.isAttachment()) { -// responseHeaders.add("Content-disposition", String.format( -// "attachment; filename=\"labyrinth-%dx%d-%d.%s\"", -// width, -// height, -// id, -// output.getFileExtension() -// )); -// } -// exchange.sendResponseHeaders(200, 0); -// final OutputStream responseBody = exchange.getResponseBody(); -// responseBody.write(render); -// responseBody.flush(); -// } catch (Exception e) { -// log.error("FSCK!", e); -// } finally { -// exchange.close(); -// } -// }); -// } - -// @NonNull -// private Map parseQueryString(@Nullable final String query) { -// if (query == null) { -// return HashMap.empty(); -// } -// HashMap result = HashMap.empty(); -// final String[] parts = query.split("&"); -// for (final String part : parts) { -// final int split = part.indexOf('='); -// if (split == -1) { -// final Try tryKey = Try.of(() -> this.normalizeParameterName(part)); -// if (tryKey.isSuccess()) { -// result = result.put(tryKey.get(), null); -// } -// } else { -// final String key = part.substring(0, split); -// final String value = part.substring(split + 1); -// final Try tryKey = Try.of(() -> this.normalizeParameterName(key)); -// if (tryKey.isSuccess()) { -// result = result.put(tryKey.get(), value); -// } -// } -// } -// return result; -// } - -// private RequestParameter normalizeParameterName(final String paramName) { -// return RequestParameter.parseName(paramName).get(); -// } - -// private T getOrDefault(@NonNull final Option input, -// @NonNull final Function mapper, -// @Nullable final T defaultValue) { -// return input.toTry().map(mapper).getOrElse(defaultValue); -// } - // private void handleRender(final HttpExchange exchange) { // this.executorService.submit(() -> { // try { @@ -198,23 +97,4 @@ public class LabyrinthServer { // } // }); // } - -// private enum RequestParameter { -// WIDTH("w", "width"), -// HEIGHT("h", "height"), -// ID("i", "id"), -// OUTPUT("o", "output"); -// private final Set names; -// -// RequestParameter(@NonNull final String... names) { -// this.names = HashSet.of(names); -// } -// -// static Option parseName(@Nullable final String name) { -// if (name == null) { -// return Option.none(); -// } -// return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); -// } -// } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/Main.java b/src/main/java/ch/fritteli/labyrinth/server/Main.java index fbdc645..d8c5bfa 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/Main.java +++ b/src/main/java/ch/fritteli/labyrinth/server/Main.java @@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class Main { public static void main(String[] args) { - LabyrinthServer.createAndStartServer().onFailure(e -> log.error("Failed to create server. Stopping.", e)); + LabyrinthServer.createAndStartServer() + .onFailure(e -> log.error("Failed to create server. Stopping.", e)); } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java index 29acf8c..60ff74f 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java +++ b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java @@ -17,38 +17,51 @@ import java.util.function.Function; public enum OutputType { TEXT_PLAIN("text/plain; charset=UTF-8", - labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), "txt", + labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), false, "t", - "text" - ), + "text"), HTML("text/html", - labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), "html", + labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), false, "h", - "html" - ), - PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", false, "p", "pdf"), - PDFFILE("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", true, "f", "pdffile"), - BINARY("application/octet-stream", SerializerDeserializer::serialize, "laby", true, "b", "binary"); + "html"), + PDF("application/pdf", + "pdf", + labyrinth -> PDFRenderer.newInstance().render(labyrinth), + false, + "p", + "pdf"), + PDFFILE("application/pdf", + "pdf", + labyrinth -> PDFRenderer.newInstance().render(labyrinth), + true, + "f", + "pdffile"), + BINARY("application/octet-stream", + "laby", + SerializerDeserializer::serialize, + true, + "b", + "binary"); @Getter @NonNull private final String contentType; + @Getter @NonNull - private final List names; + private final String fileExtension; @NonNull private final Function render; @Getter private final boolean attachment; - @Getter @NonNull - private final String fileExtension; + private final List names; OutputType(@NonNull final String contentType, - @NonNull final Function render, @NonNull final String fileExtension, + @NonNull final Function render, final boolean attachment, @NonNull final String... names) { this.contentType = contentType; @@ -58,20 +71,23 @@ public enum OutputType { this.names = List.of(names); } + @NonNull public static Option ofString(@Nullable final String name) { - if (name == null) { - return Option.none(); - } - final String nameLC = name.toLowerCase(); - return Stream.of(values()).find(param -> param.names.contains(nameLC)); + return Option.of(name) + .map(String::toLowerCase) + .flatMap(nameLC -> Stream.of(values()) + .find(param -> param.names.contains(nameLC))); + } + @NonNull @Override public String toString() { return this.names.last(); } - public byte[] render(@NonNull final Labyrinth labyrinth) { + @NonNull + public byte[] render(@NonNull final Labyrinth labyrinth) throws Exception { return this.render.apply(labyrinth); } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java b/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java index 2ac4426..196b2c5 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java +++ b/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java @@ -17,7 +17,8 @@ public class ServerConfig { public static final String SYSPROP_HOST = "fritteli.labyrinth.server.host"; public static final String SYSPROP_PORT = "fritteli.labyrinth.server.port"; - @NonNull InetAddress address; + @NonNull + InetAddress address; int port; public ServerConfig(@Nullable final String address, final int port) throws ConfigurationException { @@ -26,19 +27,6 @@ public class ServerConfig { log.debug("host={}, port={}", this.address, this.port); } - @NonNull - private static InetAddress validateAddress(@Nullable final String address) { - return Try.of(() -> InetAddress.getByName(address)) - .getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause)); - } - - private static int validatePort(final int port) { - if (port < 0 || port > 0xFFFF) { - throw new ConfigurationException("Port out of range (0..65535): " + port); - } - return port; - } - @NonNull public static ServerConfig init() throws ConfigurationException { final String host = System.getProperty(SYSPROP_HOST); @@ -47,15 +35,30 @@ public class ServerConfig { return new ServerConfig(host, port); } + @NonNull + private static InetAddress validateAddress(@Nullable final String address) { + return Try.of(() -> InetAddress.getByName(address)) + .getOrElseThrow(cause -> new ConfigurationException( + "Invalid hostname/address: %s".formatted(address), + cause + )); + } + + private static int validatePort(final int port) { + if (port < 0 || port > 0xFFFF) { + throw new ConfigurationException("Port out of range (0..65535): %s".formatted(port)); + } + return port; + } + private static int validatePort(@Nullable final String portString) { if (portString == null) { log.info("No port configured; using default."); return 0; } return Try.of(() -> Integer.valueOf(portString)) - .map(ServerConfig::validatePort) .getOrElseThrow(cause -> new ConfigurationException( - "Failed to parse port specified in system property '" + SYSPROP_PORT + "': " + portString, + "Failed to parse port specified in system property '%s': %s".formatted(SYSPROP_PORT, portString), cause )); } diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java index 5d44423..384ecc2 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java @@ -5,22 +5,28 @@ import io.undertow.server.HttpServerExchange; import io.undertow.util.StatusCodes; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; + +import java.util.UUID; @Slf4j public abstract class AbstractHttpHandler implements HttpHandler { @Override - public final void handleRequest(final HttpServerExchange exchange) { - if (exchange.isInIoThread()) { - exchange.dispatch(this); - return; - } - try { - this.handle(exchange); - } catch (@NonNull final Exception e) { - log.error("Error handling request", e); - exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) - .getResponseSender() - .send(StatusCodes.INTERNAL_SERVER_ERROR_STRING); + public final void handleRequest(@NonNull final HttpServerExchange exchange) { + try (final MDC.MDCCloseable mdc = MDC.putCloseable("correlationId", UUID.randomUUID().toString())) { + if (exchange.isInIoThread()) { + log.debug("Dispatching request"); + exchange.dispatch(this); + return; + } + try { + this.handle(exchange); + } catch (@NonNull final Exception e) { + log.error("Error handling request", e); + exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) + .getResponseSender() + .send(StatusCodes.INTERNAL_SERVER_ERROR_STRING); + } } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java index 8525918..47da5f5 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java @@ -14,11 +14,15 @@ import io.vavr.control.Try; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; +import org.slf4j.MDC; import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Deque; import java.util.Map; import java.util.Random; +import java.util.UUID; @Slf4j public class CreateHandler extends AbstractHttpHandler { @@ -26,19 +30,34 @@ public class CreateHandler extends AbstractHttpHandler { @Override protected void handle(@NonNull final HttpServerExchange exchange) { + final Instant start = Instant.now(); + log.debug("Handling create request"); this.createLabyrinthFromRequestParameters(exchange.getQueryParameters()) .onFailure(e -> { + log.error("Error creating Labyrinth from request", e); exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); - exchange.setReasonPhrase(e.getMessage()); - }).forEach(tuple -> { + exchange.setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING); + }) + .forEach(tuple -> { final OutputType outputType = tuple._1(); final Labyrinth labyrinth = tuple._2(); - final byte[] bytes = outputType.render(labyrinth); - exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, outputType.getContentType()); - exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(labyrinth.getRandomSeed())); - exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth())); - exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(labyrinth.getHeight())); + final byte[] bytes; + try { + bytes = outputType.render(labyrinth); + } catch (@NonNull final Exception e) { + log.error("Error rendering Labyrinth", e); + exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); + exchange.setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING); + return; + } + exchange.getResponseHeaders() + .put(Headers.CONTENT_TYPE, outputType.getContentType()) + .put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(labyrinth.getRandomSeed())) + .put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth())) + .put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(labyrinth.getHeight())) + .put(HttpString.tryFromString("X-Labyrinth-Generation-Duration-millis"), String.valueOf(start.until(Instant.now(), ChronoUnit.MILLIS))); exchange.getResponseSender().send(ByteBuffer.wrap(bytes)); + log.debug("Create request handled."); }); } @@ -73,13 +92,24 @@ public class CreateHandler extends AbstractHttpHandler { }); } - @NonNull Try> createLabyrinth() { - final Option output = getParameterValues(RequestParameter.OUTPUT).foldLeft(Option.none(), (type, param) -> type.orElse(() -> OutputType.ofString(param))); - final Option width = getParameterValues(RequestParameter.WIDTH).foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption())); - final Option height = getParameterValues(RequestParameter.HEIGHT).foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption())); - final Option id = getParameterValues(RequestParameter.ID).foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Long.parseLong(param)).toOption())); + @NonNull + Try> createLabyrinth() { + final Option output = getParameterValues(RequestParameter.OUTPUT) + .foldLeft(Option.none(), (type, param) -> type.orElse(() -> OutputType.ofString(param))); + final Option width = getParameterValues(RequestParameter.WIDTH) + .foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption())); + final Option height = getParameterValues(RequestParameter.HEIGHT) + .foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption())); + final Option id = getParameterValues(RequestParameter.ID) + .foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Long.parseLong(param)).toOption())); - return Try.of(() -> Tuple.of(output.get(), new Labyrinth(width.get(), height.get(), id.getOrElse(() -> new Random().nextLong())))); + return Try.of(() -> { + final OutputType t1 = output.get(); + final Integer width1 = width.get(); + final Integer height1 = height.get(); + final Long orElse = id.getOrElse(() -> new Random().nextLong()); + return Tuple.of(t1, new Labyrinth(width1, height1, orElse)); + }); } @NonNull diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/LanyrinthRenderHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java similarity index 89% rename from src/main/java/ch/fritteli/labyrinth/server/handler/LanyrinthRenderHandler.java rename to src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java index e1b17fd..a440b90 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/LanyrinthRenderHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java @@ -4,7 +4,7 @@ import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.util.StatusCodes; -public class LanyrinthRenderHandler implements HttpHandler { +public class RenderHandler implements HttpHandler { @Override public void handleRequest(final HttpServerExchange exchange) { if (exchange.isInIoThread()) { diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index b2ea4fb..6cec730 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -6,7 +6,7 @@ - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + %d{HH:mm:ss.SSS} %-5level %X{correlationId} [%thread] %logger{36} - %msg%n From 6af1558b995b2f860d1181c125a9970c3e7fbcd7 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Tue, 4 Apr 2023 02:37:40 +0200 Subject: [PATCH 09/15] Clean up; use the most recent version of labyrinth generator. --- pom.xml | 59 +++++++++++++++++-- .../server/handler/CreateHandler.java | 10 +++- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index bf06567..39521da 100644 --- a/pom.xml +++ b/pom.xml @@ -12,20 +12,41 @@ ch.fritteli.labyrinth labyrinth-server 0.0.2-SNAPSHOT + The Labyrinth server, offering a ReST endpoint to access the Labyrinth Generator. + https://manuel.friedli.info/labyrinth.html - 1.4.6 - 1.18.26 - 2.0.5 17 17 + 24.0.1 + 5.9.2 + 1.4.6 + 1.18.26 + 2.0.7 + 0.10.4 + + + Manuel Friedli + manuel + https://www.fritteli.ch/ + manuel@fritteli.ch + Europe/Zurich + + Project Lead + Software Architect + Software Engineer + Operations Manager + + + + ch.fritteli.labyrinth labyrinth-generator - 0.0.2 + 0.0.3 io.vavr @@ -65,7 +86,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.4.1 ch.fritteli.labyrinth.server.Main + + + ch.fritteli.labyrinth:labyrinth-generator + + logback.xml + + + @@ -83,10 +112,28 @@ + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + verify + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-site-plugin + 4.0.0-M6 + - scm:git:git://gittr.ch/java/labyrinth-server.git + scm:git:https://gittr.ch/java/labyrinth-server.git scm:git:ssh://git@gittr.ch/java/labyrinth-server.git https://gittr.ch/java/labyrinth-server HEAD diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java index 47da5f5..46cdfd4 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java @@ -8,13 +8,18 @@ import io.undertow.util.HttpString; import io.undertow.util.StatusCodes; import io.vavr.Tuple; import io.vavr.Tuple2; -import io.vavr.collection.*; +import io.vavr.collection.HashMap; +import io.vavr.collection.HashMultimap; +import io.vavr.collection.HashSet; +import io.vavr.collection.List; +import io.vavr.collection.Multimap; +import io.vavr.collection.Set; +import io.vavr.collection.Stream; import io.vavr.control.Option; import io.vavr.control.Try; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; -import org.slf4j.MDC; import java.nio.ByteBuffer; import java.time.Instant; @@ -22,7 +27,6 @@ import java.time.temporal.ChronoUnit; import java.util.Deque; import java.util.Map; import java.util.Random; -import java.util.UUID; @Slf4j public class CreateHandler extends AbstractHttpHandler { From 87fba97672c331863da1462a509cde154e5daa1e Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Tue, 4 Apr 2023 03:24:39 +0200 Subject: [PATCH 10/15] More logging, some simplification. BUT unsafe typecast. Needs thought. --- .../fritteli/labyrinth/server/OutputType.java | 1 + .../server/handler/CreateHandler.java | 87 ++++++++++++++----- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java index 60ff74f..d40f668 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java +++ b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java @@ -56,6 +56,7 @@ public enum OutputType { private final Function render; @Getter private final boolean attachment; + @Getter @NonNull private final List names; diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java index 46cdfd4..c5a304a 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java @@ -17,9 +17,11 @@ import io.vavr.collection.Set; import io.vavr.collection.Stream; import io.vavr.control.Option; import io.vavr.control.Try; +import lombok.Getter; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; +import org.slf4j.MDC; import java.nio.ByteBuffer; import java.time.Instant; @@ -27,6 +29,7 @@ import java.time.temporal.ChronoUnit; import java.util.Deque; import java.util.Map; import java.util.Random; +import java.util.function.Function; @Slf4j public class CreateHandler extends AbstractHttpHandler { @@ -39,8 +42,10 @@ public class CreateHandler extends AbstractHttpHandler { this.createLabyrinthFromRequestParameters(exchange.getQueryParameters()) .onFailure(e -> { log.error("Error creating Labyrinth from request", e); - exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); - exchange.setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING); + exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) + .setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING) + .getResponseSender() + .send(e.getMessage()); }) .forEach(tuple -> { final OutputType outputType = tuple._1(); @@ -50,30 +55,47 @@ public class CreateHandler extends AbstractHttpHandler { bytes = outputType.render(labyrinth); } catch (@NonNull final Exception e) { log.error("Error rendering Labyrinth", e); - exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); - exchange.setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING); + exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) + .setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING) + .getResponseSender() + .send("Error creating the Labyrinth. Please contact the administrator. Request id=%s".formatted(MDC.get("correlationId"))); return; } + final long durationMillis = start.until(Instant.now(), ChronoUnit.MILLIS); exchange.getResponseHeaders() .put(Headers.CONTENT_TYPE, outputType.getContentType()) .put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(labyrinth.getRandomSeed())) .put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth())) .put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(labyrinth.getHeight())) - .put(HttpString.tryFromString("X-Labyrinth-Generation-Duration-millis"), String.valueOf(start.until(Instant.now(), ChronoUnit.MILLIS))); + .put(HttpString.tryFromString("X-Labyrinth-Generation-Duration-millis"), String.valueOf(durationMillis)); exchange.getResponseSender().send(ByteBuffer.wrap(bytes)); - log.debug("Create request handled."); + log.debug("Create request handled in {}ms.", durationMillis); }); } - private @NonNull Try> createLabyrinthFromRequestParameters(final Map> queryParameters) { + @NonNull + private Try> createLabyrinthFromRequestParameters(final Map> queryParameters) { return new ParametersToLabyrinthExtractor(queryParameters).createLabyrinth(); } private enum RequestParameter { - WIDTH("w", "width"), HEIGHT("h", "height"), ID("i", "id"), OUTPUT("o", "output"); + WIDTH(p -> Try.of(() -> Integer.parseInt(p)).toOption(), "w", "width"), + HEIGHT(p -> Try.of(() -> Integer.parseInt(p)).toOption(), "h", "height"), + ID(p -> Try.of(() -> Long.parseLong(p)).toOption(), "i", "id"), + OUTPUT(OutputType::ofString, "o", "output"); + @NonNull + private final Function> extractor; + @Getter + @NonNull private final Set names; RequestParameter(@NonNull final String... names) { + this.extractor = null; + this.names = HashSet.of(names); + } + + RequestParameter(@NonNull final Function> extractor, @NonNull final String... names) { + this.extractor = extractor; this.names = HashSet.of(names); } @@ -83,6 +105,10 @@ public class CreateHandler extends AbstractHttpHandler { } return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); } + + @NonNull Option extractParameterValue(@NonNull final String parameter) { + return (Option) this.extractor.apply(parameter); + } } private static class ParametersToLabyrinthExtractor { @@ -98,22 +124,37 @@ public class CreateHandler extends AbstractHttpHandler { @NonNull Try> createLabyrinth() { - final Option output = getParameterValues(RequestParameter.OUTPUT) - .foldLeft(Option.none(), (type, param) -> type.orElse(() -> OutputType.ofString(param))); - final Option width = getParameterValues(RequestParameter.WIDTH) - .foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption())); - final Option height = getParameterValues(RequestParameter.HEIGHT) - .foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption())); - final Option id = getParameterValues(RequestParameter.ID) - .foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Long.parseLong(param)).toOption())); + final Option output = getParameterValue(RequestParameter.OUTPUT); + final Option width = getParameterValue(RequestParameter.WIDTH); + final Option height = getParameterValue(RequestParameter.HEIGHT); + final Option id = getParameterValue(RequestParameter.ID); - return Try.of(() -> { - final OutputType t1 = output.get(); - final Integer width1 = width.get(); - final Integer height1 = height.get(); - final Long orElse = id.getOrElse(() -> new Random().nextLong()); - return Tuple.of(t1, new Labyrinth(width1, height1, orElse)); - }); + if (output.isEmpty()) { + return Try.failure(new IllegalArgumentException("Path parameter %s is required and must be one of: %s".formatted( + RequestParameter.OUTPUT.getNames().mkString("'", " / ", "'"), + Stream.of(OutputType.values()) + .flatMap(OutputType::getNames) + .mkString(", ") + ))); + } + if (width.isEmpty()) { + return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( + RequestParameter.WIDTH.getNames().mkString("'", " / ", "'") + ))); + } + if (height.isEmpty()) { + return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( + RequestParameter.HEIGHT.getNames().mkString("'", " / ", "'") + ))); + } + + return Try.of(() -> Tuple.of(output.get(), new Labyrinth(width.get(), height.get(), id.getOrElse(() -> new Random().nextLong())))); + } + + @NonNull + private Option getParameterValue(@NonNull final RequestParameter parameter) { + return this.getParameterValues(parameter) + .foldLeft(Option.none(), (type, param) -> type.orElse(() -> parameter.extractParameterValue(param))); } @NonNull From 741894163bc8a9fe4a29936376a2e0daacd3c6e9 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Thu, 6 Apr 2023 08:19:44 +0200 Subject: [PATCH 11/15] Implement rendering of binary data. --- .../labyrinth/server/LabyrinthServer.java | 46 +------------------ .../server/handler/AbstractHttpHandler.java | 7 ++- .../server/handler/RenderHandler.java | 36 ++++++++++++++- src/main/resources/logback.xml | 2 + 4 files changed, 43 insertions(+), 48 deletions(-) diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java index 09ed5fc..f0f0321 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java +++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java @@ -45,7 +45,7 @@ public class LabyrinthServer { private void start() { Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); this.undertow.start(); - log.info("Listeing on http://{}:{}", this.config.getAddress().getHostAddress(), this.config.getPort()); + log.info("Listening on http://{}:{}", this.config.getAddress().getHostAddress(), this.config.getPort()); } private void stop() { @@ -53,48 +53,4 @@ public class LabyrinthServer { this.undertow.stop(); log.info("Server stopped."); } - -// private void handleRender(final HttpExchange exchange) { -// this.executorService.submit(() -> { -// 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(); -// -// final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); -// -// final OutputType output = exchange.getRequestHeaders() -// .get("Accept") -// .contains(OutputType.HTML.getContentType()) ? -// 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(render); -// responseBody.flush(); -// } catch (Exception e) { -// log.error("FSCK!", e); -// } finally { -// exchange.close(); -// } -// }); -// } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java index 384ecc2..198c7a4 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java @@ -7,13 +7,17 @@ import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.UUID; @Slf4j public abstract class AbstractHttpHandler implements HttpHandler { @Override public final void handleRequest(@NonNull final HttpServerExchange exchange) { - try (final MDC.MDCCloseable mdc = MDC.putCloseable("correlationId", UUID.randomUUID().toString())) { + final Instant start = Instant.now(); + try (final MDC.MDCCloseable closeable = MDC.putCloseable("correlationId", UUID.randomUUID().toString())) { + if (exchange.isInIoThread()) { log.debug("Dispatching request"); exchange.dispatch(this); @@ -27,6 +31,7 @@ public abstract class AbstractHttpHandler implements HttpHandler { .getResponseSender() .send(StatusCodes.INTERNAL_SERVER_ERROR_STRING); } + log.debug("Completed request in {}ms.", start.until(Instant.now(), ChronoUnit.MILLIS)); } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java index a440b90..c5e47d9 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java @@ -1,17 +1,49 @@ package ch.fritteli.labyrinth.server.handler; +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer; +import ch.fritteli.labyrinth.server.OutputType; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; import io.undertow.util.StatusCodes; +import io.vavr.control.Option; +import lombok.extern.slf4j.Slf4j; +import java.nio.ByteBuffer; + +@Slf4j public class RenderHandler implements HttpHandler { @Override public void handleRequest(final HttpServerExchange exchange) { + log.debug("Handling render request"); + if (exchange.isInIoThread()) { exchange.dispatch(this); return; } - exchange.setStatusCode(StatusCodes.NOT_IMPLEMENTED); - exchange.getResponseSender().send("Rendering binary data is not implemented yet."); + exchange.getRequestReceiver().receiveFullBytes((httpServerExchange, bytes) -> { + final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); + final OutputType output = Option.of(httpServerExchange.getRequestHeaders().get(Headers.ACCEPT)) + .exists(values -> values.contains(OutputType.HTML.getContentType())) ? + OutputType.HTML : + OutputType.TEXT_PLAIN; + final byte[] render; + try { + render = output.render(labyrinth); + } catch (final Exception e) { + log.error("Error rendering binary labyrinth data", e); + httpServerExchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) + .getResponseSender() + .send("Error rendering labyrinth: %s".formatted(e.getMessage())); + return; + } + httpServerExchange + .setStatusCode(StatusCodes.OK) + .getResponseHeaders() + .put(Headers.CONTENT_TYPE, output.getContentType()); + httpServerExchange.getResponseSender() + .send(ByteBuffer.wrap(render)); + }); } } diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 6cec730..69dd770 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -13,5 +13,7 @@ + + From a194e5db62e41678b089e6cfb0fe4cb285b4fbba Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sat, 8 Apr 2023 09:35:10 +0200 Subject: [PATCH 12/15] Refactoring. --- pom.xml | 6 +- .../ch/fritteli/labyrinth/server/Main.java | 3 + .../fritteli/labyrinth/server/OutputType.java | 2 +- .../server/handler/CreateHandler.java | 106 +---------------- .../ParametersToLabyrinthExtractor.java | 111 ++++++++++++++++++ 5 files changed, 123 insertions(+), 105 deletions(-) create mode 100644 src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java diff --git a/pom.xml b/pom.xml index 39521da..668a742 100644 --- a/pom.xml +++ b/pom.xml @@ -20,9 +20,11 @@ 17 24.0.1 5.9.2 + 0.0.3 1.4.6 1.18.26 2.0.7 + 2.3.5.Final 0.10.4 @@ -46,7 +48,7 @@ ch.fritteli.labyrinth labyrinth-generator - 0.0.3 + ${labyrinth-generator.version} io.vavr @@ -73,7 +75,7 @@ io.undertow undertow-core - 2.3.5.Final + ${undertow.version} org.junit.jupiter diff --git a/src/main/java/ch/fritteli/labyrinth/server/Main.java b/src/main/java/ch/fritteli/labyrinth/server/Main.java index d8c5bfa..9800d8c 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/Main.java +++ b/src/main/java/ch/fritteli/labyrinth/server/Main.java @@ -1,9 +1,12 @@ package ch.fritteli.labyrinth.server; +import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; @Slf4j +@UtilityClass public class Main { + public static void main(String[] args) { LabyrinthServer.createAndStartServer() .onFailure(e -> log.error("Failed to create server. Stopping.", e)); diff --git a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java index d40f668..4668bdd 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java +++ b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java @@ -88,7 +88,7 @@ public enum OutputType { } @NonNull - public byte[] render(@NonNull final Labyrinth labyrinth) throws Exception { + public byte[] render(@NonNull final Labyrinth labyrinth) { return this.render.apply(labyrinth); } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java index c5a304a..c1578fd 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java @@ -6,33 +6,20 @@ import io.undertow.server.HttpServerExchange; import io.undertow.util.Headers; import io.undertow.util.HttpString; import io.undertow.util.StatusCodes; -import io.vavr.Tuple; import io.vavr.Tuple2; -import io.vavr.collection.HashMap; -import io.vavr.collection.HashMultimap; -import io.vavr.collection.HashSet; -import io.vavr.collection.List; -import io.vavr.collection.Multimap; -import io.vavr.collection.Set; -import io.vavr.collection.Stream; -import io.vavr.control.Option; import io.vavr.control.Try; -import lombok.Getter; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.Nullable; -import org.slf4j.MDC; - import java.nio.ByteBuffer; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Deque; import java.util.Map; -import java.util.Random; -import java.util.function.Function; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; @Slf4j public class CreateHandler extends AbstractHttpHandler { + public static final String PATH_TEMPLATE = "/create/{output}"; @Override @@ -77,89 +64,4 @@ public class CreateHandler extends AbstractHttpHandler { private Try> createLabyrinthFromRequestParameters(final Map> queryParameters) { return new ParametersToLabyrinthExtractor(queryParameters).createLabyrinth(); } - - private enum RequestParameter { - WIDTH(p -> Try.of(() -> Integer.parseInt(p)).toOption(), "w", "width"), - HEIGHT(p -> Try.of(() -> Integer.parseInt(p)).toOption(), "h", "height"), - ID(p -> Try.of(() -> Long.parseLong(p)).toOption(), "i", "id"), - OUTPUT(OutputType::ofString, "o", "output"); - @NonNull - private final Function> extractor; - @Getter - @NonNull - private final Set names; - - RequestParameter(@NonNull final String... names) { - this.extractor = null; - this.names = HashSet.of(names); - } - - RequestParameter(@NonNull final Function> extractor, @NonNull final String... names) { - this.extractor = extractor; - this.names = HashSet.of(names); - } - - static Option parseName(@Nullable final String name) { - if (name == null) { - return Option.none(); - } - return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); - } - - @NonNull Option extractParameterValue(@NonNull final String parameter) { - return (Option) this.extractor.apply(parameter); - } - } - - private static class ParametersToLabyrinthExtractor { - @NonNull - private final Multimap queryParameters; - - ParametersToLabyrinthExtractor(@NonNull final Map> queryParameters) { - this.queryParameters = HashMap.ofAll(queryParameters).foldLeft(HashMultimap.withSet().empty(), (map, tuple) -> { - final String key = tuple._1(); - return Stream.ofAll(tuple._2()).foldLeft(map, (m, value) -> m.put(key, value)); - }); - } - - @NonNull - Try> createLabyrinth() { - final Option output = getParameterValue(RequestParameter.OUTPUT); - final Option width = getParameterValue(RequestParameter.WIDTH); - final Option height = getParameterValue(RequestParameter.HEIGHT); - final Option id = getParameterValue(RequestParameter.ID); - - if (output.isEmpty()) { - return Try.failure(new IllegalArgumentException("Path parameter %s is required and must be one of: %s".formatted( - RequestParameter.OUTPUT.getNames().mkString("'", " / ", "'"), - Stream.of(OutputType.values()) - .flatMap(OutputType::getNames) - .mkString(", ") - ))); - } - if (width.isEmpty()) { - return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( - RequestParameter.WIDTH.getNames().mkString("'", " / ", "'") - ))); - } - if (height.isEmpty()) { - return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( - RequestParameter.HEIGHT.getNames().mkString("'", " / ", "'") - ))); - } - - return Try.of(() -> Tuple.of(output.get(), new Labyrinth(width.get(), height.get(), id.getOrElse(() -> new Random().nextLong())))); - } - - @NonNull - private Option getParameterValue(@NonNull final RequestParameter parameter) { - return this.getParameterValues(parameter) - .foldLeft(Option.none(), (type, param) -> type.orElse(() -> parameter.extractParameterValue(param))); - } - - @NonNull - private Stream getParameterValues(@NonNull final RequestParameter parameter) { - return parameter.names.toStream().flatMap(name -> Stream.ofAll(this.queryParameters.getOrElse(name, List.empty()))); - } - } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java b/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java new file mode 100644 index 0000000..b766b77 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java @@ -0,0 +1,111 @@ +package ch.fritteli.labyrinth.server.handler; + +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.server.OutputType; +import io.vavr.Tuple; +import io.vavr.Tuple2; +import io.vavr.collection.HashMap; +import io.vavr.collection.HashMultimap; +import io.vavr.collection.HashSet; +import io.vavr.collection.List; +import io.vavr.collection.Multimap; +import io.vavr.collection.Set; +import io.vavr.collection.Stream; +import io.vavr.control.Option; +import io.vavr.control.Try; +import java.util.Deque; +import java.util.Map; +import java.util.Random; +import java.util.function.Function; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.Nullable; + +class ParametersToLabyrinthExtractor { + + @NonNull + private final Multimap queryParameters; + + ParametersToLabyrinthExtractor(@NonNull final Map> queryParameters) { + this.queryParameters = HashMap.ofAll(queryParameters) + .foldLeft( + HashMultimap.withSet().empty(), + (map, tuple) -> RequestParameter.parseName(tuple._1()).map(parameter -> Stream.ofAll(tuple._2()) + .flatMap(parameter::extractParameterValue) + .foldLeft(map, (m, value) -> m.put(parameter, value))) + .getOrElse(map) + ); + } + + @NonNull + Try> createLabyrinth() { + final Option output = getParameterValue(RequestParameter.OUTPUT); + final Option width = getParameterValue(RequestParameter.WIDTH); + final Option height = getParameterValue(RequestParameter.HEIGHT); + final Option id = getParameterValue(RequestParameter.ID); + + if (output.isEmpty()) { + return Try.failure(new IllegalArgumentException("Path parameter %s is required and must be one of: %s".formatted( + RequestParameter.OUTPUT.getNames().mkString("'", " / ", "'"), + Stream.of(OutputType.values()) + .flatMap(OutputType::getNames) + .mkString(", ") + ))); + } + if (width.isEmpty()) { + return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( + RequestParameter.WIDTH.getNames().mkString("'", " / ", "'") + ))); + } + if (height.isEmpty()) { + return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( + RequestParameter.HEIGHT.getNames().mkString("'", " / ", "'") + ))); + } + + return Try.of(() -> Tuple.of(output.get(), new Labyrinth(width.get(), height.get(), id.getOrElse(() -> new Random().nextLong())))); + } + + @NonNull + private Option getParameterValue(@NonNull final RequestParameter parameter) { + return (Option) this.queryParameters.getOrElse(parameter, List.empty()) + .headOption(); + } + + @Slf4j + private enum RequestParameter { + WIDTH(p -> Try.of(() -> Integer.parseInt(p)) + .toOption() + .onEmpty(() -> log.debug("Unparseable value for parameter 'width': '{}'", p)), "w", "width"), + HEIGHT(p -> Try.of(() -> Integer.parseInt(p)) + .toOption() + .onEmpty(() -> log.debug("Unparseable value for parameter 'height': '{}'", p)), "h", "height"), + ID(p -> Try.of(() -> Long.parseLong(p)) + .toOption() + .onEmpty(() -> log.debug("Unparseable value for parameter 'id': '{}'", p)), "i", "id"), + OUTPUT(p -> OutputType.ofString(p) + .onEmpty(() -> log.debug("Unparseable value for parameter 'output': '{}'", p)), "o", "output"); + @NonNull + private final Function> extractor; + @Getter + @NonNull + private final Set names; + + RequestParameter(@NonNull final Function> extractor, @NonNull final String... names) { + this.extractor = extractor; + this.names = HashSet.of(names); + } + + static Option parseName(@Nullable final String name) { + if (name == null) { + return Option.none(); + } + return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); + } + + @NonNull Option extractParameterValue(@NonNull final String parameter) { + return this.extractor.apply(parameter); + } + } +} From a45daf6ba320791109ff48a31812c841296544f0 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sat, 8 Apr 2023 09:43:04 +0200 Subject: [PATCH 13/15] Extend sAbstractHttpHandler. --- .../server/handler/RenderHandler.java | 4 +- src/main/resources/logback.xml | 4 +- src/main/resources/webassets/index.html | 33 ------------ src/main/resources/webassets/style.css | 51 ------------------- 4 files changed, 3 insertions(+), 89 deletions(-) delete mode 100644 src/main/resources/webassets/index.html delete mode 100644 src/main/resources/webassets/style.css diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java index c5e47d9..5079a09 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java @@ -13,9 +13,9 @@ import lombok.extern.slf4j.Slf4j; import java.nio.ByteBuffer; @Slf4j -public class RenderHandler implements HttpHandler { +public class RenderHandler extends AbstractHttpHandler { @Override - public void handleRequest(final HttpServerExchange exchange) { + public void handle(final HttpServerExchange exchange) { log.debug("Handling render request"); if (exchange.isInIoThread()) { diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 69dd770..d660474 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -13,7 +13,5 @@ - - - + diff --git a/src/main/resources/webassets/index.html b/src/main/resources/webassets/index.html deleted file mode 100644 index b97b934..0000000 --- a/src/main/resources/webassets/index.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - 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 deleted file mode 100644 index 153f8e8..0000000 --- a/src/main/resources/webassets/style.css +++ /dev/null @@ -1,51 +0,0 @@ -: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; -} From ed9df43aa81c2c671fb2fd8c01cbe339458ebbb5 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sat, 8 Apr 2023 22:28:32 +0200 Subject: [PATCH 14/15] One feature, one bugfix: - Feat: Enable rendering binary data to any supported output format. - Fix: Correctly serve as attachment when outputtyp is pdffile or binary. --- pom.xml | 2 +- .../labyrinth/server/LabyrinthServer.java | 13 ++-- .../fritteli/labyrinth/server/OutputType.java | 4 +- .../server/handler/CreateHandler.java | 9 +++ .../ParametersToLabyrinthExtractor.java | 64 ++----------------- .../server/handler/RenderHandler.java | 29 ++++++--- .../server/handler/RequestParameter.java | 54 ++++++++++++++++ 7 files changed, 98 insertions(+), 77 deletions(-) create mode 100644 src/main/java/ch/fritteli/labyrinth/server/handler/RequestParameter.java diff --git a/pom.xml b/pom.xml index 668a742..c47be7e 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 17 24.0.1 5.9.2 - 0.0.3 + 0.0.4 1.4.6 1.18.26 2.0.7 diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java index f0f0321..16e0aae 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java +++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java @@ -5,24 +5,23 @@ import ch.fritteli.labyrinth.server.handler.RenderHandler; import io.undertow.Undertow; import io.undertow.server.RoutingHandler; import io.vavr.control.Try; +import java.net.InetSocketAddress; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @Slf4j public class LabyrinthServer { - @NonNull - private final ServerConfig config; + @NonNull private final Undertow undertow; private LabyrinthServer(@NonNull final ServerConfig config) { - this.config = config; final String hostAddress = config.getAddress().getHostAddress(); final int port = config.getPort(); log.info("Starting Server at http://{}:{}/", hostAddress, port); final RoutingHandler routingHandler = new RoutingHandler() .get(CreateHandler.PATH_TEMPLATE, new CreateHandler()) - .post("/render", new RenderHandler()); + .post(RenderHandler.PATH_TEMPLATE, new RenderHandler()); this.undertow = Undertow.builder() .addHttpListener(port, hostAddress) @@ -45,7 +44,11 @@ public class LabyrinthServer { private void start() { Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); this.undertow.start(); - log.info("Listening on http://{}:{}", this.config.getAddress().getHostAddress(), this.config.getPort()); + final InetSocketAddress address = (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress(); + final String hostAddress = address.getAddress().getHostAddress(); + final int port = address.getPort(); + + log.info("Listening on http://{}:{}", hostAddress, port); } private void stop() { diff --git a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java index 4668bdd..c1d08d9 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java +++ b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java @@ -30,13 +30,13 @@ public enum OutputType { "html"), PDF("application/pdf", "pdf", - labyrinth -> PDFRenderer.newInstance().render(labyrinth), + labyrinth -> PDFRenderer.newInstance().render(labyrinth).toByteArray(), false, "p", "pdf"), PDFFILE("application/pdf", "pdf", - labyrinth -> PDFRenderer.newInstance().render(labyrinth), + labyrinth -> PDFRenderer.newInstance().render(labyrinth).toByteArray(), true, "f", "pdffile"), diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java index c1578fd..344a907 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java @@ -55,6 +55,15 @@ public class CreateHandler extends AbstractHttpHandler { .put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth())) .put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(labyrinth.getHeight())) .put(HttpString.tryFromString("X-Labyrinth-Generation-Duration-millis"), String.valueOf(durationMillis)); + if (outputType.isAttachment()) { + exchange.getResponseHeaders() + .put(Headers.CONTENT_DISPOSITION, "attachment; filename=\"labyrinth-%dx%d-%d.%s\"".formatted( + labyrinth.getWidth(), + labyrinth.getHeight(), + labyrinth.getRandomSeed(), + outputType.getFileExtension() + )); + } exchange.getResponseSender().send(ByteBuffer.wrap(bytes)); log.debug("Create request handled in {}ms.", durationMillis); }); diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java b/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java index b766b77..a1cf854 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java @@ -4,39 +4,20 @@ import ch.fritteli.labyrinth.generator.model.Labyrinth; import ch.fritteli.labyrinth.server.OutputType; import io.vavr.Tuple; import io.vavr.Tuple2; -import io.vavr.collection.HashMap; -import io.vavr.collection.HashMultimap; -import io.vavr.collection.HashSet; -import io.vavr.collection.List; -import io.vavr.collection.Multimap; -import io.vavr.collection.Set; import io.vavr.collection.Stream; import io.vavr.control.Option; import io.vavr.control.Try; import java.util.Deque; import java.util.Map; import java.util.Random; -import java.util.function.Function; -import lombok.Getter; import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.Nullable; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor class ParametersToLabyrinthExtractor { @NonNull - private final Multimap queryParameters; - - ParametersToLabyrinthExtractor(@NonNull final Map> queryParameters) { - this.queryParameters = HashMap.ofAll(queryParameters) - .foldLeft( - HashMultimap.withSet().empty(), - (map, tuple) -> RequestParameter.parseName(tuple._1()).map(parameter -> Stream.ofAll(tuple._2()) - .flatMap(parameter::extractParameterValue) - .foldLeft(map, (m, value) -> m.put(parameter, value))) - .getOrElse(map) - ); - } + private final Map> queryParameters; @NonNull Try> createLabyrinth() { @@ -69,43 +50,6 @@ class ParametersToLabyrinthExtractor { @NonNull private Option getParameterValue(@NonNull final RequestParameter parameter) { - return (Option) this.queryParameters.getOrElse(parameter, List.empty()) - .headOption(); - } - - @Slf4j - private enum RequestParameter { - WIDTH(p -> Try.of(() -> Integer.parseInt(p)) - .toOption() - .onEmpty(() -> log.debug("Unparseable value for parameter 'width': '{}'", p)), "w", "width"), - HEIGHT(p -> Try.of(() -> Integer.parseInt(p)) - .toOption() - .onEmpty(() -> log.debug("Unparseable value for parameter 'height': '{}'", p)), "h", "height"), - ID(p -> Try.of(() -> Long.parseLong(p)) - .toOption() - .onEmpty(() -> log.debug("Unparseable value for parameter 'id': '{}'", p)), "i", "id"), - OUTPUT(p -> OutputType.ofString(p) - .onEmpty(() -> log.debug("Unparseable value for parameter 'output': '{}'", p)), "o", "output"); - @NonNull - private final Function> extractor; - @Getter - @NonNull - private final Set names; - - RequestParameter(@NonNull final Function> extractor, @NonNull final String... names) { - this.extractor = extractor; - this.names = HashSet.of(names); - } - - static Option parseName(@Nullable final String name) { - if (name == null) { - return Option.none(); - } - return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); - } - - @NonNull Option extractParameterValue(@NonNull final String parameter) { - return this.extractor.apply(parameter); - } + return parameter.getParameterValue(this.queryParameters); } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java index 5079a09..52a61d5 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java @@ -3,17 +3,19 @@ package ch.fritteli.labyrinth.server.handler; import ch.fritteli.labyrinth.generator.model.Labyrinth; import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer; import ch.fritteli.labyrinth.server.OutputType; -import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderValues; import io.undertow.util.Headers; import io.undertow.util.StatusCodes; -import io.vavr.control.Option; -import lombok.extern.slf4j.Slf4j; - import java.nio.ByteBuffer; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; @Slf4j public class RenderHandler extends AbstractHttpHandler { + + public static final String PATH_TEMPLATE = "/render/{output}"; + @Override public void handle(final HttpServerExchange exchange) { log.debug("Handling render request"); @@ -23,13 +25,10 @@ public class RenderHandler extends AbstractHttpHandler { return; } exchange.getRequestReceiver().receiveFullBytes((httpServerExchange, bytes) -> { - final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); - final OutputType output = Option.of(httpServerExchange.getRequestHeaders().get(Headers.ACCEPT)) - .exists(values -> values.contains(OutputType.HTML.getContentType())) ? - OutputType.HTML : - OutputType.TEXT_PLAIN; + final OutputType output = this.getOutputType(httpServerExchange); final byte[] render; try { + final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); render = output.render(labyrinth); } catch (final Exception e) { log.error("Error rendering binary labyrinth data", e); @@ -46,4 +45,16 @@ public class RenderHandler extends AbstractHttpHandler { .send(ByteBuffer.wrap(render)); }); } + + @NonNull + private OutputType getOutputType(@NonNull final HttpServerExchange httpServerExchange) { + return RequestParameter.OUTPUT.getParameterValue(httpServerExchange.getQueryParameters()) + .getOrElse(() -> { + final HeaderValues accept = httpServerExchange.getRequestHeaders().get(Headers.ACCEPT); + if (accept.contains(OutputType.HTML.getContentType())) { + return OutputType.HTML; + } + return OutputType.TEXT_PLAIN; + }); + } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/RequestParameter.java b/src/main/java/ch/fritteli/labyrinth/server/handler/RequestParameter.java new file mode 100644 index 0000000..314ee22 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/RequestParameter.java @@ -0,0 +1,54 @@ +package ch.fritteli.labyrinth.server.handler; + +import ch.fritteli.labyrinth.server.OutputType; +import io.vavr.Tuple2; +import io.vavr.collection.HashMap; +import io.vavr.collection.HashSet; +import io.vavr.collection.Set; +import io.vavr.control.Option; +import io.vavr.control.Try; +import java.util.Deque; +import java.util.Map; +import java.util.function.Function; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +enum RequestParameter { + WIDTH(p -> Try.of(() -> Integer.parseInt(p)) + .toOption() + .onEmpty(() -> log.debug("Unparseable value for parameter 'width': '{}'", p)), "w", "width"), + HEIGHT(p -> Try.of(() -> Integer.parseInt(p)) + .toOption() + .onEmpty(() -> log.debug("Unparseable value for parameter 'height': '{}'", p)), "h", "height"), + ID(p -> Try.of(() -> Long.parseLong(p)) + .toOption() + .onEmpty(() -> log.debug("Unparseable value for parameter 'id': '{}'", p)), "i", "id"), + OUTPUT(p -> OutputType.ofString(p) + .onEmpty(() -> log.debug("Unparseable value for parameter 'output': '{}'", p)), "o", "output"); + @NonNull + private final Function> extractor; + @Getter + @NonNull + private final Set names; + + RequestParameter(@NonNull final Function> extractor, @NonNull final String... names) { + this.extractor = extractor; + this.names = HashSet.of(names); + } + + @NonNull + Option extractParameterValue(@NonNull final String parameter) { + return this.extractor.apply(parameter); + } + + @NonNull + public Option getParameterValue(@NonNull final Map> queryParameters) { + return (Option) HashMap.ofAll(queryParameters) + .filterKeys(this.names::contains) + .flatMap(Tuple2::_2) + .flatMap(this::extractParameterValue) + .headOption(); + } +} From 93aa9e17b692fc60f3be65ac16d2a01049b270b7 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sat, 8 Apr 2023 22:32:16 +0200 Subject: [PATCH 15/15] ReST -> REST. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c47be7e..9b3e6c0 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ ch.fritteli.labyrinth labyrinth-server 0.0.2-SNAPSHOT - The Labyrinth server, offering a ReST endpoint to access the Labyrinth Generator. + The Labyrinth server, offering a REST endpoint to access the Labyrinth Generator. https://manuel.friedli.info/labyrinth.html