feature/undertow #4
					 5 changed files with 123 additions and 105 deletions
				
			
		
							
								
								
									
										6
									
								
								pom.xml
									
										
									
									
									
								
							
							
						
						
									
										6
									
								
								pom.xml
									
										
									
									
									
								
							|  | @ -20,9 +20,11 @@ | ||||||
|         <java.target.version>17</java.target.version> |         <java.target.version>17</java.target.version> | ||||||
|         <jetbrains-annotations.version>24.0.1</jetbrains-annotations.version> |         <jetbrains-annotations.version>24.0.1</jetbrains-annotations.version> | ||||||
|         <junit-jupiter.version>5.9.2</junit-jupiter.version> |         <junit-jupiter.version>5.9.2</junit-jupiter.version> | ||||||
|  |         <labyrinth-generator.version>0.0.3</labyrinth-generator.version> | ||||||
|         <logback.version>1.4.6</logback.version> |         <logback.version>1.4.6</logback.version> | ||||||
|         <lombok.version>1.18.26</lombok.version> |         <lombok.version>1.18.26</lombok.version> | ||||||
|         <slf4j.version>2.0.7</slf4j.version> |         <slf4j.version>2.0.7</slf4j.version> | ||||||
|  |         <undertow.version>2.3.5.Final</undertow.version> | ||||||
|         <vavr.version>0.10.4</vavr.version> |         <vavr.version>0.10.4</vavr.version> | ||||||
|     </properties> |     </properties> | ||||||
| 
 | 
 | ||||||
|  | @ -46,7 +48,7 @@ | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>ch.fritteli.labyrinth</groupId> |             <groupId>ch.fritteli.labyrinth</groupId> | ||||||
|             <artifactId>labyrinth-generator</artifactId> |             <artifactId>labyrinth-generator</artifactId> | ||||||
|             <version>0.0.3</version> |             <version>${labyrinth-generator.version}</version> | ||||||
|         </dependency> |         </dependency> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>io.vavr</groupId> |             <groupId>io.vavr</groupId> | ||||||
|  | @ -73,7 +75,7 @@ | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>io.undertow</groupId> |             <groupId>io.undertow</groupId> | ||||||
|             <artifactId>undertow-core</artifactId> |             <artifactId>undertow-core</artifactId> | ||||||
|             <version>2.3.5.Final</version> |             <version>${undertow.version}</version> | ||||||
|         </dependency> |         </dependency> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>org.junit.jupiter</groupId> |             <groupId>org.junit.jupiter</groupId> | ||||||
|  |  | ||||||
|  | @ -1,9 +1,12 @@ | ||||||
| package ch.fritteli.labyrinth.server; | package ch.fritteli.labyrinth.server; | ||||||
| 
 | 
 | ||||||
|  | import lombok.experimental.UtilityClass; | ||||||
| import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||||
| 
 | 
 | ||||||
| @Slf4j | @Slf4j | ||||||
|  | @UtilityClass | ||||||
| public class Main { | public class Main { | ||||||
|  | 
 | ||||||
|     public static void main(String[] args) { |     public static void main(String[] args) { | ||||||
|         LabyrinthServer.createAndStartServer() |         LabyrinthServer.createAndStartServer() | ||||||
|                 .onFailure(e -> log.error("Failed to create server. Stopping.", e)); |                 .onFailure(e -> log.error("Failed to create server. Stopping.", e)); | ||||||
|  |  | ||||||
|  | @ -88,7 +88,7 @@ public enum OutputType { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     public byte[] render(@NonNull final Labyrinth labyrinth) throws Exception { |     public byte[] render(@NonNull final Labyrinth labyrinth) { | ||||||
|         return this.render.apply(labyrinth); |         return this.render.apply(labyrinth); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,33 +6,20 @@ import io.undertow.server.HttpServerExchange; | ||||||
| import io.undertow.util.Headers; | import io.undertow.util.Headers; | ||||||
| import io.undertow.util.HttpString; | import io.undertow.util.HttpString; | ||||||
| import io.undertow.util.StatusCodes; | import io.undertow.util.StatusCodes; | ||||||
| import io.vavr.Tuple; |  | ||||||
| import io.vavr.Tuple2; | 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 io.vavr.control.Try; | ||||||
| import lombok.Getter; |  | ||||||
| import lombok.NonNull; |  | ||||||
| import lombok.extern.slf4j.Slf4j; |  | ||||||
| import org.jetbrains.annotations.Nullable; |  | ||||||
| import org.slf4j.MDC; |  | ||||||
| 
 |  | ||||||
| import java.nio.ByteBuffer; | import java.nio.ByteBuffer; | ||||||
| import java.time.Instant; | import java.time.Instant; | ||||||
| import java.time.temporal.ChronoUnit; | import java.time.temporal.ChronoUnit; | ||||||
| import java.util.Deque; | import java.util.Deque; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.Random; | import lombok.NonNull; | ||||||
| import java.util.function.Function; | import lombok.extern.slf4j.Slf4j; | ||||||
|  | import org.slf4j.MDC; | ||||||
| 
 | 
 | ||||||
| @Slf4j | @Slf4j | ||||||
| public class CreateHandler extends AbstractHttpHandler { | public class CreateHandler extends AbstractHttpHandler { | ||||||
|  | 
 | ||||||
|     public static final String PATH_TEMPLATE = "/create/{output}"; |     public static final String PATH_TEMPLATE = "/create/{output}"; | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -77,89 +64,4 @@ public class CreateHandler extends AbstractHttpHandler { | ||||||
|     private Try<Tuple2<OutputType, Labyrinth>> createLabyrinthFromRequestParameters(final Map<String, Deque<String>> queryParameters) { |     private Try<Tuple2<OutputType, Labyrinth>> createLabyrinthFromRequestParameters(final Map<String, Deque<String>> queryParameters) { | ||||||
|         return new ParametersToLabyrinthExtractor(queryParameters).createLabyrinth(); |         return new ParametersToLabyrinthExtractor(queryParameters).createLabyrinth(); | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     private enum RequestParameter { |  | ||||||
|         WIDTH(p -> Try.of(() -> Integer.parseInt(p)).toOption(), "w", "width"), |  | ||||||
|         HEIGHT(p -> Try.of(() -> Integer.parseInt(p)).toOption(), "h", "height"), |  | ||||||
|         ID(p -> Try.of(() -> Long.parseLong(p)).toOption(), "i", "id"), |  | ||||||
|         OUTPUT(OutputType::ofString, "o", "output"); |  | ||||||
|         @NonNull |  | ||||||
|         private final Function<String, Option<?>> extractor; |  | ||||||
|         @Getter |  | ||||||
|         @NonNull |  | ||||||
|         private final Set<String> names; |  | ||||||
| 
 |  | ||||||
|         RequestParameter(@NonNull final String... names) { |  | ||||||
|             this.extractor = null; |  | ||||||
|             this.names = HashSet.of(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 <T> Option<T> extractParameterValue(@NonNull final String parameter) { |  | ||||||
|             return (Option<T>) this.extractor.apply(parameter); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static class ParametersToLabyrinthExtractor { |  | ||||||
|         @NonNull |  | ||||||
|         private final Multimap<String, String> queryParameters; |  | ||||||
| 
 |  | ||||||
|         ParametersToLabyrinthExtractor(@NonNull final Map<String, Deque<String>> queryParameters) { |  | ||||||
|             this.queryParameters = HashMap.ofAll(queryParameters).foldLeft(HashMultimap.<String>withSet().empty(), (map, tuple) -> { |  | ||||||
|                 final String key = tuple._1(); |  | ||||||
|                 return Stream.ofAll(tuple._2()).foldLeft(map, (m, value) -> m.put(key, value)); |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @NonNull |  | ||||||
|         Try<Tuple2<OutputType, Labyrinth>> createLabyrinth() { |  | ||||||
|             final Option<OutputType> output = getParameterValue(RequestParameter.OUTPUT); |  | ||||||
|             final Option<Integer> width = getParameterValue(RequestParameter.WIDTH); |  | ||||||
|             final Option<Integer> height = getParameterValue(RequestParameter.HEIGHT); |  | ||||||
|             final Option<Long> id = getParameterValue(RequestParameter.ID); |  | ||||||
| 
 |  | ||||||
|             if (output.isEmpty()) { |  | ||||||
|                 return Try.failure(new IllegalArgumentException("Path parameter %s is required and must be one of: %s".formatted( |  | ||||||
|                         RequestParameter.OUTPUT.getNames().mkString("'", " / ", "'"), |  | ||||||
|                         Stream.of(OutputType.values()) |  | ||||||
|                                 .flatMap(OutputType::getNames) |  | ||||||
|                                 .mkString(", ") |  | ||||||
|                 ))); |  | ||||||
|             } |  | ||||||
|             if (width.isEmpty()) { |  | ||||||
|                 return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( |  | ||||||
|                         RequestParameter.WIDTH.getNames().mkString("'", " / ", "'") |  | ||||||
|                 ))); |  | ||||||
|             } |  | ||||||
|             if (height.isEmpty()) { |  | ||||||
|                 return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( |  | ||||||
|                         RequestParameter.HEIGHT.getNames().mkString("'", " / ", "'") |  | ||||||
|                 ))); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return Try.of(() -> Tuple.of(output.get(), new Labyrinth(width.get(), height.get(), id.getOrElse(() -> new Random().nextLong())))); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @NonNull |  | ||||||
|         private <T> Option<T> getParameterValue(@NonNull final RequestParameter parameter) { |  | ||||||
|             return this.getParameterValues(parameter) |  | ||||||
|                     .foldLeft(Option.none(), (type, param) -> type.orElse(() -> parameter.extractParameterValue(param))); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @NonNull |  | ||||||
|         private Stream<String> getParameterValues(@NonNull final RequestParameter parameter) { |  | ||||||
|             return parameter.names.toStream().flatMap(name -> Stream.ofAll(this.queryParameters.getOrElse(name, List.empty()))); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,111 @@ | ||||||
|  | package ch.fritteli.labyrinth.server.handler; | ||||||
|  | 
 | ||||||
|  | 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; | ||||||
|  | 
 | ||||||
|  | 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) | ||||||
|  |                 ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     Try<Tuple2<OutputType, Labyrinth>> createLabyrinth() { | ||||||
|  |         final Option<OutputType> output = getParameterValue(RequestParameter.OUTPUT); | ||||||
|  |         final Option<Integer> width = getParameterValue(RequestParameter.WIDTH); | ||||||
|  |         final Option<Integer> height = getParameterValue(RequestParameter.HEIGHT); | ||||||
|  |         final Option<Long> id = getParameterValue(RequestParameter.ID); | ||||||
|  | 
 | ||||||
|  |         if (output.isEmpty()) { | ||||||
|  |             return Try.failure(new IllegalArgumentException("Path parameter %s is required and must be one of: %s".formatted( | ||||||
|  |                     RequestParameter.OUTPUT.getNames().mkString("'", " / ", "'"), | ||||||
|  |                     Stream.of(OutputType.values()) | ||||||
|  |                             .flatMap(OutputType::getNames) | ||||||
|  |                             .mkString(", ") | ||||||
|  |             ))); | ||||||
|  |         } | ||||||
|  |         if (width.isEmpty()) { | ||||||
|  |             return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( | ||||||
|  |                     RequestParameter.WIDTH.getNames().mkString("'", " / ", "'") | ||||||
|  |             ))); | ||||||
|  |         } | ||||||
|  |         if (height.isEmpty()) { | ||||||
|  |             return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( | ||||||
|  |                     RequestParameter.HEIGHT.getNames().mkString("'", " / ", "'") | ||||||
|  |             ))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return Try.of(() -> Tuple.of(output.get(), new Labyrinth(width.get(), height.get(), id.getOrElse(() -> new Random().nextLong())))); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @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); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue