From dd571a190ad10c9e59e554c99f32401688794ee2 Mon Sep 17 00:00:00 2001
From: Manuel Friedli <manuel@fritteli.ch>
Date: Sun, 2 Apr 2023 20:16:34 +0200
Subject: [PATCH] Refactoring

---
 .../labyrinth/server/LabyrinthServer.java     | 297 +++++++++---------
 .../ch/fritteli/labyrinth/server/Main.java    |  19 +-
 .../server/handler/AbstractHttpHandler.java   |  28 ++
 .../CreateHandler.java}                       |  14 +-
 .../LanyrinthRenderHandler.java               |  17 +
 .../UndertowPlayground.java                   |  18 --
 6 files changed, 202 insertions(+), 191 deletions(-)
 create mode 100644 src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java
 rename src/main/java/ch/fritteli/labyrinth/server/{undertow_playground/LabyrinthHttpHandler.java => handler/CreateHandler.java} (83%)
 create mode 100644 src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LanyrinthRenderHandler.java

diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java
index 32bb27c..f6cdbd0 100644
--- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java
+++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java
@@ -1,10 +1,11 @@
 package ch.fritteli.labyrinth.server;
 
-import ch.fritteli.labyrinth.generator.model.Labyrinth;
-import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer;
-import com.sun.net.httpserver.Headers;
-import com.sun.net.httpserver.HttpExchange;
-import com.sun.net.httpserver.HttpServer;
+import ch.fritteli.labyrinth.server.handler.CreateHandler;
+import ch.fritteli.labyrinth.server.undertow_playground.LanyrinthRenderHandler;
+import io.undertow.Undertow;
+import io.undertow.server.RoutingHandler;
+import io.undertow.server.handlers.RedirectHandler;
+import io.undertow.util.StatusCodes;
 import io.vavr.collection.HashMap;
 import io.vavr.collection.HashSet;
 import io.vavr.collection.Map;
@@ -16,28 +17,13 @@ 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.ExecutorService;
-import java.util.concurrent.SynchronousQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Function;
 
 @Slf4j
 public class LabyrinthServer {
     @NonNull
-    private final HttpServer httpServer;
-    @NonNull
-    private final ExecutorService executorService = new ThreadPoolExecutor(0,
-                                                                           1_000,
-                                                                           5,
-                                                                           TimeUnit.SECONDS,
-                                                                           new SynchronousQueue<>()
-    );
+    private final Undertow undertow;
 
     public static Option<LabyrinthServer> createAndStartServer() {
         final Option<LabyrinthServer> serverOption = Try.of(ServerConfig::init)
@@ -51,147 +37,106 @@ public class LabyrinthServer {
         return serverOption;
     }
 
-    public LabyrinthServer(@NonNull final ServerConfig config) throws IOException {
-        this.httpServer = HttpServer.create(new InetSocketAddress(config.getAddress(), config.getPort()), 5);
-        this.httpServer.createContext("/", new StaticResourcesFileHandler(this.executorService));
-        this.httpServer.createContext("/create", this::handleCreate);
-        this.httpServer.createContext("/render", this::handleRender);
+    public LabyrinthServer(@NonNull final ServerConfig config) {
+        log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort());
+        final RoutingHandler routingHandler = new RoutingHandler().get("/", new RedirectHandler("/create/text"))
+                                                                  .get("/create", new RedirectHandler("/create/text"))
+                                                                  .get("/create/{output}", new CreateHandler())
+                                                                  .post("/render", new LanyrinthRenderHandler())
+                                                                  .setFallbackHandler(exchange -> {
+                                                                      exchange.setStatusCode(StatusCodes.NOT_FOUND)
+                                                                              .getResponseSender()
+                                                                              .send("Resource %s not found".formatted(
+                                                                                      exchange.getRequestURI()));
+                                                                  });
+        this.undertow = Undertow.builder()
+                                .addHttpListener(config.getPort(), config.getAddress().getHostAddress())
+                                .setHandler(routingHandler)
+                                .build();
     }
 
     public void start() {
         Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper"));
-        this.httpServer.start();
-        log.info("Listening on http://{}:{}",
-                 this.httpServer.getAddress().getHostString(),
-                 this.httpServer.getAddress().getPort()
-        );
-    }
-
-    private void handleCreate(HttpExchange exchange) {
-        this.executorService.submit(() -> {
-            log.debug("Handling request to {}", exchange.getRequestURI());
-            try {
-                final String requestMethod = exchange.getRequestMethod();
-                if (!requestMethod.equals("GET")) {
-                    exchange.getResponseBody().close();
-                    exchange.sendResponseHeaders(405, -1);
-                    return;
-                }
-                final Map<RequestParameter, String> requestParams = this.parseQueryString(exchange.getRequestURI()
-                                                                                                  .getQuery());
-                final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 5);
-                final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 7);
-                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);
-                    return;
-                }
-                final byte[] render;
-                try {
-                    final Labyrinth labyrinth = new Labyrinth(width, height, id);
-                    render = output.render(labyrinth);
-                } catch (Exception e) {
-                    responseHeaders.add("Content-type", "text/plain; charset=UTF-8");
-                    exchange.sendResponseHeaders(500, 0);
-                    final OutputStream responseBody = exchange.getResponseBody();
-                    responseBody.write(("Error: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
-                    responseBody.flush();
-                    return;
-                }
-                responseHeaders.add("Content-type", output.getContentType());
-                if (output.isAttachment()) {
-                    responseHeaders.add("Content-disposition", String.format(
-                            "attachment; filename=\"labyrinth-%dx%d-%d.%s\"",
-                            width,
-                            height,
-                            id,
-                            output.getFileExtension()
-                    ));
-                }
-                exchange.sendResponseHeaders(200, 0);
-                final OutputStream responseBody = exchange.getResponseBody();
-                responseBody.write(render);
-                responseBody.flush();
-            } catch (Exception e) {
-                log.error("FSCK!", e);
-            } finally {
-                exchange.close();
-            }
-        });
-    }
-
-    private void handleRender(final HttpExchange exchange) {
-        this.executorService.submit(() -> {
-            try {
-                log.debug("Handling request to {}", exchange.getRequestURI());
-                final String requestMethod = exchange.getRequestMethod();
-                if (!requestMethod.equals("POST")) {
-                    exchange.getResponseBody().close();
-                    exchange.sendResponseHeaders(405, -1);
-                    return;
-                }
-                final byte[] bytes = exchange.getRequestBody().readAllBytes();
-
-                final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes);
-
-                final OutputType output = exchange.getRequestHeaders()
-                                                  .get("Accept")
-                                                  .contains(OutputType.HTML.getContentType()) ?
-                                          OutputType.HTML :
-                                          OutputType.TEXT_PLAIN;
-                final byte[] render;
-                final Headers responseHeaders = exchange.getResponseHeaders();
-                try {
-                    render = output.render(labyrinth);
-                } catch (Exception e) {
-                    responseHeaders.add("Content-type", "text/plain; charset=UTF-8");
-                    exchange.sendResponseHeaders(500, 0);
-                    final OutputStream responseBody = exchange.getResponseBody();
-                    responseBody.write(("Error: " + e).getBytes(StandardCharsets.UTF_8));
-                    responseBody.flush();
-                    return;
-                }
-                responseHeaders.add("Content-type", output.getContentType());
-                exchange.sendResponseHeaders(200, 0);
-                final OutputStream responseBody = exchange.getResponseBody();
-                responseBody.write(render);
-                responseBody.flush();
-            } catch (Exception e) {
-                log.error("FSCK!", e);
-            } finally {
-                exchange.close();
-            }
-        });
+        this.undertow.start();
+        Try.of(() -> (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress())
+           .onFailure(e -> log.warn("Started server, unable to determine listeing address/port."))
+           .forEach(address -> log.info("Listening on http://{}:{}", address.getHostString(), address.getPort()));
     }
 
     public void stop() {
         log.info("Stopping server ...");
-        this.httpServer.stop(5);
-        this.executorService.shutdown();
-        try {
-            if (!this.executorService.awaitTermination(5, TimeUnit.SECONDS)) {
-                log.warn("Timeout occurred while awaiting termination of executor service");
-            }
-        } catch (final InterruptedException e) {
-            log.error("Failed to await termination of executor service", e);
-        }
+        this.undertow.stop();
         log.info("Server stopped.");
     }
 
+//    private void handleCreate(HttpExchange exchange) {
+//        this.executorService.submit(() -> {
+//            log.debug("Handling request to {}", exchange.getRequestURI());
+//            try {
+//                final String requestMethod = exchange.getRequestMethod();
+//                if (!requestMethod.equals("GET")) {
+//                    exchange.getResponseBody().close();
+//                    exchange.sendResponseHeaders(405, -1);
+//                    return;
+//                }
+//                final Map<RequestParameter, String> requestParams = this.parseQueryString(exchange.getRequestURI()
+//                                                                                                  .getQuery());
+//                final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 5);
+//                final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 7);
+//                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);
+//                    return;
+//                }
+//                final byte[] render;
+//                try {
+//                    final Labyrinth labyrinth = new Labyrinth(width, height, id);
+//                    render = output.render(labyrinth);
+//                } catch (Exception e) {
+//                    responseHeaders.add("Content-type", "text/plain; charset=UTF-8");
+//                    exchange.sendResponseHeaders(500, 0);
+//                    final OutputStream responseBody = exchange.getResponseBody();
+//                    responseBody.write(("Error: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
+//                    responseBody.flush();
+//                    return;
+//                }
+//                responseHeaders.add("Content-type", output.getContentType());
+//                if (output.isAttachment()) {
+//                    responseHeaders.add("Content-disposition", String.format(
+//                            "attachment; filename=\"labyrinth-%dx%d-%d.%s\"",
+//                            width,
+//                            height,
+//                            id,
+//                            output.getFileExtension()
+//                    ));
+//                }
+//                exchange.sendResponseHeaders(200, 0);
+//                final OutputStream responseBody = exchange.getResponseBody();
+//                responseBody.write(render);
+//                responseBody.flush();
+//            } catch (Exception e) {
+//                log.error("FSCK!", e);
+//            } finally {
+//                exchange.close();
+//            }
+//        });
+//    }
+
     @NonNull
     private Map<RequestParameter, String> parseQueryString(@Nullable final String query) {
         if (query == null) {
@@ -218,15 +163,59 @@ public class LabyrinthServer {
         return result;
     }
 
+    private RequestParameter normalizeParameterName(final String paramName) {
+        return RequestParameter.parseName(paramName).get();
+    }
+
     private <T> T getOrDefault(@NonNull final Option<String> input,
                                @NonNull final Function<String, T> mapper,
                                @Nullable final T defaultValue) {
         return input.toTry().map(mapper).getOrElse(defaultValue);
     }
 
-    private RequestParameter normalizeParameterName(final String paramName) {
-        return RequestParameter.parseName(paramName).get();
-    }
+//    private void handleRender(final HttpExchange exchange) {
+//        this.executorService.submit(() -> {
+//            try {
+//                log.debug("Handling request to {}", exchange.getRequestURI());
+//                final String requestMethod = exchange.getRequestMethod();
+//                if (!requestMethod.equals("POST")) {
+//                    exchange.getResponseBody().close();
+//                    exchange.sendResponseHeaders(405, -1);
+//                    return;
+//                }
+//                final byte[] bytes = exchange.getRequestBody().readAllBytes();
+//
+//                final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes);
+//
+//                final OutputType output = exchange.getRequestHeaders()
+//                                                  .get("Accept")
+//                                                  .contains(OutputType.HTML.getContentType()) ?
+//                                          OutputType.HTML :
+//                                          OutputType.TEXT_PLAIN;
+//                final byte[] render;
+//                final Headers responseHeaders = exchange.getResponseHeaders();
+//                try {
+//                    render = output.render(labyrinth);
+//                } catch (Exception e) {
+//                    responseHeaders.add("Content-type", "text/plain; charset=UTF-8");
+//                    exchange.sendResponseHeaders(500, 0);
+//                    final OutputStream responseBody = exchange.getResponseBody();
+//                    responseBody.write(("Error: " + e).getBytes(StandardCharsets.UTF_8));
+//                    responseBody.flush();
+//                    return;
+//                }
+//                responseHeaders.add("Content-type", output.getContentType());
+//                exchange.sendResponseHeaders(200, 0);
+//                final OutputStream responseBody = exchange.getResponseBody();
+//                responseBody.write(render);
+//                responseBody.flush();
+//            } catch (Exception e) {
+//                log.error("FSCK!", e);
+//            } finally {
+//                exchange.close();
+//            }
+//        });
+//    }
 
     private enum RequestParameter {
         WIDTH("w", "width"),
diff --git a/src/main/java/ch/fritteli/labyrinth/server/Main.java b/src/main/java/ch/fritteli/labyrinth/server/Main.java
index 21e0d6c..e11f627 100644
--- a/src/main/java/ch/fritteli/labyrinth/server/Main.java
+++ b/src/main/java/ch/fritteli/labyrinth/server/Main.java
@@ -1,21 +1,18 @@
 package ch.fritteli.labyrinth.server;
 
-import ch.fritteli.labyrinth.server.undertow_playground.UndertowPlayground;
-import io.undertow.Undertow;
 import lombok.extern.slf4j.Slf4j;
 
 @Slf4j
 public class Main {
     public static void main(String[] args) {
-//        LabyrinthServer.createAndStartServer()
-//                .onEmpty(() -> log.error("Failed to create server. Stopping."));
+        LabyrinthServer.createAndStartServer().onEmpty(() -> log.error("Failed to create server. Stopping."));
 
-        final ServerConfig config = ServerConfig.init();
-        log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort());
-        Undertow.builder()
-                .addHttpListener(config.getPort(), config.getAddress().getHostAddress())
-                .setHandler(UndertowPlayground.r)
-                .build()
-                .start();
+//        final ServerConfig config = ServerConfig.init();
+//        log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort());
+//        Undertow.builder()
+//                .addHttpListener(config.getPort(), config.getAddress().getHostAddress())
+//                .setHandler(UndertowPlayground.r)
+//                .build()
+//                .start();
     }
 }
diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java
new file mode 100644
index 0000000..5d44423
--- /dev/null
+++ b/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java
@@ -0,0 +1,28 @@
+package ch.fritteli.labyrinth.server.handler;
+
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.StatusCodes;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public abstract class AbstractHttpHandler implements HttpHandler {
+    @Override
+    public final void handleRequest(final HttpServerExchange exchange) {
+        if (exchange.isInIoThread()) {
+            exchange.dispatch(this);
+            return;
+        }
+        try {
+            this.handle(exchange);
+        } catch (@NonNull final Exception e) {
+            log.error("Error handling request", e);
+            exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR)
+                    .getResponseSender()
+                    .send(StatusCodes.INTERNAL_SERVER_ERROR_STRING);
+        }
+    }
+
+    protected abstract void handle(@NonNull final HttpServerExchange exchange) throws Exception;
+}
diff --git a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LabyrinthHttpHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java
similarity index 83%
rename from src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LabyrinthHttpHandler.java
rename to src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java
index 4cc6658..c1a023f 100644
--- a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LabyrinthHttpHandler.java
+++ b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java
@@ -1,12 +1,14 @@
-package ch.fritteli.labyrinth.server.undertow_playground;
+package ch.fritteli.labyrinth.server.handler;
 
 import ch.fritteli.labyrinth.generator.model.Labyrinth;
 import ch.fritteli.labyrinth.server.OutputType;
-import io.undertow.server.HttpHandler;
+import ch.fritteli.labyrinth.server.handler.AbstractHttpHandler;
+import ch.fritteli.labyrinth.server.undertow_playground.UndertowPlayground;
 import io.undertow.server.HttpServerExchange;
 import io.undertow.util.Headers;
 import io.undertow.util.HttpString;
 import io.vavr.control.Option;
+import lombok.NonNull;
 import lombok.extern.slf4j.Slf4j;
 
 import java.nio.ByteBuffer;
@@ -15,13 +17,9 @@ import java.util.Map;
 import java.util.Random;
 
 @Slf4j
-class LabyrinthHttpHandler implements HttpHandler {
+public class CreateHandler extends AbstractHttpHandler {
     @Override
-    public void handleRequest(HttpServerExchange exchange) throws Exception {
-        if (exchange.isInIoThread()) {
-            exchange.dispatch(this);
-            return;
-        }
+    protected void handle(@NonNull final HttpServerExchange exchange) throws Exception {
         final Map<String, Deque<String>> queryParameters = exchange.getQueryParameters();
         final Option<String> output = UndertowPlayground.getFirstOption(queryParameters, "output");
         final Option<Integer> width = UndertowPlayground.getIntOption(queryParameters, "width");
diff --git a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LanyrinthRenderHandler.java b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LanyrinthRenderHandler.java
new file mode 100644
index 0000000..b176f09
--- /dev/null
+++ b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/LanyrinthRenderHandler.java
@@ -0,0 +1,17 @@
+package ch.fritteli.labyrinth.server.undertow_playground;
+
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.StatusCodes;
+
+public class LanyrinthRenderHandler implements HttpHandler {
+    @Override
+    public void handleRequest(final HttpServerExchange exchange) {
+        if (exchange.isInIoThread()) {
+            exchange.dispatch(this);
+            return;
+        }
+        exchange.setStatusCode(StatusCodes.NOT_IMPLEMENTED);
+        exchange.getResponseSender().send("Rendering binary data is not implemented yet.");
+    }
+}
diff --git a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java
index 1ef547c..1caf698 100644
--- a/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java
+++ b/src/main/java/ch/fritteli/labyrinth/server/undertow_playground/UndertowPlayground.java
@@ -14,24 +14,6 @@ import java.util.Map;
 
 @Slf4j
 public class UndertowPlayground {
-    public static final RoutingHandler r = new RoutingHandler().get("/create/{output}", new LabyrinthHttpHandler()).post("/render", new HttpHandler() {
-        @Override
-        public void handleRequest(final HttpServerExchange exchange) {
-            if (exchange.isInIoThread()) {
-                exchange.dispatch(this);
-                return;
-            }
-            exchange.getResponseSender().send("TODO: read body, render stuff");
-        }
-    }).setFallbackHandler(new HttpHandler() {
-        @Override
-        public void handleRequest(HttpServerExchange exchange) throws Exception {
-            exchange.getResponseSender().send("Request: " + exchange.getRequestURI());
-            final HeaderValues strings = exchange.getRequestHeaders().get(Headers.ACCEPT);
-            strings.peekFirst();
-        }
-    });
-
     @NonNull
     public static Option<Integer> getIntOption(@NonNull final Map<String, Deque<String>> queryParams,
                                                 @NonNull final String paramName) {