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 createAndStartServer() { final Option 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 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 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); - 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 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 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); +// 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 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 getOrDefault(@NonNull final Option input, @NonNull final Function 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> queryParameters = exchange.getQueryParameters(); final Option output = UndertowPlayground.getFirstOption(queryParameters, "output"); final Option 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 getIntOption(@NonNull final Map> queryParams, @NonNull final String paramName) {