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