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>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
<version>2.2.22.Final</version>
<version>2.3.5.Final</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>

View file

@ -1,48 +1,51 @@
package ch.fritteli.labyrinth.server;
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.server.RoutingHandler;
import io.undertow.util.StatusCodes;
import io.vavr.control.Try;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
@Slf4j
public class LabyrinthServer {
@NonNull
private final ServerConfig config;
@NonNull
private final Undertow undertow;
private LabyrinthServer(@NonNull final ServerConfig config) {
log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort());
final RoutingHandler routingHandler = new RoutingHandler().get(CreateHandler.PATH_TEMPLATE, new CreateHandler())
.post("/render", new LanyrinthRenderHandler())
.setFallbackHandler(exchange -> exchange.setStatusCode(
StatusCodes.NOT_FOUND)
.getResponseSender()
.send("Resource %s not found".formatted(
exchange.getRequestURI())));
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());
this.undertow = Undertow.builder()
.addHttpListener(config.getPort(), config.getAddress().getHostAddress())
.addHttpListener(port, hostAddress)
.setHandler(routingHandler)
.build();
}
@NonNull
public static Try<LabyrinthServer> createAndStartServer() {
final Try<LabyrinthServer> serverOption = Try.of(ServerConfig::init).mapTry(LabyrinthServer::new);
serverOption.forEach(LabyrinthServer::start);
return serverOption;
return Try.of(ServerConfig::init)
.flatMapTry(LabyrinthServer::createAndStartServer);
}
@NonNull
public static Try<LabyrinthServer> createAndStartServer(@NonNull final ServerConfig config) {
return Try.of(() -> new LabyrinthServer(config))
.peek(LabyrinthServer::start);
}
private void start() {
Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper"));
this.undertow.start();
Try.of(() -> (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress())
.onFailure(e -> log.warn("Started server, unable to determine listeing address/port."))
.forEach(address -> log.info("Listening on http://{}:{}", address.getHostString(), address.getPort()));
log.info("Listeing on http://{}:{}", this.config.getAddress().getHostAddress(), this.config.getPort());
}
private void stop() {
@ -51,110 +54,6 @@ public class LabyrinthServer {
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) {
// this.executorService.submit(() -> {
// 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
public class Main {
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 {
TEXT_PLAIN("text/plain; charset=UTF-8",
labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
"txt",
labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
false,
"t",
"text"
),
"text"),
HTML("text/html",
labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
"html",
labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
false,
"h",
"html"
),
PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", false, "p", "pdf"),
PDFFILE("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", true, "f", "pdffile"),
BINARY("application/octet-stream", SerializerDeserializer::serialize, "laby", true, "b", "binary");
"html"),
PDF("application/pdf",
"pdf",
labyrinth -> PDFRenderer.newInstance().render(labyrinth),
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
@NonNull
private final String contentType;
@Getter
@NonNull
private final List<String> names;
private final String fileExtension;
@NonNull
private final Function<Labyrinth, byte[]> render;
@Getter
private final boolean attachment;
@Getter
@NonNull
private final String fileExtension;
private final List<String> names;
OutputType(@NonNull final String contentType,
@NonNull final Function<Labyrinth, byte[]> render,
@NonNull final String fileExtension,
@NonNull final Function<Labyrinth, byte[]> render,
final boolean attachment,
@NonNull final String... names) {
this.contentType = contentType;
@ -58,20 +71,23 @@ public enum OutputType {
this.names = List.of(names);
}
@NonNull
public static Option<OutputType> ofString(@Nullable final String name) {
if (name == null) {
return Option.none();
}
final String nameLC = name.toLowerCase();
return Stream.of(values()).find(param -> param.names.contains(nameLC));
return Option.of(name)
.map(String::toLowerCase)
.flatMap(nameLC -> Stream.of(values())
.find(param -> param.names.contains(nameLC)));
}
@NonNull
@Override
public String toString() {
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);
}
}

View file

@ -17,7 +17,8 @@ public class ServerConfig {
public static final String SYSPROP_HOST = "fritteli.labyrinth.server.host";
public static final String SYSPROP_PORT = "fritteli.labyrinth.server.port";
@NonNull InetAddress address;
@NonNull
InetAddress address;
int port;
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);
}
@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
public static ServerConfig init() throws ConfigurationException {
final String host = System.getProperty(SYSPROP_HOST);
@ -47,15 +35,30 @@ public class ServerConfig {
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) {
if (portString == null) {
log.info("No port configured; using default.");
return 0;
}
return Try.of(() -> Integer.valueOf(portString))
.map(ServerConfig::validatePort)
.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
));
}

View file

@ -5,22 +5,28 @@ import io.undertow.server.HttpServerExchange;
import io.undertow.util.StatusCodes;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import java.util.UUID;
@Slf4j
public abstract class AbstractHttpHandler implements HttpHandler {
@Override
public final void handleRequest(final HttpServerExchange exchange) {
if (exchange.isInIoThread()) {
exchange.dispatch(this);
return;
}
try {
this.handle(exchange);
} catch (@NonNull final Exception e) {
log.error("Error handling request", e);
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR)
.getResponseSender()
.send(StatusCodes.INTERNAL_SERVER_ERROR_STRING);
public final void handleRequest(@NonNull final HttpServerExchange exchange) {
try (final MDC.MDCCloseable mdc = MDC.putCloseable("correlationId", UUID.randomUUID().toString())) {
if (exchange.isInIoThread()) {
log.debug("Dispatching request");
exchange.dispatch(this);
return;
}
try {
this.handle(exchange);
} catch (@NonNull final Exception e) {
log.error("Error handling request", e);
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.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import org.slf4j.MDC;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Deque;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
@Slf4j
public class CreateHandler extends AbstractHttpHandler {
@ -26,19 +30,34 @@ public class CreateHandler extends AbstractHttpHandler {
@Override
protected void handle(@NonNull final HttpServerExchange exchange) {
final Instant start = Instant.now();
log.debug("Handling create request");
this.createLabyrinthFromRequestParameters(exchange.getQueryParameters())
.onFailure(e -> {
log.error("Error creating Labyrinth from request", e);
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
exchange.setReasonPhrase(e.getMessage());
}).forEach(tuple -> {
exchange.setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING);
})
.forEach(tuple -> {
final OutputType outputType = tuple._1();
final Labyrinth labyrinth = tuple._2();
final byte[] bytes = outputType.render(labyrinth);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, outputType.getContentType());
exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(labyrinth.getRandomSeed()));
exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth()));
exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(labyrinth.getHeight()));
final byte[] bytes;
try {
bytes = outputType.render(labyrinth);
} catch (@NonNull final Exception e) {
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));
log.debug("Create request handled.");
});
}
@ -73,13 +92,24 @@ public class CreateHandler extends AbstractHttpHandler {
});
}
@NonNull Try<Tuple2<OutputType, Labyrinth>> createLabyrinth() {
final Option<OutputType> output = getParameterValues(RequestParameter.OUTPUT).foldLeft(Option.none(), (type, param) -> type.orElse(() -> OutputType.ofString(param)));
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()));
@NonNull
Try<Tuple2<OutputType, Labyrinth>> createLabyrinth() {
final Option<OutputType> output = getParameterValues(RequestParameter.OUTPUT)
.foldLeft(Option.none(), (type, param) -> type.orElse(() -> OutputType.ofString(param)));
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

View file

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

View file

@ -6,7 +6,7 @@
<!-- encoders are by default assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<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>
</appender>