From 5ba689841f41ad6b9a806cc9d938322d7f19b0ea Mon Sep 17 00:00:00 2001
From: Manuel Friedli <manuel@fritteli.ch>
Date: Wed, 7 Oct 2020 01:27:38 +0200
Subject: [PATCH] Refactoring.

---
 labyrinth-server.iml                          |   8 +-
 pom.xml                                       |  19 +++
 .../server/ConfigurationException.java        |  13 +++
 .../labyrinth/server/LabyrinthServer.java     | 110 ++++++++----------
 .../ch/fritteli/labyrinth/server/Main.java    |   7 +-
 .../labyrinth/server/ServerConfig.java        |  61 ++++++++++
 .../labyrinth/server/ServerConfigTest.java    |  61 ++++++++++
 7 files changed, 215 insertions(+), 64 deletions(-)
 create mode 100644 src/main/java/ch/fritteli/labyrinth/server/ConfigurationException.java
 create mode 100644 src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java
 create mode 100644 src/test/java/ch/fritteli/labyrinth/server/ServerConfigTest.java

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 @@
     <orderEntry type="inheritedJdk" />
     <orderEntry type="sourceFolder" forTests="false" />
     <orderEntry type="library" name="Maven: ch.fritteli.labyrinth:labyrinth-generator:0.0.1" level="project" />
-    <orderEntry type="library" name="Maven: org.jetbrains:annotations:19.0.0" level="project" />
     <orderEntry type="library" name="Maven: org.apache.pdfbox:pdfbox:2.0.20" level="project" />
     <orderEntry type="library" name="Maven: org.apache.pdfbox:fontbox:2.0.20" level="project" />
     <orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" />
     <orderEntry type="library" name="Maven: io.vavr:vavr:0.10.2" level="project" />
     <orderEntry type="library" name="Maven: io.vavr:vavr-match:0.10.2" level="project" />
     <orderEntry type="library" scope="PROVIDED" name="Maven: org.projectlombok:lombok:1.18.12" level="project" />
+    <orderEntry type="library" name="Maven: org.jetbrains:annotations:19.0.0" level="project" />
+    <orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.30" level="project" />
+    <orderEntry type="library" name="Maven: org.slf4j:slf4j-simple:1.7.30" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.6.1" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.0" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.opentest4j:opentest4j:1.2.0" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.platform:junit-platform-commons:1.6.1" level="project" />
   </component>
 </module>
\ 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 @@
 			<groupId>org.projectlombok</groupId>
 			<artifactId>lombok</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>org.jetbrains</groupId>
+			<artifactId>annotations</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-api</artifactId>
+			<version>1.7.30</version>
+		</dependency>
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-simple</artifactId>
+			<version>1.7.30</version>
+		</dependency>
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter-api</artifactId>
+			<scope>test</scope>
+		</dependency>
 	</dependencies>
 
 	<build>
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<RequestParameter, String> 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<Long> seedOption = requestParams.get(RequestParameter.ID).toTry().map(Long::valueOf).toOption();
-            final long seed;
-            final Option<OutputType> 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<LabyrinthServer> createListener() {
-        final String listenerPort = System.getProperty("fritteli.labyrinth.listenerport");
-        final Option<LabyrinthServer> 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<LabyrinthServer> createAndStartServer() {
+        final Option<LabyrinthServer> 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> T getOrDefault(@NonNull final Option<String> input, @NonNull final Function<String, T> 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<RequestParameter, String> 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<Long> idOption = requestParams.get(RequestParameter.ID).toTry().map(Long::valueOf).toOption();
+        final Option<OutputType> 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());
+    }
+}