feature/undertow #4

Merged
manuel merged 15 commits from feature/undertow into master 2023-04-08 22:40:46 +02:00
5 changed files with 123 additions and 105 deletions
Showing only changes of commit a194e5db62 - Show all commits

View file

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

View file

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

View file

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

View file

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

View file

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