One feature, one bugfix:
All checks were successful
continuous-integration/drone/push Build is passing
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:
parent
a45daf6ba3
commit
ed9df43aa8
7 changed files with 98 additions and 77 deletions
2
pom.xml
2
pom.xml
|
@ -20,7 +20,7 @@
|
|||
<java.target.version>17</java.target.version>
|
||||
<jetbrains-annotations.version>24.0.1</jetbrains-annotations.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>
|
||||
<lombok.version>1.18.26</lombok.version>
|
||||
<slf4j.version>2.0.7</slf4j.version>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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<RequestParameter, ?> 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)
|
||||
);
|
||||
}
|
||||
private final Map<String, Deque<String>> queryParameters;
|
||||
|
||||
@NonNull
|
||||
Try<Tuple2<OutputType, Labyrinth>> createLabyrinth() {
|
||||
|
@ -69,43 +50,6 @@ class ParametersToLabyrinthExtractor {
|
|||
|
||||
@NonNull
|
||||
private <T> Option<T> getParameterValue(@NonNull final RequestParameter parameter) {
|
||||
return (Option<T>) 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<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);
|
||||
}
|
||||
return parameter.getParameterValue(this.queryParameters);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.<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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue