One feature, one bugfix:
All checks were successful
continuous-integration/drone/push Build is passing

- Feat: Enable rendering binary data to any supported output format.
- Fix: Correctly serve as attachment when outputtyp is pdffile or
  binary.
This commit is contained in:
Manuel Friedli 2023-04-08 22:28:32 +02:00
parent a45daf6ba3
commit ed9df43aa8
7 changed files with 98 additions and 77 deletions

View file

@ -20,7 +20,7 @@
<java.target.version>17</java.target.version> <java.target.version>17</java.target.version>
<jetbrains-annotations.version>24.0.1</jetbrains-annotations.version> <jetbrains-annotations.version>24.0.1</jetbrains-annotations.version>
<junit-jupiter.version>5.9.2</junit-jupiter.version> <junit-jupiter.version>5.9.2</junit-jupiter.version>
<labyrinth-generator.version>0.0.3</labyrinth-generator.version> <labyrinth-generator.version>0.0.4</labyrinth-generator.version>
<logback.version>1.4.6</logback.version> <logback.version>1.4.6</logback.version>
<lombok.version>1.18.26</lombok.version> <lombok.version>1.18.26</lombok.version>
<slf4j.version>2.0.7</slf4j.version> <slf4j.version>2.0.7</slf4j.version>

View file

@ -5,24 +5,23 @@ import ch.fritteli.labyrinth.server.handler.RenderHandler;
import io.undertow.Undertow; import io.undertow.Undertow;
import io.undertow.server.RoutingHandler; import io.undertow.server.RoutingHandler;
import io.vavr.control.Try; import io.vavr.control.Try;
import java.net.InetSocketAddress;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class LabyrinthServer { public class LabyrinthServer {
@NonNull
private final ServerConfig config;
@NonNull @NonNull
private final Undertow undertow; private final Undertow undertow;
private LabyrinthServer(@NonNull final ServerConfig config) { private LabyrinthServer(@NonNull final ServerConfig config) {
this.config = config;
final String hostAddress = config.getAddress().getHostAddress(); final String hostAddress = config.getAddress().getHostAddress();
final int port = config.getPort(); final int port = config.getPort();
log.info("Starting Server at http://{}:{}/", hostAddress, port); log.info("Starting Server at http://{}:{}/", hostAddress, port);
final RoutingHandler routingHandler = new RoutingHandler() final RoutingHandler routingHandler = new RoutingHandler()
.get(CreateHandler.PATH_TEMPLATE, new CreateHandler()) .get(CreateHandler.PATH_TEMPLATE, new CreateHandler())
.post("/render", new RenderHandler()); .post(RenderHandler.PATH_TEMPLATE, new RenderHandler());
this.undertow = Undertow.builder() this.undertow = Undertow.builder()
.addHttpListener(port, hostAddress) .addHttpListener(port, hostAddress)
@ -45,7 +44,11 @@ public class LabyrinthServer {
private void start() { private void start() {
Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper"));
this.undertow.start(); 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() { private void stop() {

View file

@ -30,13 +30,13 @@ public enum OutputType {
"html"), "html"),
PDF("application/pdf", PDF("application/pdf",
"pdf", "pdf",
labyrinth -> PDFRenderer.newInstance().render(labyrinth), labyrinth -> PDFRenderer.newInstance().render(labyrinth).toByteArray(),
false, false,
"p", "p",
"pdf"), "pdf"),
PDFFILE("application/pdf", PDFFILE("application/pdf",
"pdf", "pdf",
labyrinth -> PDFRenderer.newInstance().render(labyrinth), labyrinth -> PDFRenderer.newInstance().render(labyrinth).toByteArray(),
true, true,
"f", "f",
"pdffile"), "pdffile"),

View file

@ -55,6 +55,15 @@ public class CreateHandler extends AbstractHttpHandler {
.put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth())) .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-Height"), String.valueOf(labyrinth.getHeight()))
.put(HttpString.tryFromString("X-Labyrinth-Generation-Duration-millis"), String.valueOf(durationMillis)); .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)); exchange.getResponseSender().send(ByteBuffer.wrap(bytes));
log.debug("Create request handled in {}ms.", durationMillis); log.debug("Create request handled in {}ms.", durationMillis);
}); });

View file

@ -4,39 +4,20 @@ import ch.fritteli.labyrinth.generator.model.Labyrinth;
import ch.fritteli.labyrinth.server.OutputType; import ch.fritteli.labyrinth.server.OutputType;
import io.vavr.Tuple; import io.vavr.Tuple;
import io.vavr.Tuple2; 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.collection.Stream;
import io.vavr.control.Option; import io.vavr.control.Option;
import io.vavr.control.Try; import io.vavr.control.Try;
import java.util.Deque; import java.util.Deque;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.function.Function;
import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
@RequiredArgsConstructor
class ParametersToLabyrinthExtractor { class ParametersToLabyrinthExtractor {
@NonNull @NonNull
private final Multimap<RequestParameter, ?> queryParameters; private final Map<String, Deque<String>> queryParameters;
ParametersToLabyrinthExtractor(@NonNull final Map<String, Deque<String>> 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)
);
}
@NonNull @NonNull
Try<Tuple2<OutputType, Labyrinth>> createLabyrinth() { Try<Tuple2<OutputType, Labyrinth>> createLabyrinth() {
@ -69,43 +50,6 @@ class ParametersToLabyrinthExtractor {
@NonNull @NonNull
private <T> Option<T> getParameterValue(@NonNull final RequestParameter parameter) { private <T> Option<T> getParameterValue(@NonNull final RequestParameter parameter) {
return (Option<T>) this.queryParameters.getOrElse(parameter, List.empty()) return parameter.getParameterValue(this.queryParameters);
.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<String, Option<?>> extractor;
@Getter
@NonNull
private final Set<String> names;
RequestParameter(@NonNull final Function<String, Option<?>> extractor, @NonNull final String... names) {
this.extractor = extractor;
this.names = HashSet.of(names);
}
static Option<RequestParameter> 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);
}
} }
} }

View file

@ -3,17 +3,19 @@ package ch.fritteli.labyrinth.server.handler;
import ch.fritteli.labyrinth.generator.model.Labyrinth; import ch.fritteli.labyrinth.generator.model.Labyrinth;
import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer; import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer;
import ch.fritteli.labyrinth.server.OutputType; import ch.fritteli.labyrinth.server.OutputType;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import io.undertow.util.HeaderValues;
import io.undertow.util.Headers; import io.undertow.util.Headers;
import io.undertow.util.StatusCodes; import io.undertow.util.StatusCodes;
import io.vavr.control.Option;
import lombok.extern.slf4j.Slf4j;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class RenderHandler extends AbstractHttpHandler { public class RenderHandler extends AbstractHttpHandler {
public static final String PATH_TEMPLATE = "/render/{output}";
@Override @Override
public void handle(final HttpServerExchange exchange) { public void handle(final HttpServerExchange exchange) {
log.debug("Handling render request"); log.debug("Handling render request");
@ -23,13 +25,10 @@ public class RenderHandler extends AbstractHttpHandler {
return; return;
} }
exchange.getRequestReceiver().receiveFullBytes((httpServerExchange, bytes) -> { exchange.getRequestReceiver().receiveFullBytes((httpServerExchange, bytes) -> {
final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); final OutputType output = this.getOutputType(httpServerExchange);
final OutputType output = Option.of(httpServerExchange.getRequestHeaders().get(Headers.ACCEPT))
.exists(values -> values.contains(OutputType.HTML.getContentType())) ?
OutputType.HTML :
OutputType.TEXT_PLAIN;
final byte[] render; final byte[] render;
try { try {
final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes);
render = output.render(labyrinth); render = output.render(labyrinth);
} catch (final Exception e) { } catch (final Exception e) {
log.error("Error rendering binary labyrinth data", e); log.error("Error rendering binary labyrinth data", e);
@ -46,4 +45,16 @@ public class RenderHandler extends AbstractHttpHandler {
.send(ByteBuffer.wrap(render)); .send(ByteBuffer.wrap(render));
}); });
} }
@NonNull
private OutputType getOutputType(@NonNull final HttpServerExchange httpServerExchange) {
return RequestParameter.OUTPUT.<OutputType>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;
});
}
} }

View file

@ -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<String, Option<?>> extractor;
@Getter
@NonNull
private final Set<String> names;
RequestParameter(@NonNull final Function<String, Option<?>> 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 <T> Option<T> getParameterValue(@NonNull final Map<String, Deque<String>> queryParameters) {
return (Option<T>) HashMap.ofAll(queryParameters)
.filterKeys(this.names::contains)
.flatMap(Tuple2::_2)
.flatMap(this::extractParameterValue)
.headOption();
}
}