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