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