feature/undertow #4

Merged
manuel merged 15 commits from feature/undertow into master 2023-04-08 22:40:46 +02:00
9 changed files with 142 additions and 206 deletions
Showing only changes of commit 4dc15acf0c - Show all commits

View file

@ -52,7 +52,7 @@
<dependency> <dependency>
<groupId>io.undertow</groupId> <groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId> <artifactId>undertow-core</artifactId>
<version>2.2.22.Final</version> <version>2.3.5.Final</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>

View file

@ -1,48 +1,51 @@
package ch.fritteli.labyrinth.server; package ch.fritteli.labyrinth.server;
import ch.fritteli.labyrinth.server.handler.CreateHandler; import ch.fritteli.labyrinth.server.handler.CreateHandler;
import ch.fritteli.labyrinth.server.handler.LanyrinthRenderHandler; 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.undertow.util.StatusCodes;
import io.vavr.control.Try; import io.vavr.control.Try;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
@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) {
log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort()); this.config = config;
final RoutingHandler routingHandler = new RoutingHandler().get(CreateHandler.PATH_TEMPLATE, new CreateHandler()) final String hostAddress = config.getAddress().getHostAddress();
.post("/render", new LanyrinthRenderHandler()) final int port = config.getPort();
.setFallbackHandler(exchange -> exchange.setStatusCode( log.info("Starting Server at http://{}:{}/", hostAddress, port);
StatusCodes.NOT_FOUND) final RoutingHandler routingHandler = new RoutingHandler()
.getResponseSender() .get(CreateHandler.PATH_TEMPLATE, new CreateHandler())
.send("Resource %s not found".formatted( .post("/render", new RenderHandler());
exchange.getRequestURI())));
this.undertow = Undertow.builder() this.undertow = Undertow.builder()
.addHttpListener(config.getPort(), config.getAddress().getHostAddress()) .addHttpListener(port, hostAddress)
.setHandler(routingHandler) .setHandler(routingHandler)
.build(); .build();
} }
@NonNull
public static Try<LabyrinthServer> createAndStartServer() { public static Try<LabyrinthServer> createAndStartServer() {
final Try<LabyrinthServer> serverOption = Try.of(ServerConfig::init).mapTry(LabyrinthServer::new); return Try.of(ServerConfig::init)
serverOption.forEach(LabyrinthServer::start); .flatMapTry(LabyrinthServer::createAndStartServer);
return serverOption; }
@NonNull
public static Try<LabyrinthServer> createAndStartServer(@NonNull final ServerConfig config) {
return Try.of(() -> new LabyrinthServer(config))
.peek(LabyrinthServer::start);
} }
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();
Try.of(() -> (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress()) log.info("Listeing on http://{}:{}", this.config.getAddress().getHostAddress(), this.config.getPort());
.onFailure(e -> log.warn("Started server, unable to determine listeing address/port."))
.forEach(address -> log.info("Listening on http://{}:{}", address.getHostString(), address.getPort()));
} }
private void stop() { private void stop() {
@ -51,110 +54,6 @@ public class LabyrinthServer {
log.info("Server stopped."); log.info("Server stopped.");
} }
// 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<RequestParameter, String> 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<Long> idOption = requestParams.get(RequestParameter.ID)
// .toTry()
// .map(Long::valueOf)
// .toOption();
// final Option<OutputType> 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 byte[] render;
// try {
// final Labyrinth labyrinth = new Labyrinth(width, height, id);
// 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.getMessage()).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();
// }
// });
// }
// @NonNull
// private Map<RequestParameter, String> parseQueryString(@Nullable final String query) {
// if (query == null) {
// return HashMap.empty();
// }
// HashMap<RequestParameter, String> result = HashMap.empty();
// final String[] parts = query.split("&");
// for (final String part : parts) {
// final int split = part.indexOf('=');
// if (split == -1) {
// final Try<RequestParameter> tryKey = Try.of(() -> this.normalizeParameterName(part));
// if (tryKey.isSuccess()) {
// result = result.put(tryKey.get(), null);
// }
// } else {
// final String key = part.substring(0, split);
// final String value = part.substring(split + 1);
// final Try<RequestParameter> tryKey = Try.of(() -> this.normalizeParameterName(key));
// if (tryKey.isSuccess()) {
// result = result.put(tryKey.get(), value);
// }
// }
// }
// return result;
// }
// private RequestParameter normalizeParameterName(final String paramName) {
// return RequestParameter.parseName(paramName).get();
// }
// private <T> T getOrDefault(@NonNull final Option<String> input,
// @NonNull final Function<String, T> mapper,
// @Nullable final T defaultValue) {
// return input.toTry().map(mapper).getOrElse(defaultValue);
// }
// private void handleRender(final HttpExchange exchange) { // private void handleRender(final HttpExchange exchange) {
// this.executorService.submit(() -> { // this.executorService.submit(() -> {
// try { // try {
@ -198,23 +97,4 @@ public class LabyrinthServer {
// } // }
// }); // });
// } // }
// private enum RequestParameter {
// WIDTH("w", "width"),
// HEIGHT("h", "height"),
// ID("i", "id"),
// OUTPUT("o", "output");
// private final Set<String> names;
//
// RequestParameter(@NonNull final String... names) {
// 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));
// }
// }
} }

View file

@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class Main { public class Main {
public static void main(String[] args) { public static void main(String[] args) {
LabyrinthServer.createAndStartServer().onFailure(e -> log.error("Failed to create server. Stopping.", e)); LabyrinthServer.createAndStartServer()
.onFailure(e -> log.error("Failed to create server. Stopping.", e));
} }
} }

View file

@ -17,38 +17,51 @@ import java.util.function.Function;
public enum OutputType { public enum OutputType {
TEXT_PLAIN("text/plain; charset=UTF-8", TEXT_PLAIN("text/plain; charset=UTF-8",
labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
"txt", "txt",
labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
false, false,
"t", "t",
"text" "text"),
),
HTML("text/html", HTML("text/html",
labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
"html", "html",
labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
false, false,
"h", "h",
"html" "html"),
), PDF("application/pdf",
PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", false, "p", "pdf"), "pdf",
PDFFILE("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", true, "f", "pdffile"), labyrinth -> PDFRenderer.newInstance().render(labyrinth),
BINARY("application/octet-stream", SerializerDeserializer::serialize, "laby", true, "b", "binary"); false,
"p",
"pdf"),
PDFFILE("application/pdf",
"pdf",
labyrinth -> PDFRenderer.newInstance().render(labyrinth),
true,
"f",
"pdffile"),
BINARY("application/octet-stream",
"laby",
SerializerDeserializer::serialize,
true,
"b",
"binary");
@Getter @Getter
@NonNull @NonNull
private final String contentType; private final String contentType;
@Getter
@NonNull @NonNull
private final List<String> names; private final String fileExtension;
@NonNull @NonNull
private final Function<Labyrinth, byte[]> render; private final Function<Labyrinth, byte[]> render;
@Getter @Getter
private final boolean attachment; private final boolean attachment;
@Getter
@NonNull @NonNull
private final String fileExtension; private final List<String> names;
OutputType(@NonNull final String contentType, OutputType(@NonNull final String contentType,
@NonNull final Function<Labyrinth, byte[]> render,
@NonNull final String fileExtension, @NonNull final String fileExtension,
@NonNull final Function<Labyrinth, byte[]> render,
final boolean attachment, final boolean attachment,
@NonNull final String... names) { @NonNull final String... names) {
this.contentType = contentType; this.contentType = contentType;
@ -58,20 +71,23 @@ public enum OutputType {
this.names = List.of(names); this.names = List.of(names);
} }
@NonNull
public static Option<OutputType> ofString(@Nullable final String name) { public static Option<OutputType> ofString(@Nullable final String name) {
if (name == null) { return Option.of(name)
return Option.none(); .map(String::toLowerCase)
} .flatMap(nameLC -> Stream.of(values())
final String nameLC = name.toLowerCase(); .find(param -> param.names.contains(nameLC)));
return Stream.of(values()).find(param -> param.names.contains(nameLC));
} }
@NonNull
@Override @Override
public String toString() { public String toString() {
return this.names.last(); return this.names.last();
} }
public byte[] render(@NonNull final Labyrinth labyrinth) { @NonNull
public byte[] render(@NonNull final Labyrinth labyrinth) throws Exception {
return this.render.apply(labyrinth); return this.render.apply(labyrinth);
} }
} }

View file

@ -17,7 +17,8 @@ public class ServerConfig {
public static final String SYSPROP_HOST = "fritteli.labyrinth.server.host"; public static final String SYSPROP_HOST = "fritteli.labyrinth.server.host";
public static final String SYSPROP_PORT = "fritteli.labyrinth.server.port"; public static final String SYSPROP_PORT = "fritteli.labyrinth.server.port";
@NonNull InetAddress address; @NonNull
InetAddress address;
int port; int port;
public ServerConfig(@Nullable final String address, final int port) throws ConfigurationException { public ServerConfig(@Nullable final String address, final int port) throws ConfigurationException {
@ -26,19 +27,6 @@ public class ServerConfig {
log.debug("host={}, port={}", this.address, this.port); log.debug("host={}, port={}", this.address, this.port);
} }
@NonNull
private static InetAddress validateAddress(@Nullable final String address) {
return Try.of(() -> InetAddress.getByName(address))
.getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause));
}
private static int validatePort(final int port) {
if (port < 0 || port > 0xFFFF) {
throw new ConfigurationException("Port out of range (0..65535): " + port);
}
return port;
}
@NonNull @NonNull
public static ServerConfig init() throws ConfigurationException { public static ServerConfig init() throws ConfigurationException {
final String host = System.getProperty(SYSPROP_HOST); final String host = System.getProperty(SYSPROP_HOST);
@ -47,15 +35,30 @@ public class ServerConfig {
return new ServerConfig(host, port); return new ServerConfig(host, port);
} }
@NonNull
private static InetAddress validateAddress(@Nullable final String address) {
return Try.of(() -> InetAddress.getByName(address))
.getOrElseThrow(cause -> new ConfigurationException(
"Invalid hostname/address: %s".formatted(address),
cause
));
}
private static int validatePort(final int port) {
if (port < 0 || port > 0xFFFF) {
throw new ConfigurationException("Port out of range (0..65535): %s".formatted(port));
}
return port;
}
private static int validatePort(@Nullable final String portString) { private static int validatePort(@Nullable final String portString) {
if (portString == null) { if (portString == null) {
log.info("No port configured; using default."); log.info("No port configured; using default.");
return 0; return 0;
} }
return Try.of(() -> Integer.valueOf(portString)) return Try.of(() -> Integer.valueOf(portString))
.map(ServerConfig::validatePort)
.getOrElseThrow(cause -> new ConfigurationException( .getOrElseThrow(cause -> new ConfigurationException(
"Failed to parse port specified in system property '" + SYSPROP_PORT + "': " + portString, "Failed to parse port specified in system property '%s': %s".formatted(SYSPROP_PORT, portString),
cause cause
)); ));
} }

View file

@ -5,22 +5,28 @@ import io.undertow.server.HttpServerExchange;
import io.undertow.util.StatusCodes; import io.undertow.util.StatusCodes;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import java.util.UUID;
@Slf4j @Slf4j
public abstract class AbstractHttpHandler implements HttpHandler { public abstract class AbstractHttpHandler implements HttpHandler {
@Override @Override
public final void handleRequest(final HttpServerExchange exchange) { public final void handleRequest(@NonNull final HttpServerExchange exchange) {
if (exchange.isInIoThread()) { try (final MDC.MDCCloseable mdc = MDC.putCloseable("correlationId", UUID.randomUUID().toString())) {
exchange.dispatch(this); if (exchange.isInIoThread()) {
return; log.debug("Dispatching request");
} exchange.dispatch(this);
try { return;
this.handle(exchange); }
} catch (@NonNull final Exception e) { try {
log.error("Error handling request", e); this.handle(exchange);
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) } catch (@NonNull final Exception e) {
.getResponseSender() log.error("Error handling request", e);
.send(StatusCodes.INTERNAL_SERVER_ERROR_STRING); exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR)
.getResponseSender()
.send(StatusCodes.INTERNAL_SERVER_ERROR_STRING);
}
} }
} }

View file

@ -14,11 +14,15 @@ import io.vavr.control.Try;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.MDC;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
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.UUID;
@Slf4j @Slf4j
public class CreateHandler extends AbstractHttpHandler { public class CreateHandler extends AbstractHttpHandler {
@ -26,19 +30,34 @@ public class CreateHandler extends AbstractHttpHandler {
@Override @Override
protected void handle(@NonNull final HttpServerExchange exchange) { protected void handle(@NonNull final HttpServerExchange exchange) {
final Instant start = Instant.now();
log.debug("Handling create request");
this.createLabyrinthFromRequestParameters(exchange.getQueryParameters()) this.createLabyrinthFromRequestParameters(exchange.getQueryParameters())
.onFailure(e -> { .onFailure(e -> {
log.error("Error creating Labyrinth from request", e);
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
exchange.setReasonPhrase(e.getMessage()); exchange.setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING);
}).forEach(tuple -> { })
.forEach(tuple -> {
final OutputType outputType = tuple._1(); final OutputType outputType = tuple._1();
final Labyrinth labyrinth = tuple._2(); final Labyrinth labyrinth = tuple._2();
final byte[] bytes = outputType.render(labyrinth); final byte[] bytes;
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, outputType.getContentType()); try {
exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(labyrinth.getRandomSeed())); bytes = outputType.render(labyrinth);
exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth())); } catch (@NonNull final Exception e) {
exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(labyrinth.getHeight())); log.error("Error rendering Labyrinth", e);
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
exchange.setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING);
return;
}
exchange.getResponseHeaders()
.put(Headers.CONTENT_TYPE, outputType.getContentType())
.put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(labyrinth.getRandomSeed()))
.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(start.until(Instant.now(), ChronoUnit.MILLIS)));
exchange.getResponseSender().send(ByteBuffer.wrap(bytes)); exchange.getResponseSender().send(ByteBuffer.wrap(bytes));
log.debug("Create request handled.");
}); });
} }
@ -73,13 +92,24 @@ public class CreateHandler extends AbstractHttpHandler {
}); });
} }
@NonNull Try<Tuple2<OutputType, Labyrinth>> createLabyrinth() { @NonNull
final Option<OutputType> output = getParameterValues(RequestParameter.OUTPUT).foldLeft(Option.none(), (type, param) -> type.orElse(() -> OutputType.ofString(param))); Try<Tuple2<OutputType, Labyrinth>> createLabyrinth() {
final Option<Integer> width = getParameterValues(RequestParameter.WIDTH).foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption())); final Option<OutputType> output = getParameterValues(RequestParameter.OUTPUT)
final Option<Integer> height = getParameterValues(RequestParameter.HEIGHT).foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption())); .foldLeft(Option.none(), (type, param) -> type.orElse(() -> OutputType.ofString(param)));
final Option<Long> id = getParameterValues(RequestParameter.ID).foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Long.parseLong(param)).toOption())); final Option<Integer> width = getParameterValues(RequestParameter.WIDTH)
.foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption()));
final Option<Integer> height = getParameterValues(RequestParameter.HEIGHT)
.foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption()));
final Option<Long> id = getParameterValues(RequestParameter.ID)
.foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Long.parseLong(param)).toOption()));
return Try.of(() -> Tuple.of(output.get(), new Labyrinth(width.get(), height.get(), id.getOrElse(() -> new Random().nextLong())))); return Try.of(() -> {
final OutputType t1 = output.get();
final Integer width1 = width.get();
final Integer height1 = height.get();
final Long orElse = id.getOrElse(() -> new Random().nextLong());
return Tuple.of(t1, new Labyrinth(width1, height1, orElse));
});
} }
@NonNull @NonNull

View file

@ -4,7 +4,7 @@ import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import io.undertow.util.StatusCodes; import io.undertow.util.StatusCodes;
public class LanyrinthRenderHandler implements HttpHandler { public class RenderHandler implements HttpHandler {
@Override @Override
public void handleRequest(final HttpServerExchange exchange) { public void handleRequest(final HttpServerExchange exchange) {
if (exchange.isInIoThread()) { if (exchange.isInIoThread()) {

View file

@ -6,7 +6,7 @@
<!-- encoders are by default assigned the type <!-- encoders are by default assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder --> ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder> <encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{HH:mm:ss.SSS} %-5level %X{correlationId} [%thread] %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>