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…
	
	Add table
		Add a link
		
	
		Reference in a new issue