feature/algorithms #9

Merged
manuel merged 2 commits from feature/algorithms into master 2024-12-24 03:35:17 +01:00
10 changed files with 233 additions and 11 deletions

View file

@ -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

View file

@ -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>

View 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);
}
}

View file

@ -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)

View file

@ -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;

View file

@ -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()

View file

@ -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) {
}
}

View file

@ -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;
});
}
}

View file

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

View file

@ -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