From c4d707d64a208d177a8b6b32b8cd2b77f8a7249f Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sat, 14 Dec 2024 04:50:30 +0100 Subject: [PATCH 1/2] Very simple way to specify the algorithm to use to generate mazes. --- docker/Dockerfile | 7 ++- pom.xml | 2 +- .../ch/fritteli/maze/server/Algorithm.java | 45 +++++++++++++++++++ .../handler/ParametersToMazeExtractor.java | 7 ++- .../maze/server/handler/RequestParameter.java | 5 ++- 5 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 src/main/java/ch/fritteli/maze/server/Algorithm.java diff --git a/docker/Dockerfile b/docker/Dockerfile index 9680592..74a2661 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,4 +4,9 @@ COPY target/maze-server-*.jar /app/ RUN rm /app/*-sources.jar RUN mv /app/*.jar /app/app.jar -CMD java -Dfritteli.maze.server.host=0.0.0.0 -Dfritteli.maze.server.port=80 -jar /app/app.jar +CMD java \ + -Dfritteli.maze.server.host=0.0.0.0 \ + -Dfritteli.maze.server.port=80 \ + -Dfritteli.maze.maxheight=256 \ + -Dfritteli.maze.maxwidth=256 \ + -jar /app/app.jar diff --git a/pom.xml b/pom.xml index aed0ac3..6693fcf 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ - 0.2.1 + 0.2.2-SNAPSHOT 4.0.0-M8 2.3.18.Final diff --git a/src/main/java/ch/fritteli/maze/server/Algorithm.java b/src/main/java/ch/fritteli/maze/server/Algorithm.java new file mode 100644 index 0000000..3b0dcf7 --- /dev/null +++ b/src/main/java/ch/fritteli/maze/server/Algorithm.java @@ -0,0 +1,45 @@ +package ch.fritteli.maze.server; + +import ch.fritteli.maze.generator.algorithm.MazeGeneratorAlgorithm; +import ch.fritteli.maze.generator.algorithm.RandomDepthFirst; +import ch.fritteli.maze.generator.algorithm.Wilson; +import ch.fritteli.maze.generator.model.Maze; +import io.vavr.collection.List; +import io.vavr.collection.Stream; +import io.vavr.control.Option; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Function; + +public enum Algorithm { + RANDOM_DEPTH_FIRST(RandomDepthFirst::new, "random", "random-depth-first"), + WILSON(Wilson::new, "wilson"); + + @NotNull + private final Function creator; + + @Getter + @NotNull + private final List names; + + Algorithm(@NotNull final Function creator, + @NotNull final String... names) { + this.creator = creator; + this.names = List.of(names); + } + + @NotNull + public static Option ofString(@Nullable final String name) { + return Option.of(name) + .map(String::toLowerCase) + .flatMap(nameLC -> Stream.of(values()) + .find(algorithm -> algorithm.getNames().contains(nameLC))); + } + + @NotNull + public MazeGeneratorAlgorithm createAlgorithm(@NotNull final Maze maze) { + return this.creator.apply(maze); + } +} diff --git a/src/main/java/ch/fritteli/maze/server/handler/ParametersToMazeExtractor.java b/src/main/java/ch/fritteli/maze/server/handler/ParametersToMazeExtractor.java index 6c747e5..8ff14aa 100644 --- a/src/main/java/ch/fritteli/maze/server/handler/ParametersToMazeExtractor.java +++ b/src/main/java/ch/fritteli/maze/server/handler/ParametersToMazeExtractor.java @@ -3,6 +3,7 @@ package ch.fritteli.maze.server.handler; import ch.fritteli.maze.generator.algorithm.RandomDepthFirst; import ch.fritteli.maze.generator.model.Maze; import ch.fritteli.maze.generator.model.Position; +import ch.fritteli.maze.server.Algorithm; import ch.fritteli.maze.server.InvalidRequestParameterException; import ch.fritteli.maze.server.OutputType; import io.vavr.collection.Stream; @@ -33,6 +34,7 @@ class ParametersToMazeExtractor { final Option id = getParameterValue(RequestParameter.ID); final Option start = getParameterValue(RequestParameter.START); final Option end = getParameterValue(RequestParameter.END); + final Option algorithm = getParameterValue(RequestParameter.ALGORITHM); if (output.isEmpty()) { return Try.failure(new InvalidRequestParameterException("Path parameter %s is required and must be one of: %s".formatted( @@ -77,7 +79,10 @@ class ParametersToMazeExtractor { } else { maze = new Maze(desiredWidth, desiredHeight, id.getOrElse(() -> new Random().nextLong())); } - new RandomDepthFirst(maze).run(); + + algorithm.getOrElse(Algorithm.RANDOM_DEPTH_FIRST) + .createAlgorithm(maze) + .run(); return new GeneratedMaze(maze, output.get(), RandomDepthFirst.class.getSimpleName()); }); } diff --git a/src/main/java/ch/fritteli/maze/server/handler/RequestParameter.java b/src/main/java/ch/fritteli/maze/server/handler/RequestParameter.java index 3808313..5cdd893 100644 --- a/src/main/java/ch/fritteli/maze/server/handler/RequestParameter.java +++ b/src/main/java/ch/fritteli/maze/server/handler/RequestParameter.java @@ -1,6 +1,7 @@ package ch.fritteli.maze.server.handler; import ch.fritteli.maze.generator.model.Position; +import ch.fritteli.maze.server.Algorithm; import ch.fritteli.maze.server.OutputType; import io.vavr.Tuple2; import io.vavr.collection.HashMap; @@ -44,7 +45,9 @@ enum RequestParameter { return new Position(x, y); }) .toOption() - .onEmpty(() -> log.debug("Unparseable value for parameter 'end': '{}'", p)), "e", "end"); + .onEmpty(() -> log.debug("Unparseable value for parameter 'end': '{}'", p)), "e", "end"), + ALGORITHM(p -> Algorithm.ofString(p) + .onEmpty(() -> log.debug("Unparseable value for parameter 'algorithm': '{}'", p)), "a", "algorithm"); @NotNull private final Function> extractor; @Getter -- 2.45.2 From 3c2fca9a7441aea365307f1a0d7d4aa1b31ad9e6 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Tue, 24 Dec 2024 03:28:35 +0100 Subject: [PATCH 2/2] Implement Wilson's algorithm and let the caller choose the algorithm. --- pom.xml | 2 +- .../ch/fritteli/maze/server/Algorithm.java | 2 +- .../ch/fritteli/maze/server/MazeServer.java | 6 +- .../ch/fritteli/maze/server/OutputType.java | 12 ++- .../maze/server/handler/CreateHandler.java | 2 +- .../handler/ParametersToMazeExtractor.java | 12 +-- .../maze/server/handler/RenderV3Handler.java | 61 +++++++++++++ .../maze/server/handler/RenderVxHandler.java | 91 +++++++++++++++++++ 8 files changed, 176 insertions(+), 12 deletions(-) create mode 100644 src/main/java/ch/fritteli/maze/server/handler/RenderV3Handler.java create mode 100644 src/main/java/ch/fritteli/maze/server/handler/RenderVxHandler.java diff --git a/pom.xml b/pom.xml index 6693fcf..d6efbf3 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ - 0.2.2-SNAPSHOT + 0.3.0 4.0.0-M8 2.3.18.Final diff --git a/src/main/java/ch/fritteli/maze/server/Algorithm.java b/src/main/java/ch/fritteli/maze/server/Algorithm.java index 3b0dcf7..6d4f29f 100644 --- a/src/main/java/ch/fritteli/maze/server/Algorithm.java +++ b/src/main/java/ch/fritteli/maze/server/Algorithm.java @@ -2,7 +2,7 @@ package ch.fritteli.maze.server; import ch.fritteli.maze.generator.algorithm.MazeGeneratorAlgorithm; import ch.fritteli.maze.generator.algorithm.RandomDepthFirst; -import ch.fritteli.maze.generator.algorithm.Wilson; +import ch.fritteli.maze.generator.algorithm.wilson.Wilson; import ch.fritteli.maze.generator.model.Maze; import io.vavr.collection.List; import io.vavr.collection.Stream; diff --git a/src/main/java/ch/fritteli/maze/server/MazeServer.java b/src/main/java/ch/fritteli/maze/server/MazeServer.java index 84a1fcb..f864975 100644 --- a/src/main/java/ch/fritteli/maze/server/MazeServer.java +++ b/src/main/java/ch/fritteli/maze/server/MazeServer.java @@ -3,6 +3,8 @@ package ch.fritteli.maze.server; import ch.fritteli.maze.server.handler.CreateHandler; import ch.fritteli.maze.server.handler.RenderV1Handler; import ch.fritteli.maze.server.handler.RenderV2Handler; +import ch.fritteli.maze.server.handler.RenderV3Handler; +import ch.fritteli.maze.server.handler.RenderVxHandler; import io.undertow.Undertow; import io.undertow.server.RoutingHandler; import io.vavr.control.Try; @@ -24,7 +26,9 @@ public class MazeServer { final RoutingHandler routingHandler = new RoutingHandler() .get(CreateHandler.PATH_TEMPLATE, new CreateHandler(config.maxMazeHeight(), config.maxMazeWidth())) .post(RenderV1Handler.PATH_TEMPLATE, new RenderV1Handler()) - .post(RenderV2Handler.PATH_TEMPLATE, new RenderV2Handler()); + .post(RenderV2Handler.PATH_TEMPLATE, new RenderV2Handler()) + .post(RenderV3Handler.PATH_TEMPLATE, new RenderV3Handler()) + .post(RenderVxHandler.PATH_TEMPLATE, new RenderVxHandler()); this.undertow = Undertow.builder() .addHttpListener(port, hostAddress) diff --git a/src/main/java/ch/fritteli/maze/server/OutputType.java b/src/main/java/ch/fritteli/maze/server/OutputType.java index cbc23f1..ad8d25d 100644 --- a/src/main/java/ch/fritteli/maze/server/OutputType.java +++ b/src/main/java/ch/fritteli/maze/server/OutputType.java @@ -7,6 +7,7 @@ import ch.fritteli.maze.generator.renderer.pdf.PDFRenderer; import ch.fritteli.maze.generator.renderer.text.TextRenderer; import ch.fritteli.maze.generator.serialization.v1.SerializerDeserializerV1; import ch.fritteli.maze.generator.serialization.v2.SerializerDeserializerV2; +import ch.fritteli.maze.generator.serialization.v3.SerializerDeserializerV3; import io.vavr.collection.List; import io.vavr.collection.Stream; import io.vavr.control.Option; @@ -23,7 +24,8 @@ public enum OutputType { maze -> TextRenderer.newInstance().render(maze).getBytes(StandardCharsets.UTF_8), false, "t", - "text"), + "text", + "txt"), HTML("text/html", "html", maze -> HTMLRenderer.newInstance().render(maze).getBytes(StandardCharsets.UTF_8), @@ -65,7 +67,13 @@ public enum OutputType { SerializerDeserializerV2::serialize, true, "v", - "binaryv2"); + "binaryv2"), + BINARY_V3("application/octet-stream", + "maz3", + SerializerDeserializerV3::serialize, + true, + "3", + "binaryv3"); @Getter @NotNull private final String contentType; diff --git a/src/main/java/ch/fritteli/maze/server/handler/CreateHandler.java b/src/main/java/ch/fritteli/maze/server/handler/CreateHandler.java index 4d44ab2..8eb8f0f 100644 --- a/src/main/java/ch/fritteli/maze/server/handler/CreateHandler.java +++ b/src/main/java/ch/fritteli/maze/server/handler/CreateHandler.java @@ -68,7 +68,7 @@ public class CreateHandler extends AbstractHttpHandler { .put(HttpString.tryFromString("X-Maze-ID"), String.valueOf(maze.getRandomSeed())) .put(HttpString.tryFromString("X-Maze-Width"), String.valueOf(maze.getWidth())) .put(HttpString.tryFromString("X-Maze-Height"), String.valueOf(maze.getHeight())) - .put(HttpString.tryFromString("X-Maze-Algorithm"), generatedMaze.generatorName()) + .put(HttpString.tryFromString("X-Maze-Algorithm"), maze.getAlgorithm()) .put(HttpString.tryFromString("X-Maze-Generation-Duration-millis"), String.valueOf(durationMillis)); if (outputType.isAttachment()) { exchange.getResponseHeaders() diff --git a/src/main/java/ch/fritteli/maze/server/handler/ParametersToMazeExtractor.java b/src/main/java/ch/fritteli/maze/server/handler/ParametersToMazeExtractor.java index 8ff14aa..93f9964 100644 --- a/src/main/java/ch/fritteli/maze/server/handler/ParametersToMazeExtractor.java +++ b/src/main/java/ch/fritteli/maze/server/handler/ParametersToMazeExtractor.java @@ -1,6 +1,6 @@ package ch.fritteli.maze.server.handler; -import ch.fritteli.maze.generator.algorithm.RandomDepthFirst; +import ch.fritteli.maze.generator.algorithm.MazeGeneratorAlgorithm; import ch.fritteli.maze.generator.model.Maze; import ch.fritteli.maze.generator.model.Position; import ch.fritteli.maze.server.Algorithm; @@ -80,10 +80,10 @@ class ParametersToMazeExtractor { maze = new Maze(desiredWidth, desiredHeight, id.getOrElse(() -> new Random().nextLong())); } - algorithm.getOrElse(Algorithm.RANDOM_DEPTH_FIRST) - .createAlgorithm(maze) - .run(); - return new GeneratedMaze(maze, output.get(), RandomDepthFirst.class.getSimpleName()); + final MazeGeneratorAlgorithm generator = algorithm.getOrElse(Algorithm.WILSON) + .createAlgorithm(maze); + generator.run(); + return new GeneratedMaze(maze, output.get()); }); } @@ -92,6 +92,6 @@ class ParametersToMazeExtractor { return parameter.getParameterValue(this.queryParameters); } - public record GeneratedMaze(@NotNull Maze maze, @NotNull OutputType outputType, @NotNull String generatorName) { + public record GeneratedMaze(@NotNull Maze maze, @NotNull OutputType outputType) { } } diff --git a/src/main/java/ch/fritteli/maze/server/handler/RenderV3Handler.java b/src/main/java/ch/fritteli/maze/server/handler/RenderV3Handler.java new file mode 100644 index 0000000..0b8987e --- /dev/null +++ b/src/main/java/ch/fritteli/maze/server/handler/RenderV3Handler.java @@ -0,0 +1,61 @@ +package ch.fritteli.maze.server.handler; + +import ch.fritteli.maze.generator.model.Maze; +import ch.fritteli.maze.generator.serialization.v3.SerializerDeserializerV3; +import ch.fritteli.maze.server.OutputType; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderValues; +import io.undertow.util.Headers; +import io.undertow.util.StatusCodes; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +import java.nio.ByteBuffer; + +@Slf4j +public class RenderV3Handler extends AbstractHttpHandler { + + public static final String PATH_TEMPLATE = "/render/v3/{output}"; + + @Override + public void handle(@NotNull final HttpServerExchange exchange) { + log.debug("Handling render request"); + + if (exchange.isInIoThread()) { + exchange.dispatch(this); + return; + } + exchange.getRequestReceiver().receiveFullBytes((httpServerExchange, bytes) -> { + final OutputType output = this.getOutputType(httpServerExchange); + final byte[] render; + try { + final Maze maze = SerializerDeserializerV3.deserialize(bytes); + render = output.render(maze); + } catch (final Exception e) { + log.error("Error rendering binary maze data", e); + httpServerExchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) + .getResponseSender() + .send("Error rendering maze: %s".formatted(e.getMessage())); + return; + } + httpServerExchange + .setStatusCode(StatusCodes.OK) + .getResponseHeaders() + .put(Headers.CONTENT_TYPE, output.getContentType()); + httpServerExchange.getResponseSender() + .send(ByteBuffer.wrap(render)); + }); + } + + @NotNull + private OutputType getOutputType(@NotNull 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/maze/server/handler/RenderVxHandler.java b/src/main/java/ch/fritteli/maze/server/handler/RenderVxHandler.java new file mode 100644 index 0000000..5db984f --- /dev/null +++ b/src/main/java/ch/fritteli/maze/server/handler/RenderVxHandler.java @@ -0,0 +1,91 @@ +package ch.fritteli.maze.server.handler; + +import ch.fritteli.maze.generator.model.Maze; +import ch.fritteli.maze.generator.serialization.MazeConstants; +import ch.fritteli.maze.generator.serialization.v1.SerializerDeserializerV1; +import ch.fritteli.maze.generator.serialization.v2.SerializerDeserializerV2; +import ch.fritteli.maze.generator.serialization.v3.SerializerDeserializerV3; +import ch.fritteli.maze.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 lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +import java.nio.ByteBuffer; + +@Slf4j +public class RenderVxHandler implements HttpHandler { + public static final String PATH_TEMPLATE = "/render/dyn/{output}"; + + @Override + public void handleRequest(@NotNull final HttpServerExchange exchange) { + log.debug("Handling render request"); + + if (exchange.isInIoThread()) { + exchange.dispatch(this); + return; + } + exchange.getRequestReceiver().receiveFullBytes((httpServerExchange, bytes) -> { + final OutputType output = this.getOutputType(httpServerExchange); + final byte[] render; + try { + final Version version = this.getVersion(bytes); + final Maze maze = switch (version) { + case V1 -> SerializerDeserializerV1.deserialize(bytes); + case V2 -> SerializerDeserializerV2.deserialize(bytes); + case V3 -> SerializerDeserializerV3.deserialize(bytes); + }; + render = output.render(maze); + } catch (final Exception e) { + log.error("Error rendering binary maze data", e); + httpServerExchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) + .getResponseSender() + .send("Error rendering maze: %s".formatted(e.getMessage())); + return; + } + httpServerExchange + .setStatusCode(StatusCodes.OK) + .getResponseHeaders() + .put(Headers.CONTENT_TYPE, output.getContentType()); + httpServerExchange.getResponseSender() + .send(ByteBuffer.wrap(render)); + }); + } + + @NotNull + private Version getVersion(@NotNull final byte[] bytes) throws IllegalArgumentException { + if (bytes.length < 3) { + throw new IllegalArgumentException("Invalid input: too short"); + } + if (bytes[0] == MazeConstants.MAGIC_BYTE_1 && bytes[1] == MazeConstants.MAGIC_BYTE_2) { + final byte version = bytes[2]; + return switch (version) { + case SerializerDeserializerV1.VERSION_BYTE -> Version.V1; + case SerializerDeserializerV2.VERSION_BYTE -> Version.V2; + case SerializerDeserializerV3.VERSION_BYTE -> Version.V3; + default -> throw new IllegalArgumentException("Invalid version: " + version); + }; + } else { + throw new IllegalArgumentException("Invalid input: not a Maze file"); + } + } + + @NotNull + private OutputType getOutputType(@NotNull 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; + }); + } + + private enum Version { + V1, V2, V3; + } +} -- 2.45.2