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