diff --git a/.drone.yml b/.drone.yml index e4c41bc..940a6dc 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,7 +4,7 @@ name: default steps: - name: test - image: maven:3.6-jdk-11 + image: maven:3.8-openjdk-18-slim commands: - mvn clean install -DskipTests=true -Dmaven.javadoc.skip=true -B -V - mvn test -B @@ -14,13 +14,13 @@ steps: - master - feature/* - name: deploy - image: maven:3.6-jdk-11 + image: maven:3.8-openjdk-18-slim environment: REPO_TOKEN: from_secret: repo-token commands: - mvn -s maven-settings.xml deploy -DskipTests=true - trigger: + when: branch: - master event: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..abccd12 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:11 + +COPY target/labyrinth-server-*.jar /app/app.jar + +CMD java -Dfritteli.labyrinth.server.host=0.0.0.0 -Dfritteli.labyrinth.server.port=80 -jar /app/app.jar diff --git a/labyrinth-server.iml b/labyrinth-server.iml deleted file mode 100644 index ee5c0f7..0000000 --- a/labyrinth-server.iml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9be4803..3518f8b 100644 --- a/pom.xml +++ b/pom.xml @@ -15,8 +15,9 @@ 0.0.1-SNAPSHOT - 1.2.10 - 1.7.35 + 1.4.6 + 1.18.26 + 2.0.5 @@ -57,23 +58,21 @@ - maven-assembly-plugin + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 - - + + ch.fritteli.labyrinth.server.Main - - - - jar-with-dependencies - + + - make-assembly package - single + shade diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java index a3a3790..689d9bc 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java +++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java @@ -24,7 +24,6 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; -import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; import java.util.concurrent.SynchronousQueue; @@ -38,32 +37,166 @@ 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 ExecutorService executorService = new ThreadPoolExecutor(0, + 1_000, + 5, + TimeUnit.SECONDS, + new SynchronousQueue<>() ); - public LabyrinthServer(@NonNull final ServerConfig config) throws IOException, URISyntaxException { + public static Option createAndStartServer() { + final Option serverOption = Try.of(ServerConfig::init) + .mapTry(LabyrinthServer::new) + .onFailure(cause -> log.error( + "Failed to create LabyrinthServer.", + cause + )) + .toOption(); + serverOption.forEach(LabyrinthServer::start); + 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 static Option createAndStartServer() { - final Option serverOption = Try.of(ServerConfig::init) - .mapTry(LabyrinthServer::new) - .onFailure(cause -> log.error("Failed to create LabyrinthServer.", cause)) - .toOption(); - serverOption.forEach(LabyrinthServer::start); - return serverOption; + 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 T getOrDefault(@NonNull final Option input, @NonNull final Function mapper, @Nullable final T defaultValue) { - return input.toTry().map(mapper).getOrElse(defaultValue); + 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 Labyrinth labyrinth = new Labyrinth(width, height, id); + final byte[] render; + 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()); + 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(); + } + }); + } + + 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); + } + log.info("Server stopped."); } @NonNull @@ -92,126 +225,16 @@ public class LabyrinthServer { return result; } + 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(); } - 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()); - } - - 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); - } - log.info("Server stopped."); - } - - private void handleCreate(HttpExchange exchange) throws IOException { - 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, 5); - 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 Labyrinth labyrinth = new Labyrinth(width, height, id); - final byte[] render; - 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()); - if (output.equals(OutputType.BINARY)) { - responseHeaders.add("Content-disposition", "attachment; filename=\"labyrinth-" + width + "x" + height + "-" + id + ".laby\""); - } - 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) throws IOException { - 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("text/html") ? - 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"), HEIGHT("h", "height"), @@ -227,16 +250,34 @@ public class LabyrinthServer { if (name == null) { return Option.none(); } - return Stream.of(values()) - .find(param -> param.names.exists(name::equalsIgnoreCase)); + return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); } } private enum OutputType { - TEXT_PLAIN("text/plain; charset=UTF-8", labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), "t", "text"), - HTML("text/html", labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), "h", "html"), - PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "p", "pdf"), - BINARY("application/octet-stream", SerializerDeserializer::serialize, "b", "binary"); + TEXT_PLAIN("text/plain; charset=UTF-8", + labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), + "txt", + false, + "t", + "text" + ), + HTML("text/html", + labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), + "html", + false, + "h", + "html" + ), + PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", false, "p", "pdf"), + PDFFILE("application/pdf", + labyrinth -> PDFRenderer.newInstance().render(labyrinth), + "pdf", + true, + "f", + "pdffile" + ), + BINARY("application/octet-stream", SerializerDeserializer::serialize, "laby", true, "b", "binary"); @Getter @NonNull private final String contentType; @@ -244,10 +285,21 @@ public class LabyrinthServer { private final List names; @NonNull private final Function render; + @Getter + private final boolean attachment; + @Getter + @NonNull + private final String fileExtension; - OutputType(@NonNull final String contentType, @NonNull final Function render, @NonNull final String... names) { + OutputType(@NonNull final String contentType, + @NonNull final Function render, + @NonNull final String fileExtension, + final boolean attachment, + @NonNull final String... names) { this.contentType = contentType; this.render = render; + this.fileExtension = fileExtension; + this.attachment = attachment; this.names = List.of(names); } @@ -256,9 +308,7 @@ public class LabyrinthServer { return Option.none(); } final String nameLC = name.toLowerCase(); - return Stream.of(values()) - .find(param -> param.names.contains(nameLC)); - + return Stream.of(values()).find(param -> param.names.contains(nameLC)); } @Override diff --git a/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java b/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java index d15f1fd..d1cabfc 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java @@ -21,6 +21,7 @@ public class StaticResourcesFileHandler implements HttpHandler { public StaticResourcesFileHandler(final ExecutorService executorService) { this.executorService = executorService; + log.debug("Created {}", this.getClass().getSimpleName()); } private static void redirect(@NonNull final HttpExchange exchange, @NonNull final String target) throws IOException { @@ -44,7 +45,9 @@ public class StaticResourcesFileHandler implements HttpHandler { log.debug("Resource '{}' not found in classpath.", path); return new byte[0]; } - return IOUtils.toByteArray(stream); + final byte[] response = IOUtils.toByteArray(stream); + log.debug("Sending reply; {} bytes", response.length); + return response; } @Nullable @@ -66,7 +69,7 @@ public class StaticResourcesFileHandler implements HttpHandler { final String path = requestURI.getPath(); log.debug("Handling request to {}", path); if ("/".equals(path)) { - redirect(exchange, "/index.html"); + redirect(exchange, "index.html"); return; } if (!path.startsWith("/")) { diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 08ae395..4bba3c1 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,6 +1,6 @@ - +