feature/algorithms #9
10 changed files with 233 additions and 11 deletions
|
@ -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
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -56,7 +56,7 @@
|
|||
</distributionManagement>
|
||||
|
||||
<properties>
|
||||
<maze-generator.version>0.2.1</maze-generator.version>
|
||||
<maze-generator.version>0.3.0</maze-generator.version>
|
||||
<maven-site-plugin.version>4.0.0-M8</maven-site-plugin.version>
|
||||
<undertow.version>2.3.18.Final</undertow.version>
|
||||
</properties>
|
||||
|
|
45
src/main/java/ch/fritteli/maze/server/Algorithm.java
Normal file
45
src/main/java/ch/fritteli/maze/server/Algorithm.java
Normal file
|
@ -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<Maze, MazeGeneratorAlgorithm> creator;
|
||||
|
||||
@Getter
|
||||
@NotNull
|
||||
private final List<String> names;
|
||||
|
||||
Algorithm(@NotNull final Function<Maze, MazeGeneratorAlgorithm> creator,
|
||||
@NotNull final String... names) {
|
||||
this.creator = creator;
|
||||
this.names = List.of(names);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Option<Algorithm> 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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<Long> id = getParameterValue(RequestParameter.ID);
|
||||
final Option<Position> start = getParameterValue(RequestParameter.START);
|
||||
final Option<Position> end = getParameterValue(RequestParameter.END);
|
||||
final Option<Algorithm> 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) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.<OutputType>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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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.<OutputType>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;
|
||||
}
|
||||
}
|
|
@ -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<String, Option<?>> extractor;
|
||||
@Getter
|
||||
|
|
Loading…
Reference in a new issue