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..d6efbf3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,7 +56,7 @@
- 0.2.1
+ 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
new file mode 100644
index 0000000..6d4f29f
--- /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.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/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 6c747e5..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,8 +1,9 @@
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;
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,8 +79,11 @@ class ParametersToMazeExtractor {
} else {
maze = new Maze(desiredWidth, desiredHeight, id.getOrElse(() -> new Random().nextLong()));
}
- new RandomDepthFirst(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());
});
}
@@ -87,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;
+ }
+}
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