From ed9df43aa81c2c671fb2fd8c01cbe339458ebbb5 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sat, 8 Apr 2023 22:28:32 +0200 Subject: [PATCH] One feature, one bugfix: - Feat: Enable rendering binary data to any supported output format. - Fix: Correctly serve as attachment when outputtyp is pdffile or binary. --- pom.xml | 2 +- .../labyrinth/server/LabyrinthServer.java | 13 ++-- .../fritteli/labyrinth/server/OutputType.java | 4 +- .../server/handler/CreateHandler.java | 9 +++ .../ParametersToLabyrinthExtractor.java | 64 ++----------------- .../server/handler/RenderHandler.java | 29 ++++++--- .../server/handler/RequestParameter.java | 54 ++++++++++++++++ 7 files changed, 98 insertions(+), 77 deletions(-) create mode 100644 src/main/java/ch/fritteli/labyrinth/server/handler/RequestParameter.java diff --git a/pom.xml b/pom.xml index 668a742..c47be7e 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 17 24.0.1 5.9.2 - 0.0.3 + 0.0.4 1.4.6 1.18.26 2.0.7 diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java index f0f0321..16e0aae 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java +++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java @@ -5,24 +5,23 @@ import ch.fritteli.labyrinth.server.handler.RenderHandler; import io.undertow.Undertow; import io.undertow.server.RoutingHandler; import io.vavr.control.Try; +import java.net.InetSocketAddress; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @Slf4j public class LabyrinthServer { - @NonNull - private final ServerConfig config; + @NonNull private final Undertow undertow; private LabyrinthServer(@NonNull final ServerConfig config) { - this.config = config; final String hostAddress = config.getAddress().getHostAddress(); final int port = config.getPort(); log.info("Starting Server at http://{}:{}/", hostAddress, port); final RoutingHandler routingHandler = new RoutingHandler() .get(CreateHandler.PATH_TEMPLATE, new CreateHandler()) - .post("/render", new RenderHandler()); + .post(RenderHandler.PATH_TEMPLATE, new RenderHandler()); this.undertow = Undertow.builder() .addHttpListener(port, hostAddress) @@ -45,7 +44,11 @@ public class LabyrinthServer { private void start() { Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); this.undertow.start(); - log.info("Listening on http://{}:{}", this.config.getAddress().getHostAddress(), this.config.getPort()); + final InetSocketAddress address = (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress(); + final String hostAddress = address.getAddress().getHostAddress(); + final int port = address.getPort(); + + log.info("Listening on http://{}:{}", hostAddress, port); } private void stop() { diff --git a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java index 4668bdd..c1d08d9 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java +++ b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java @@ -30,13 +30,13 @@ public enum OutputType { "html"), PDF("application/pdf", "pdf", - labyrinth -> PDFRenderer.newInstance().render(labyrinth), + labyrinth -> PDFRenderer.newInstance().render(labyrinth).toByteArray(), false, "p", "pdf"), PDFFILE("application/pdf", "pdf", - labyrinth -> PDFRenderer.newInstance().render(labyrinth), + labyrinth -> PDFRenderer.newInstance().render(labyrinth).toByteArray(), true, "f", "pdffile"), diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java index c1578fd..344a907 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java @@ -55,6 +55,15 @@ public class CreateHandler extends AbstractHttpHandler { .put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth())) .put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(labyrinth.getHeight())) .put(HttpString.tryFromString("X-Labyrinth-Generation-Duration-millis"), String.valueOf(durationMillis)); + if (outputType.isAttachment()) { + exchange.getResponseHeaders() + .put(Headers.CONTENT_DISPOSITION, "attachment; filename=\"labyrinth-%dx%d-%d.%s\"".formatted( + labyrinth.getWidth(), + labyrinth.getHeight(), + labyrinth.getRandomSeed(), + outputType.getFileExtension() + )); + } exchange.getResponseSender().send(ByteBuffer.wrap(bytes)); log.debug("Create request handled in {}ms.", durationMillis); }); diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java b/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java index b766b77..a1cf854 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java @@ -4,39 +4,20 @@ import ch.fritteli.labyrinth.generator.model.Labyrinth; import ch.fritteli.labyrinth.server.OutputType; import io.vavr.Tuple; import io.vavr.Tuple2; -import io.vavr.collection.HashMap; -import io.vavr.collection.HashMultimap; -import io.vavr.collection.HashSet; -import io.vavr.collection.List; -import io.vavr.collection.Multimap; -import io.vavr.collection.Set; import io.vavr.collection.Stream; import io.vavr.control.Option; import io.vavr.control.Try; import java.util.Deque; import java.util.Map; import java.util.Random; -import java.util.function.Function; -import lombok.Getter; import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.Nullable; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor class ParametersToLabyrinthExtractor { @NonNull - private final Multimap queryParameters; - - ParametersToLabyrinthExtractor(@NonNull final Map> queryParameters) { - this.queryParameters = HashMap.ofAll(queryParameters) - .foldLeft( - HashMultimap.withSet().empty(), - (map, tuple) -> RequestParameter.parseName(tuple._1()).map(parameter -> Stream.ofAll(tuple._2()) - .flatMap(parameter::extractParameterValue) - .foldLeft(map, (m, value) -> m.put(parameter, value))) - .getOrElse(map) - ); - } + private final Map> queryParameters; @NonNull Try> createLabyrinth() { @@ -69,43 +50,6 @@ class ParametersToLabyrinthExtractor { @NonNull private Option getParameterValue(@NonNull final RequestParameter parameter) { - return (Option) this.queryParameters.getOrElse(parameter, List.empty()) - .headOption(); - } - - @Slf4j - private enum RequestParameter { - WIDTH(p -> Try.of(() -> Integer.parseInt(p)) - .toOption() - .onEmpty(() -> log.debug("Unparseable value for parameter 'width': '{}'", p)), "w", "width"), - HEIGHT(p -> Try.of(() -> Integer.parseInt(p)) - .toOption() - .onEmpty(() -> log.debug("Unparseable value for parameter 'height': '{}'", p)), "h", "height"), - ID(p -> Try.of(() -> Long.parseLong(p)) - .toOption() - .onEmpty(() -> log.debug("Unparseable value for parameter 'id': '{}'", p)), "i", "id"), - OUTPUT(p -> OutputType.ofString(p) - .onEmpty(() -> log.debug("Unparseable value for parameter 'output': '{}'", p)), "o", "output"); - @NonNull - private final Function> extractor; - @Getter - @NonNull - private final Set names; - - RequestParameter(@NonNull final Function> extractor, @NonNull final String... names) { - this.extractor = extractor; - this.names = HashSet.of(names); - } - - static Option parseName(@Nullable final String name) { - if (name == null) { - return Option.none(); - } - return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); - } - - @NonNull Option extractParameterValue(@NonNull final String parameter) { - return this.extractor.apply(parameter); - } + return parameter.getParameterValue(this.queryParameters); } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java index 5079a09..52a61d5 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java @@ -3,17 +3,19 @@ package ch.fritteli.labyrinth.server.handler; import ch.fritteli.labyrinth.generator.model.Labyrinth; import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer; import ch.fritteli.labyrinth.server.OutputType; -import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderValues; import io.undertow.util.Headers; import io.undertow.util.StatusCodes; -import io.vavr.control.Option; -import lombok.extern.slf4j.Slf4j; - import java.nio.ByteBuffer; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; @Slf4j public class RenderHandler extends AbstractHttpHandler { + + public static final String PATH_TEMPLATE = "/render/{output}"; + @Override public void handle(final HttpServerExchange exchange) { log.debug("Handling render request"); @@ -23,13 +25,10 @@ public class RenderHandler extends AbstractHttpHandler { return; } exchange.getRequestReceiver().receiveFullBytes((httpServerExchange, bytes) -> { - final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); - final OutputType output = Option.of(httpServerExchange.getRequestHeaders().get(Headers.ACCEPT)) - .exists(values -> values.contains(OutputType.HTML.getContentType())) ? - OutputType.HTML : - OutputType.TEXT_PLAIN; + final OutputType output = this.getOutputType(httpServerExchange); final byte[] render; try { + final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); render = output.render(labyrinth); } catch (final Exception e) { log.error("Error rendering binary labyrinth data", e); @@ -46,4 +45,16 @@ public class RenderHandler extends AbstractHttpHandler { .send(ByteBuffer.wrap(render)); }); } + + @NonNull + private OutputType getOutputType(@NonNull final HttpServerExchange httpServerExchange) { + return RequestParameter.OUTPUT.getParameterValue(httpServerExchange.getQueryParameters()) + .getOrElse(() -> { + final HeaderValues accept = httpServerExchange.getRequestHeaders().get(Headers.ACCEPT); + if (accept.contains(OutputType.HTML.getContentType())) { + return OutputType.HTML; + } + return OutputType.TEXT_PLAIN; + }); + } } diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/RequestParameter.java b/src/main/java/ch/fritteli/labyrinth/server/handler/RequestParameter.java new file mode 100644 index 0000000..314ee22 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/RequestParameter.java @@ -0,0 +1,54 @@ +package ch.fritteli.labyrinth.server.handler; + +import ch.fritteli.labyrinth.server.OutputType; +import io.vavr.Tuple2; +import io.vavr.collection.HashMap; +import io.vavr.collection.HashSet; +import io.vavr.collection.Set; +import io.vavr.control.Option; +import io.vavr.control.Try; +import java.util.Deque; +import java.util.Map; +import java.util.function.Function; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +enum RequestParameter { + WIDTH(p -> Try.of(() -> Integer.parseInt(p)) + .toOption() + .onEmpty(() -> log.debug("Unparseable value for parameter 'width': '{}'", p)), "w", "width"), + HEIGHT(p -> Try.of(() -> Integer.parseInt(p)) + .toOption() + .onEmpty(() -> log.debug("Unparseable value for parameter 'height': '{}'", p)), "h", "height"), + ID(p -> Try.of(() -> Long.parseLong(p)) + .toOption() + .onEmpty(() -> log.debug("Unparseable value for parameter 'id': '{}'", p)), "i", "id"), + OUTPUT(p -> OutputType.ofString(p) + .onEmpty(() -> log.debug("Unparseable value for parameter 'output': '{}'", p)), "o", "output"); + @NonNull + private final Function> extractor; + @Getter + @NonNull + private final Set names; + + RequestParameter(@NonNull final Function> extractor, @NonNull final String... names) { + this.extractor = extractor; + this.names = HashSet.of(names); + } + + @NonNull + Option extractParameterValue(@NonNull final String parameter) { + return this.extractor.apply(parameter); + } + + @NonNull + public Option getParameterValue(@NonNull final Map> queryParameters) { + return (Option) HashMap.ofAll(queryParameters) + .filterKeys(this.names::contains) + .flatMap(Tuple2::_2) + .flatMap(this::extractParameterValue) + .headOption(); + } +}