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; + } +}