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>
|
<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>
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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