Compare commits
3 commits
dbdfb0c8a4
...
a613a92adb
Author | SHA1 | Date | |
---|---|---|---|
a613a92adb | |||
87b16cb24a | |||
f370037289 |
6 changed files with 213 additions and 44 deletions
13
pom.xml
13
pom.xml
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
|
@ -57,7 +58,7 @@
|
|||
<properties>
|
||||
<maze-generator.version>0.2.1</maze-generator.version>
|
||||
<maven-site-plugin.version>4.0.0-M8</maven-site-plugin.version>
|
||||
<undertow.version>2.3.13.Final</undertow.version>
|
||||
<undertow.version>2.3.18.Final</undertow.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
@ -95,6 +96,11 @@
|
|||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -104,7 +110,8 @@
|
|||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>ch.fritteli.maze.server.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
|
|
|
@ -18,11 +18,11 @@ public class MazeServer {
|
|||
private final Undertow undertow;
|
||||
|
||||
private MazeServer(@NotNull final ServerConfig config) {
|
||||
final String hostAddress = config.getAddress().getHostAddress();
|
||||
final int port = config.getPort();
|
||||
final String hostAddress = config.address().getHostAddress();
|
||||
final int port = config.port();
|
||||
log.info("Starting Server at http://{}:{}/", hostAddress, port);
|
||||
final RoutingHandler routingHandler = new RoutingHandler()
|
||||
.get(CreateHandler.PATH_TEMPLATE, new CreateHandler())
|
||||
.get(CreateHandler.PATH_TEMPLATE, new CreateHandler(config.maxMazeHeight(), config.maxMazeWidth()))
|
||||
.post(RenderV1Handler.PATH_TEMPLATE, new RenderV1Handler())
|
||||
.post(RenderV2Handler.PATH_TEMPLATE, new RenderV2Handler());
|
||||
|
||||
|
@ -47,7 +47,7 @@ public class MazeServer {
|
|||
private void start() {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper"));
|
||||
this.undertow.start();
|
||||
final InetSocketAddress address = (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress();
|
||||
final InetSocketAddress address = (InetSocketAddress) this.undertow.getListenerInfo().getFirst().getAddress();
|
||||
final String hostAddress = address.getAddress().getHostAddress();
|
||||
final int port = address.getPort();
|
||||
|
||||
|
|
|
@ -1,42 +1,56 @@
|
|||
package ch.fritteli.maze.server;
|
||||
|
||||
import io.vavr.control.Option;
|
||||
import io.vavr.control.Try;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Value;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.wildfly.common.annotation.NotNull;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@Slf4j
|
||||
@Value
|
||||
public class ServerConfig {
|
||||
public record ServerConfig(@NotNull InetAddress address,
|
||||
int port,
|
||||
@NotNull Option<Integer> maxMazeHeight,
|
||||
@NotNull Option<Integer> maxMazeWidth) {
|
||||
public static final String SYSPROP_HOST = "fritteli.maze.server.host";
|
||||
public static final String SYSPROP_PORT = "fritteli.maze.server.port";
|
||||
public static final String SYSPROP_MAX_MAZE_HEIGHT = "fritteli.maze.maxheight";
|
||||
public static final String SYSPROP_MAX_MAZE_WIDTH = "fritteli.maze.maxwidth";
|
||||
|
||||
@NotNull
|
||||
InetAddress address;
|
||||
int port;
|
||||
|
||||
public ServerConfig(@Nullable final String address, final int port) throws ConfigurationException {
|
||||
this.address = validateAddress(address);
|
||||
public ServerConfig(@NotNull final InetAddress address,
|
||||
final int port,
|
||||
@NotNull final Option<Integer> maxMazeHeight,
|
||||
@NotNull final Option<Integer> maxMazeWidth) {
|
||||
this.address = address;
|
||||
this.port = validatePort(port);
|
||||
log.debug("host={}, port={}", this.address, this.port);
|
||||
this.maxMazeHeight = validateDimension(maxMazeHeight, "height");
|
||||
this.maxMazeWidth = validateDimension(maxMazeWidth, "width");
|
||||
log.debug("host={}, port={}, maxHeight={}, maxWidth={}", this.address, this.port, this.maxMazeHeight, this.maxMazeWidth);
|
||||
}
|
||||
|
||||
public ServerConfig(@Nullable final String address,
|
||||
final int port,
|
||||
@NotNull final Option<Integer> maxMazeHeight,
|
||||
@NotNull final Option<Integer> maxMazeWidth)
|
||||
throws ConfigurationException {
|
||||
this(validateAddress(address), port, maxMazeHeight, maxMazeWidth);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static ServerConfig init() throws ConfigurationException {
|
||||
final String host = System.getProperty(SYSPROP_HOST);
|
||||
final String portString = System.getProperty(SYSPROP_PORT);
|
||||
final String maxMazeHeightString = System.getProperty(SYSPROP_MAX_MAZE_HEIGHT);
|
||||
final String maxMazeWidthString = System.getProperty(SYSPROP_MAX_MAZE_WIDTH);
|
||||
final int port = validatePort(portString);
|
||||
return new ServerConfig(host, port);
|
||||
final Option<Integer> maxMazeHeight = validateDimension(maxMazeHeightString, "height", SYSPROP_MAX_MAZE_HEIGHT);
|
||||
final Option<Integer> maxMazeWidth = validateDimension(maxMazeWidthString, "width", SYSPROP_MAX_MAZE_WIDTH);
|
||||
return new ServerConfig(host, port, maxMazeHeight, maxMazeWidth);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static InetAddress validateAddress(@Nullable final String address) {
|
||||
private static InetAddress validateAddress(@Nullable final String address) throws ConfigurationException {
|
||||
return Try.of(() -> InetAddress.getByName(address))
|
||||
.getOrElseThrow(cause -> new ConfigurationException(
|
||||
"Invalid hostname/address: %s".formatted(address),
|
||||
|
@ -44,22 +58,48 @@ public class ServerConfig {
|
|||
));
|
||||
}
|
||||
|
||||
private static int validatePort(final int port) {
|
||||
private static int validatePort(final int port) throws ConfigurationException {
|
||||
if (port < 0 || port > 0xFFFF) {
|
||||
throw new ConfigurationException("Port out of range (0..65535): %s".formatted(port));
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
private static int validatePort(@Nullable final String portString) {
|
||||
private static int validatePort(@Nullable final String portString) throws ConfigurationException {
|
||||
if (portString == null) {
|
||||
log.info("No port configured; using default.");
|
||||
return 0;
|
||||
}
|
||||
return Try.of(() -> Integer.valueOf(portString))
|
||||
return Try.of(() -> Integer.parseInt(portString))
|
||||
.getOrElseThrow(cause -> new ConfigurationException(
|
||||
"Failed to parse port specified in system property '%s': %s".formatted(SYSPROP_PORT, portString),
|
||||
cause
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Option<Integer> validateDimension(@NotNull final Option<Integer> dimension,
|
||||
@NotNull final String identifier) {
|
||||
if (dimension.exists(d -> d <= 1)) {
|
||||
throw new ConfigurationException("Maximum %s must be greater than 1: %s"
|
||||
.formatted(identifier, dimension.get()));
|
||||
}
|
||||
return dimension;
|
||||
}
|
||||
|
||||
private static Option<Integer> validateDimension(@Nullable final String dimensionString,
|
||||
@NotNull final String identifier,
|
||||
@NotNull final String syspropName) {
|
||||
if (dimensionString == null) {
|
||||
log.info("No maximum {} configured; using default (unlimited).", identifier);
|
||||
return Option.none();
|
||||
}
|
||||
final int desiredDimension = Try.of(() -> Integer.parseInt(dimensionString))
|
||||
.getOrElseThrow(cause -> new ConfigurationException(
|
||||
"Failed to parse maximum %s specified in system property '%s': %s"
|
||||
.formatted(identifier, syspropName, dimensionString),
|
||||
cause
|
||||
));
|
||||
return Option.some(desiredDimension);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import io.undertow.server.HttpServerExchange;
|
|||
import io.undertow.util.Headers;
|
||||
import io.undertow.util.HttpString;
|
||||
import io.undertow.util.StatusCodes;
|
||||
import io.vavr.control.Option;
|
||||
import io.vavr.control.Try;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
@ -22,6 +23,15 @@ import java.util.Map;
|
|||
public class CreateHandler extends AbstractHttpHandler {
|
||||
|
||||
public static final String PATH_TEMPLATE = "/create/{output}";
|
||||
@NotNull
|
||||
private final Option<Integer> maxHeight;
|
||||
@NotNull
|
||||
private final Option<Integer> maxWidth;
|
||||
|
||||
public CreateHandler(@NotNull final Option<Integer> maxHeight, @NotNull final Option<Integer> maxWidth) {
|
||||
this.maxHeight = maxHeight;
|
||||
this.maxWidth = maxWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handle(@NotNull final HttpServerExchange exchange) {
|
||||
|
@ -29,10 +39,11 @@ public class CreateHandler extends AbstractHttpHandler {
|
|||
log.debug("Handling create request");
|
||||
this.createMazeFromRequestParameters(exchange.getQueryParameters())
|
||||
.onFailure(e -> {
|
||||
log.error("Error creating maze from request", e);
|
||||
if (e instanceof InvalidRequestParameterException) {
|
||||
log.debug("Error creating maze from request", e);
|
||||
exchange.setStatusCode(StatusCodes.BAD_REQUEST);
|
||||
} else {
|
||||
log.error("Error creating maze from request", e);
|
||||
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
exchange.getResponseSender()
|
||||
|
@ -75,6 +86,6 @@ public class CreateHandler extends AbstractHttpHandler {
|
|||
|
||||
@NotNull
|
||||
private Try<ParametersToMazeExtractor.GeneratedMaze> createMazeFromRequestParameters(final Map<String, Deque<String>> queryParameters) {
|
||||
return new ParametersToMazeExtractor(queryParameters).createMaze();
|
||||
return new ParametersToMazeExtractor(queryParameters, this.maxHeight, this.maxWidth).createMaze();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,10 @@ class ParametersToMazeExtractor {
|
|||
|
||||
@NotNull
|
||||
private final Map<String, Deque<String>> queryParameters;
|
||||
@NotNull
|
||||
private final Option<Integer> maxHeight;
|
||||
@NotNull
|
||||
private final Option<Integer> maxWidth;
|
||||
|
||||
@NotNull
|
||||
Try<GeneratedMaze> createMaze() {
|
||||
|
@ -49,12 +53,29 @@ class ParametersToMazeExtractor {
|
|||
)));
|
||||
}
|
||||
|
||||
final int desiredHeight = height.get();
|
||||
final int desiredWidth = width.get();
|
||||
|
||||
if (desiredHeight <= 1) {
|
||||
return Try.failure(new InvalidRequestParameterException("Specified height (%s) must be > 1".formatted(desiredHeight)));
|
||||
}
|
||||
if (desiredWidth <= 1) {
|
||||
return Try.failure(new InvalidRequestParameterException("Specified width (%s) must be > 1".formatted(desiredHeight)));
|
||||
}
|
||||
if (this.maxHeight.exists(max -> desiredHeight > max)) {
|
||||
return Try.failure(new InvalidRequestParameterException("Specified height (%s) is greater than allowed maximum of %s".formatted(desiredHeight, this.maxHeight.get())));
|
||||
}
|
||||
|
||||
if (this.maxWidth.exists(max -> desiredWidth > max)) {
|
||||
return Try.failure(new InvalidRequestParameterException("Specified width (%s) is greater than allowed maximum of %s".formatted(desiredWidth, this.maxWidth.get())));
|
||||
}
|
||||
|
||||
return Try.of(() -> {
|
||||
final Maze maze;
|
||||
if (start.isDefined() && end.isDefined()) {
|
||||
maze = new Maze(width.get(), height.get(), id.getOrElse(() -> new Random().nextLong()), start.get(), end.get());
|
||||
maze = new Maze(desiredWidth, desiredHeight, id.getOrElse(() -> new Random().nextLong()), start.get(), end.get());
|
||||
} else {
|
||||
maze = new Maze(width.get(), height.get(), id.getOrElse(() -> new Random().nextLong()));
|
||||
maze = new Maze(desiredWidth, desiredHeight, id.getOrElse(() -> new Random().nextLong()));
|
||||
}
|
||||
new RandomDepthFirst(maze).run();
|
||||
return new GeneratedMaze(maze, output.get(), RandomDepthFirst.class.getSimpleName());
|
||||
|
|
|
@ -3,16 +3,16 @@ package ch.fritteli.maze.server;
|
|||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
class ServerConfigTest {
|
||||
@BeforeEach
|
||||
void clearSysProperties() {
|
||||
System.clearProperty(ServerConfig.SYSPROP_HOST);
|
||||
System.clearProperty(ServerConfig.SYSPROP_PORT);
|
||||
System.clearProperty(ServerConfig.SYSPROP_MAX_MAZE_HEIGHT);
|
||||
System.clearProperty(ServerConfig.SYSPROP_MAX_MAZE_WIDTH);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -22,9 +22,27 @@ class ServerConfigTest {
|
|||
final ServerConfig sut = ServerConfig.init();
|
||||
|
||||
// assert
|
||||
assertEquals("127.0.0.1", sut.getAddress().getHostAddress());
|
||||
assertEquals("localhost", sut.getAddress().getHostName());
|
||||
assertEquals(0, sut.getPort());
|
||||
assertThat(sut.address())
|
||||
.satisfies(
|
||||
address -> assertThat(address.getHostAddress()).isEqualTo("127.0.0.1"),
|
||||
address -> assertThat(address.getHostName()).isEqualTo("localhost")
|
||||
);
|
||||
assertThat(sut.port()).isZero();
|
||||
assertThat(sut.maxMazeHeight()).isEmpty();
|
||||
assertThat(sut.maxMazeWidth()).isEmpty();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testInit_invalidAddress() {
|
||||
// arrange
|
||||
System.setProperty(ServerConfig.SYSPROP_HOST, "256.2515.19.0");
|
||||
|
||||
// act / assert
|
||||
assertThatExceptionOfType(ConfigurationException.class)
|
||||
.isThrownBy(ServerConfig::init)
|
||||
.withMessageContaining("Invalid hostname/address")
|
||||
.withMessageContaining("256.2515.19.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -33,29 +51,101 @@ class ServerConfigTest {
|
|||
System.setProperty(ServerConfig.SYSPROP_PORT, "Hello World!");
|
||||
|
||||
// act / assert
|
||||
assertThrows(ConfigurationException.class, ServerConfig::init);
|
||||
assertThatExceptionOfType(ConfigurationException.class)
|
||||
.isThrownBy(ServerConfig::init)
|
||||
.withMessageContaining("Failed to parse port")
|
||||
.withMessageContaining("Hello World!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInit_invalidPort() {
|
||||
void testInit_invalidPortTooSmall() {
|
||||
// arrange
|
||||
System.setProperty(ServerConfig.SYSPROP_PORT, "-5");
|
||||
|
||||
// act / assert
|
||||
assertThatExceptionOfType(ConfigurationException.class)
|
||||
.isThrownBy(ServerConfig::init)
|
||||
.withMessageContaining("Port out of range")
|
||||
.withMessageContaining("-5");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInit_invalidPortTooLarge() {
|
||||
// arrange
|
||||
System.setProperty(ServerConfig.SYSPROP_PORT, "99999");
|
||||
|
||||
// act / assert
|
||||
assertThrows(ConfigurationException.class, ServerConfig::init);
|
||||
assertThatExceptionOfType(ConfigurationException.class)
|
||||
.isThrownBy(ServerConfig::init)
|
||||
.withMessageContaining("Port out of range")
|
||||
.withMessageContaining("99999");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInit() throws ConfigurationException, UnknownHostException {
|
||||
void testInit_unparseableMaxMazeHeight() {
|
||||
// arrange
|
||||
System.setProperty(ServerConfig.SYSPROP_HOST, "127.0.0.1");
|
||||
System.setProperty(ServerConfig.SYSPROP_MAX_MAZE_HEIGHT, "Hello World!");
|
||||
|
||||
// act / assert
|
||||
assertThatExceptionOfType(ConfigurationException.class)
|
||||
.isThrownBy(ServerConfig::init)
|
||||
.withMessageContaining("Failed to parse maximum height")
|
||||
.withMessageContaining("Hello World!");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInit_invalidMaxMazeHeight() {
|
||||
// arrange
|
||||
System.setProperty(ServerConfig.SYSPROP_MAX_MAZE_HEIGHT, "1");
|
||||
|
||||
// act / assert
|
||||
assertThatExceptionOfType(ConfigurationException.class)
|
||||
.isThrownBy(ServerConfig::init)
|
||||
.withMessageContaining("Maximum height must be greater than 1:")
|
||||
.withMessageEndingWith("1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInit_unparseableMaxMazeWidth() {
|
||||
// arrange
|
||||
System.setProperty(ServerConfig.SYSPROP_MAX_MAZE_WIDTH, "Hello World!");
|
||||
|
||||
// act / assert
|
||||
assertThatExceptionOfType(ConfigurationException.class)
|
||||
.isThrownBy(ServerConfig::init)
|
||||
.withMessageContaining("Failed to parse maximum width")
|
||||
.withMessageContaining("Hello World!");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInit_invalidMaxMazeWidth() {
|
||||
// arrange
|
||||
System.setProperty(ServerConfig.SYSPROP_MAX_MAZE_WIDTH, "-24");
|
||||
|
||||
// act / assert
|
||||
assertThatExceptionOfType(ConfigurationException.class)
|
||||
.isThrownBy(ServerConfig::init)
|
||||
.withMessageContaining("Maximum width must be greater than 1:")
|
||||
.withMessageContaining("-24");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInit() throws ConfigurationException {
|
||||
// arrange
|
||||
System.setProperty(ServerConfig.SYSPROP_HOST, "10.0.9.12");
|
||||
System.setProperty(ServerConfig.SYSPROP_PORT, "12345");
|
||||
System.setProperty(ServerConfig.SYSPROP_MAX_MAZE_HEIGHT, "100");
|
||||
System.setProperty(ServerConfig.SYSPROP_MAX_MAZE_WIDTH, "42");
|
||||
|
||||
// act
|
||||
final ServerConfig sut = ServerConfig.init();
|
||||
|
||||
// assert
|
||||
assertEquals("127.0.0.1", sut.getAddress().getHostAddress());
|
||||
assertEquals(12345, sut.getPort());
|
||||
assertThat(sut.address().getHostAddress()).isEqualTo("10.0.9.12");
|
||||
assertThat(sut.port()).isEqualTo(12345);
|
||||
assertThat(sut.maxMazeHeight()).singleElement().isEqualTo(100);
|
||||
assertThat(sut.maxMazeWidth()).singleElement().isEqualTo(42);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue