diff --git a/labyrinth-server.iml b/labyrinth-server.iml
index fd372d5..8d460f3 100644
--- a/labyrinth-server.iml
+++ b/labyrinth-server.iml
@@ -13,12 +13,18 @@
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 28f3f51..b18d2cf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,6 +28,25 @@
org.projectlombok
lombok
+
+ org.jetbrains
+ annotations
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.30
+
+
+ org.slf4j
+ slf4j-simple
+ 1.7.30
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
diff --git a/src/main/java/ch/fritteli/labyrinth/server/ConfigurationException.java b/src/main/java/ch/fritteli/labyrinth/server/ConfigurationException.java
new file mode 100644
index 0000000..1905a95
--- /dev/null
+++ b/src/main/java/ch/fritteli/labyrinth/server/ConfigurationException.java
@@ -0,0 +1,13 @@
+package ch.fritteli.labyrinth.server;
+
+import org.jetbrains.annotations.Nullable;
+
+public class ConfigurationException extends RuntimeException {
+ public ConfigurationException(@Nullable final String message) {
+ super(message);
+ }
+
+ public ConfigurationException(@Nullable final String message, @Nullable final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java
index 93fd8a1..94c7892 100644
--- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java
+++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java
@@ -5,6 +5,7 @@ import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer;
import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer;
import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer;
import com.sun.net.httpserver.Headers;
+import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import io.vavr.collection.HashMap;
import io.vavr.collection.HashSet;
@@ -16,79 +17,32 @@ import io.vavr.control.Option;
import io.vavr.control.Try;
import lombok.Getter;
import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
+@Slf4j
public class LabyrinthServer {
-
private final HttpServer httpServer;
- public LabyrinthServer(final int port) throws IOException {
- this.httpServer = HttpServer.create(new InetSocketAddress(port), 0);
- this.httpServer.createContext("/create", exchange -> {
- final String requestMethod = exchange.getRequestMethod();
- if (!requestMethod.equals("GET")) {
- exchange.getResponseBody().close();
- exchange.sendResponseHeaders(405, -1);
- exchange.close();
- return;
- }
- final Map requestParams = this.parseQueryString(exchange.getRequestURI().getQuery());
- final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 1);
- final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 1);
- final Option seedOption = requestParams.get(RequestParameter.ID).toTry().map(Long::valueOf).toOption();
- final long seed;
- final Option outputOption = requestParams.get(RequestParameter.OUTPUT).flatMap(OutputType::ofString);
- final OutputType output;
- final Headers responseHeaders = exchange.getResponseHeaders();
- boolean needsRedirect = false;
- if (seedOption.isEmpty()) {
- needsRedirect = true;
- seed = System.nanoTime();
- } else {
- seed = seedOption.get();
- }
- if (outputOption.isEmpty()) {
- needsRedirect = true;
- output = OutputType.HTML;
- } else {
- output = outputOption.get();
- }
- if (needsRedirect) {
- responseHeaders.add("Location", "/create?width=" + width + "&height=" + height + "&output=" + output.toString() + "&id=" + seed);
- exchange.sendResponseHeaders(302, -1);
- exchange.close();
- return;
- }
- responseHeaders.add("Content-type", output.getContentType());
- exchange.sendResponseHeaders(200, 0);
- final Labyrinth labyrinth = new Labyrinth(width, height, seed);
- final byte[] render = output.render(labyrinth);
- final OutputStream responseBody = exchange.getResponseBody();
- responseBody.write(render);
- responseBody.flush();
- exchange.close();
- });
+ public LabyrinthServer(@NonNull final ServerConfig config) throws IOException {
+ this.httpServer = HttpServer.create(new InetSocketAddress(config.getAddress(), config.getPort()), 5);
+ this.httpServer.createContext("/create", this::handleCreate);
}
- public static Option createListener() {
- final String listenerPort = System.getProperty("fritteli.labyrinth.listenerport");
- final Option listenerOption = Option.of(listenerPort)
- .toTry()
- .map(Integer::valueOf)
- .onFailure(cause -> System.err.println("Invalid port specified via system property 'fritteli.labyrinth.listenerport': "
- + listenerPort
- + ". Not starting webserver."))
+ public static Option createAndStartServer() {
+ final Option serverOption = Try.of(ServerConfig::init)
.mapTry(LabyrinthServer::new)
- .onFailure(cause -> System.err.println("Failed to create Listener: " + cause))
+ .onFailure(cause -> log.error("Failed to create LabyrinthServer.", cause))
.toOption();
- listenerOption.forEach(LabyrinthServer::start);
- return listenerOption;
+ serverOption.forEach(LabyrinthServer::start);
+ return serverOption;
}
private T getOrDefault(@NonNull final Option input, @NonNull final Function mapper, @Nullable final T defaultValue) {
@@ -128,13 +82,47 @@ public class LabyrinthServer {
public void start() {
Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper"));
this.httpServer.start();
- System.out.println("Listening on " + this.httpServer.getAddress());
+ log.info("Listening on {}", this.httpServer.getAddress());
}
public void stop() {
- System.out.println("Stopping listener ...");
+ log.info("Stopping server ...");
this.httpServer.stop(5);
- System.out.println("Listener stopped.");
+ log.info("Server stopped.");
+ }
+
+ private void handleCreate(HttpExchange exchange) throws IOException {
+ log.debug("Handling request to {}", exchange.getRequestURI());
+ final String requestMethod = exchange.getRequestMethod();
+ if (!requestMethod.equals("GET")) {
+ exchange.getResponseBody().close();
+ exchange.sendResponseHeaders(405, -1);
+ exchange.close();
+ return;
+ }
+ final Map requestParams = this.parseQueryString(exchange.getRequestURI().getQuery());
+ final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 1);
+ final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 1);
+ final Option idOption = requestParams.get(RequestParameter.ID).toTry().map(Long::valueOf).toOption();
+ final Option outputOption = requestParams.get(RequestParameter.OUTPUT).flatMap(OutputType::ofString);
+ final Headers responseHeaders = exchange.getResponseHeaders();
+ final AtomicBoolean needsRedirect = new AtomicBoolean(false);
+ final long id = idOption.onEmpty(() -> needsRedirect.set(true)).getOrElse(System::nanoTime);
+ final OutputType output = outputOption.onEmpty(() -> needsRedirect.set(true)).getOrElse(OutputType.HTML);
+ if (needsRedirect.get()) {
+ responseHeaders.add("Location", "/create?width=" + width + "&height=" + height + "&output=" + output.toString() + "&id=" + id);
+ exchange.sendResponseHeaders(302, -1);
+ exchange.close();
+ return;
+ }
+ responseHeaders.add("Content-type", output.getContentType());
+ exchange.sendResponseHeaders(200, 0);
+ final Labyrinth labyrinth = new Labyrinth(width, height, id);
+ final byte[] render = output.render(labyrinth);
+ final OutputStream responseBody = exchange.getResponseBody();
+ responseBody.write(render);
+ responseBody.flush();
+ exchange.close();
}
private enum RequestParameter {
diff --git a/src/main/java/ch/fritteli/labyrinth/server/Main.java b/src/main/java/ch/fritteli/labyrinth/server/Main.java
index dd5e963..ced13bd 100644
--- a/src/main/java/ch/fritteli/labyrinth/server/Main.java
+++ b/src/main/java/ch/fritteli/labyrinth/server/Main.java
@@ -1,8 +1,11 @@
package ch.fritteli.labyrinth.server;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
public class Main {
public static void main(String[] args) {
- LabyrinthServer.createListener()
- .onEmpty(() -> System.err.println("Failed to create server. Stopping."));
+ LabyrinthServer.createAndStartServer()
+ .onEmpty(() -> log.error("Failed to create server. Stopping."));
}
}
diff --git a/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java b/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java
new file mode 100644
index 0000000..58cee13
--- /dev/null
+++ b/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java
@@ -0,0 +1,61 @@
+package ch.fritteli.labyrinth.server;
+
+import io.vavr.control.Try;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.NonNull;
+import lombok.Value;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.Nullable;
+
+import java.net.InetAddress;
+
+
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Slf4j
+@Value
+public class ServerConfig {
+ public static final String SYSPROP_HOST = "fritteli.labyrinth.server.host";
+ public static final String SYSPROP_PORT = "fritteli.labyrinth.server.port";
+
+ @NonNull
+ InetAddress address;
+ int port;
+
+ public ServerConfig(@Nullable final String address, final int port) throws ConfigurationException {
+ this.address = validateAddress(address);
+ this.port = validatePort(port);
+ log.debug("host={}, port={}", this.address, this.port);
+ }
+
+ @NonNull
+ public static ServerConfig init() throws ConfigurationException {
+ final String host = System.getProperty(SYSPROP_HOST);
+ final String portString = System.getProperty(SYSPROP_PORT);
+ final int port = validatePort(portString);
+ return new ServerConfig(host, port);
+ }
+
+ @NonNull
+ private static InetAddress validateAddress(@Nullable final String address) {
+ return Try.of(() -> InetAddress.getByName(address))
+ .getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause));
+ }
+
+ private static int validatePort(@Nullable final String portString) {
+ if (portString == null) {
+ log.info("No port configured; using default.");
+ return 0;
+ }
+ return Try.of(() -> Integer.valueOf(portString))
+ .map(ServerConfig::validatePort)
+ .getOrElseThrow(cause -> new ConfigurationException("Failed to parse port specified in system property '" + SYSPROP_PORT + "': " + portString, cause));
+ }
+
+ private static int validatePort(final int port) {
+ if (port < 0 || port > 0xFFFF) {
+ throw new ConfigurationException("Port out of range (0..65535): " + port);
+ }
+ return port;
+ }
+}
diff --git a/src/test/java/ch/fritteli/labyrinth/server/ServerConfigTest.java b/src/test/java/ch/fritteli/labyrinth/server/ServerConfigTest.java
new file mode 100644
index 0000000..277abd3
--- /dev/null
+++ b/src/test/java/ch/fritteli/labyrinth/server/ServerConfigTest.java
@@ -0,0 +1,61 @@
+package ch.fritteli.labyrinth.server;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.net.UnknownHostException;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ServerConfigTest {
+ @BeforeEach
+ void clearSysProperties() {
+ System.setProperties(new Properties());
+ }
+
+ @Test
+ void testInit_noProperties() throws ConfigurationException {
+
+ // act
+ final ServerConfig sut = ServerConfig.init();
+
+ // assert
+ assertEquals("127.0.0.1", sut.getAddress().getHostAddress());
+ assertEquals("localhost", sut.getAddress().getHostName());
+ assertEquals(0, sut.getPort());
+ }
+
+ @Test
+ void testInit_unparseablePort() {
+ // arrange
+ System.setProperty(ServerConfig.SYSPROP_PORT, "Hello World!");
+
+ // act / assert
+ assertThrows(ConfigurationException.class, ServerConfig::init);
+ }
+
+ @Test
+ void testInit_invalidPort() {
+ // arrange
+ System.setProperty(ServerConfig.SYSPROP_PORT, "99999");
+
+ // act / assert
+ assertThrows(ConfigurationException.class, ServerConfig::init);
+ }
+
+ @Test
+ void testInit() throws ConfigurationException, UnknownHostException {
+ // arrange
+ System.setProperty(ServerConfig.SYSPROP_HOST, "127.0.0.1");
+ System.setProperty(ServerConfig.SYSPROP_PORT, "12345");
+
+ // act
+ final ServerConfig sut = ServerConfig.init();
+
+ // assert
+ assertEquals("127.0.0.1", sut.getAddress().getHostAddress());
+ assertEquals(12345, sut.getPort());
+ }
+}