feature/undertow #4
					 7 changed files with 98 additions and 77 deletions
				
			
		
							
								
								
									
										2
									
								
								pom.xml
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								pom.xml
									
										
									
									
									
								
							|  | @ -20,7 +20,7 @@ | |||
|         <java.target.version>17</java.target.version> | ||||
|         <jetbrains-annotations.version>24.0.1</jetbrains-annotations.version> | ||||
|         <junit-jupiter.version>5.9.2</junit-jupiter.version> | ||||
|         <labyrinth-generator.version>0.0.3</labyrinth-generator.version> | ||||
|         <labyrinth-generator.version>0.0.4</labyrinth-generator.version> | ||||
|         <logback.version>1.4.6</logback.version> | ||||
|         <lombok.version>1.18.26</lombok.version> | ||||
|         <slf4j.version>2.0.7</slf4j.version> | ||||
|  |  | |||
|  | @ -5,24 +5,23 @@ import ch.fritteli.labyrinth.server.handler.RenderHandler; | |||
| import io.undertow.Undertow; | ||||
| import io.undertow.server.RoutingHandler; | ||||
| import io.vavr.control.Try; | ||||
| import java.net.InetSocketAddress; | ||||
| import lombok.NonNull; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| 
 | ||||
| @Slf4j | ||||
| public class LabyrinthServer { | ||||
|     @NonNull | ||||
|     private final ServerConfig config; | ||||
| 
 | ||||
|     @NonNull | ||||
|     private final Undertow undertow; | ||||
| 
 | ||||
|     private LabyrinthServer(@NonNull final ServerConfig config) { | ||||
|         this.config = config; | ||||
|         final String hostAddress = config.getAddress().getHostAddress(); | ||||
|         final int port = config.getPort(); | ||||
|         log.info("Starting Server at http://{}:{}/", hostAddress, port); | ||||
|         final RoutingHandler routingHandler = new RoutingHandler() | ||||
|                 .get(CreateHandler.PATH_TEMPLATE, new CreateHandler()) | ||||
|                 .post("/render", new RenderHandler()); | ||||
|                 .post(RenderHandler.PATH_TEMPLATE, new RenderHandler()); | ||||
| 
 | ||||
|         this.undertow = Undertow.builder() | ||||
|                 .addHttpListener(port, hostAddress) | ||||
|  | @ -45,7 +44,11 @@ public class LabyrinthServer { | |||
|     private void start() { | ||||
|         Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); | ||||
|         this.undertow.start(); | ||||
|         log.info("Listening on http://{}:{}", this.config.getAddress().getHostAddress(), this.config.getPort()); | ||||
|         final InetSocketAddress address = (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress(); | ||||
|         final String hostAddress = address.getAddress().getHostAddress(); | ||||
|         final int port = address.getPort(); | ||||
| 
 | ||||
|         log.info("Listening on http://{}:{}", hostAddress, port); | ||||
|     } | ||||
| 
 | ||||
|     private void stop() { | ||||
|  |  | |||
|  | @ -30,13 +30,13 @@ public enum OutputType { | |||
|             "html"), | ||||
|     PDF("application/pdf", | ||||
|             "pdf", | ||||
|             labyrinth -> PDFRenderer.newInstance().render(labyrinth), | ||||
|             labyrinth -> PDFRenderer.newInstance().render(labyrinth).toByteArray(), | ||||
|             false, | ||||
|             "p", | ||||
|             "pdf"), | ||||
|     PDFFILE("application/pdf", | ||||
|             "pdf", | ||||
|             labyrinth -> PDFRenderer.newInstance().render(labyrinth), | ||||
|             labyrinth -> PDFRenderer.newInstance().render(labyrinth).toByteArray(), | ||||
|             true, | ||||
|             "f", | ||||
|             "pdffile"), | ||||
|  |  | |||
|  | @ -55,6 +55,15 @@ public class CreateHandler extends AbstractHttpHandler { | |||
|                             .put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth())) | ||||
|                             .put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(labyrinth.getHeight())) | ||||
|                             .put(HttpString.tryFromString("X-Labyrinth-Generation-Duration-millis"), String.valueOf(durationMillis)); | ||||
|                     if (outputType.isAttachment()) { | ||||
|                         exchange.getResponseHeaders() | ||||
|                                 .put(Headers.CONTENT_DISPOSITION, "attachment; filename=\"labyrinth-%dx%d-%d.%s\"".formatted( | ||||
|                                         labyrinth.getWidth(), | ||||
|                                         labyrinth.getHeight(), | ||||
|                                         labyrinth.getRandomSeed(), | ||||
|                                         outputType.getFileExtension() | ||||
|                                 )); | ||||
|                     } | ||||
|                     exchange.getResponseSender().send(ByteBuffer.wrap(bytes)); | ||||
|                     log.debug("Create request handled in {}ms.", durationMillis); | ||||
|                 }); | ||||
|  |  | |||
|  | @ -4,39 +4,20 @@ import ch.fritteli.labyrinth.generator.model.Labyrinth; | |||
| import ch.fritteli.labyrinth.server.OutputType; | ||||
| import io.vavr.Tuple; | ||||
| import io.vavr.Tuple2; | ||||
| import io.vavr.collection.HashMap; | ||||
| import io.vavr.collection.HashMultimap; | ||||
| import io.vavr.collection.HashSet; | ||||
| import io.vavr.collection.List; | ||||
| import io.vavr.collection.Multimap; | ||||
| import io.vavr.collection.Set; | ||||
| import io.vavr.collection.Stream; | ||||
| import io.vavr.control.Option; | ||||
| import io.vavr.control.Try; | ||||
| import java.util.Deque; | ||||
| import java.util.Map; | ||||
| import java.util.Random; | ||||
| import java.util.function.Function; | ||||
| import lombok.Getter; | ||||
| import lombok.NonNull; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.jetbrains.annotations.Nullable; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| 
 | ||||
| @RequiredArgsConstructor | ||||
| class ParametersToLabyrinthExtractor { | ||||
| 
 | ||||
|     @NonNull | ||||
|     private final Multimap<RequestParameter, ?> queryParameters; | ||||
| 
 | ||||
|     ParametersToLabyrinthExtractor(@NonNull final Map<String, Deque<String>> queryParameters) { | ||||
|         this.queryParameters = HashMap.ofAll(queryParameters) | ||||
|                 .foldLeft( | ||||
|                         HashMultimap.withSet().empty(), | ||||
|                         (map, tuple) -> RequestParameter.parseName(tuple._1()).map(parameter -> Stream.ofAll(tuple._2()) | ||||
|                                         .flatMap(parameter::extractParameterValue) | ||||
|                                         .foldLeft(map, (m, value) -> m.put(parameter, value))) | ||||
|                                 .getOrElse(map) | ||||
|                 ); | ||||
|     } | ||||
|     private final Map<String, Deque<String>> queryParameters; | ||||
| 
 | ||||
|     @NonNull | ||||
|     Try<Tuple2<OutputType, Labyrinth>> createLabyrinth() { | ||||
|  | @ -69,43 +50,6 @@ class ParametersToLabyrinthExtractor { | |||
| 
 | ||||
|     @NonNull | ||||
|     private <T> Option<T> getParameterValue(@NonNull final RequestParameter parameter) { | ||||
|         return (Option<T>) this.queryParameters.getOrElse(parameter, List.empty()) | ||||
|                 .headOption(); | ||||
|     } | ||||
| 
 | ||||
|     @Slf4j | ||||
|     private enum RequestParameter { | ||||
|         WIDTH(p -> Try.of(() -> Integer.parseInt(p)) | ||||
|                 .toOption() | ||||
|                 .onEmpty(() -> log.debug("Unparseable value for parameter 'width': '{}'", p)), "w", "width"), | ||||
|         HEIGHT(p -> Try.of(() -> Integer.parseInt(p)) | ||||
|                 .toOption() | ||||
|                 .onEmpty(() -> log.debug("Unparseable value for parameter 'height': '{}'", p)), "h", "height"), | ||||
|         ID(p -> Try.of(() -> Long.parseLong(p)) | ||||
|                 .toOption() | ||||
|                 .onEmpty(() -> log.debug("Unparseable value for parameter 'id': '{}'", p)), "i", "id"), | ||||
|         OUTPUT(p -> OutputType.ofString(p) | ||||
|                 .onEmpty(() -> log.debug("Unparseable value for parameter 'output': '{}'", p)), "o", "output"); | ||||
|         @NonNull | ||||
|         private final Function<String, Option<?>> extractor; | ||||
|         @Getter | ||||
|         @NonNull | ||||
|         private final Set<String> names; | ||||
| 
 | ||||
|         RequestParameter(@NonNull final Function<String, Option<?>> extractor, @NonNull final String... names) { | ||||
|             this.extractor = extractor; | ||||
|             this.names = HashSet.of(names); | ||||
|         } | ||||
| 
 | ||||
|         static Option<RequestParameter> parseName(@Nullable final String name) { | ||||
|             if (name == null) { | ||||
|                 return Option.none(); | ||||
|             } | ||||
|             return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull Option<?> extractParameterValue(@NonNull final String parameter) { | ||||
|             return this.extractor.apply(parameter); | ||||
|         } | ||||
|         return parameter.getParameterValue(this.queryParameters); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -3,17 +3,19 @@ package ch.fritteli.labyrinth.server.handler; | |||
| import ch.fritteli.labyrinth.generator.model.Labyrinth; | ||||
| import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer; | ||||
| import ch.fritteli.labyrinth.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 io.vavr.control.Option; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| 
 | ||||
| import java.nio.ByteBuffer; | ||||
| import lombok.NonNull; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| 
 | ||||
| @Slf4j | ||||
| public class RenderHandler extends AbstractHttpHandler { | ||||
| 
 | ||||
|     public static final String PATH_TEMPLATE = "/render/{output}"; | ||||
| 
 | ||||
|     @Override | ||||
|     public void handle(final HttpServerExchange exchange) { | ||||
|         log.debug("Handling render request"); | ||||
|  | @ -23,13 +25,10 @@ public class RenderHandler extends AbstractHttpHandler { | |||
|             return; | ||||
|         } | ||||
|         exchange.getRequestReceiver().receiveFullBytes((httpServerExchange, bytes) -> { | ||||
|             final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); | ||||
|             final OutputType output = Option.of(httpServerExchange.getRequestHeaders().get(Headers.ACCEPT)) | ||||
|                     .exists(values -> values.contains(OutputType.HTML.getContentType())) ? | ||||
|                     OutputType.HTML : | ||||
|                     OutputType.TEXT_PLAIN; | ||||
|             final OutputType output = this.getOutputType(httpServerExchange); | ||||
|             final byte[] render; | ||||
|             try { | ||||
|                 final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); | ||||
|                 render = output.render(labyrinth); | ||||
|             } catch (final Exception e) { | ||||
|                 log.error("Error rendering binary labyrinth data", e); | ||||
|  | @ -46,4 +45,16 @@ public class RenderHandler extends AbstractHttpHandler { | |||
|                     .send(ByteBuffer.wrap(render)); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private OutputType getOutputType(@NonNull 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,54 @@ | |||
| package ch.fritteli.labyrinth.server.handler; | ||||
| 
 | ||||
| import ch.fritteli.labyrinth.server.OutputType; | ||||
| import io.vavr.Tuple2; | ||||
| import io.vavr.collection.HashMap; | ||||
| import io.vavr.collection.HashSet; | ||||
| import io.vavr.collection.Set; | ||||
| import io.vavr.control.Option; | ||||
| import io.vavr.control.Try; | ||||
| import java.util.Deque; | ||||
| import java.util.Map; | ||||
| import java.util.function.Function; | ||||
| import lombok.Getter; | ||||
| import lombok.NonNull; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| 
 | ||||
| @Slf4j | ||||
| enum RequestParameter { | ||||
|     WIDTH(p -> Try.of(() -> Integer.parseInt(p)) | ||||
|             .toOption() | ||||
|             .onEmpty(() -> log.debug("Unparseable value for parameter 'width': '{}'", p)), "w", "width"), | ||||
|     HEIGHT(p -> Try.of(() -> Integer.parseInt(p)) | ||||
|             .toOption() | ||||
|             .onEmpty(() -> log.debug("Unparseable value for parameter 'height': '{}'", p)), "h", "height"), | ||||
|     ID(p -> Try.of(() -> Long.parseLong(p)) | ||||
|             .toOption() | ||||
|             .onEmpty(() -> log.debug("Unparseable value for parameter 'id': '{}'", p)), "i", "id"), | ||||
|     OUTPUT(p -> OutputType.ofString(p) | ||||
|             .onEmpty(() -> log.debug("Unparseable value for parameter 'output': '{}'", p)), "o", "output"); | ||||
|     @NonNull | ||||
|     private final Function<String, Option<?>> extractor; | ||||
|     @Getter | ||||
|     @NonNull | ||||
|     private final Set<String> names; | ||||
| 
 | ||||
|     RequestParameter(@NonNull final Function<String, Option<?>> extractor, @NonNull final String... names) { | ||||
|         this.extractor = extractor; | ||||
|         this.names = HashSet.of(names); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     Option<?> extractParameterValue(@NonNull final String parameter) { | ||||
|         return this.extractor.apply(parameter); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public <T> Option<T> getParameterValue(@NonNull final Map<String, Deque<String>> queryParameters) { | ||||
|         return (Option<T>) HashMap.ofAll(queryParameters) | ||||
|                 .filterKeys(this.names::contains) | ||||
|                 .flatMap(Tuple2::_2) | ||||
|                 .flatMap(this::extractParameterValue) | ||||
|                 .headOption(); | ||||
|     } | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue