feature/undertow #4
8 changed files with 105 additions and 199 deletions
|
@ -1,10 +1,9 @@
|
||||||
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.undertow_playground.LanyrinthRenderHandler;
|
import ch.fritteli.labyrinth.server.handler.LanyrinthRenderHandler;
|
||||||
import io.undertow.Undertow;
|
import io.undertow.Undertow;
|
||||||
import io.undertow.server.RoutingHandler;
|
import io.undertow.server.RoutingHandler;
|
||||||
import io.undertow.server.handlers.RedirectHandler;
|
|
||||||
import io.undertow.util.StatusCodes;
|
import io.undertow.util.StatusCodes;
|
||||||
import io.vavr.control.Try;
|
import io.vavr.control.Try;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
@ -17,36 +16,33 @@ public class LabyrinthServer {
|
||||||
@NonNull
|
@NonNull
|
||||||
private final Undertow undertow;
|
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.undertow = Undertow.builder()
|
||||||
|
.addHttpListener(config.getPort(), config.getAddress().getHostAddress())
|
||||||
|
.setHandler(routingHandler)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
public static Try<LabyrinthServer> createAndStartServer() {
|
public static Try<LabyrinthServer> createAndStartServer() {
|
||||||
final Try<LabyrinthServer> serverOption = Try.of(ServerConfig::init).mapTry(LabyrinthServer::new);
|
final Try<LabyrinthServer> serverOption = Try.of(ServerConfig::init).mapTry(LabyrinthServer::new);
|
||||||
serverOption.forEach(LabyrinthServer::start);
|
serverOption.forEach(LabyrinthServer::start);
|
||||||
return serverOption;
|
return serverOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
private LabyrinthServer(@NonNull final ServerConfig config) {
|
|
||||||
log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort());
|
|
||||||
final RoutingHandler routingHandler = new RoutingHandler().get("/", new RedirectHandler("/create/text"))
|
|
||||||
.get("/create", new RedirectHandler("/create/text"))
|
|
||||||
.get("/create/{output}", new CreateHandler())
|
|
||||||
.post("/render", new LanyrinthRenderHandler())
|
|
||||||
.setFallbackHandler(exchange -> {
|
|
||||||
exchange.setStatusCode(StatusCodes.NOT_FOUND)
|
|
||||||
.getResponseSender()
|
|
||||||
.send("Resource %s not found".formatted(
|
|
||||||
exchange.getRequestURI()));
|
|
||||||
});
|
|
||||||
this.undertow = Undertow.builder()
|
|
||||||
.addHttpListener(config.getPort(), config.getAddress().getHostAddress())
|
|
||||||
.setHandler(routingHandler)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
Try.of(() -> (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress())
|
||||||
.onFailure(e -> log.warn("Started server, unable to determine listeing address/port."))
|
.onFailure(e -> log.warn("Started server, unable to determine listeing address/port."))
|
||||||
.forEach(address -> log.info("Listening on http://{}:{}", address.getHostString(), address.getPort()));
|
.forEach(address -> log.info("Listening on http://{}:{}", address.getHostString(), address.getPort()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stop() {
|
private void stop() {
|
||||||
|
|
|
@ -17,18 +17,18 @@ 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),
|
labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
|
||||||
"txt",
|
"txt",
|
||||||
false,
|
false,
|
||||||
"t",
|
"t",
|
||||||
"text"
|
"text"
|
||||||
),
|
),
|
||||||
HTML("text/html",
|
HTML("text/html",
|
||||||
labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
|
labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
|
||||||
"html",
|
"html",
|
||||||
false,
|
false,
|
||||||
"h",
|
"h",
|
||||||
"html"
|
"html"
|
||||||
),
|
),
|
||||||
PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", false, "p", "pdf"),
|
PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", false, "p", "pdf"),
|
||||||
PDFFILE("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", true, "f", "pdffile"),
|
PDFFILE("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", true, "f", "pdffile"),
|
||||||
|
|
|
@ -29,7 +29,7 @@ public class ServerConfig {
|
||||||
@NonNull
|
@NonNull
|
||||||
private static InetAddress validateAddress(@Nullable final String address) {
|
private static InetAddress validateAddress(@Nullable final String address) {
|
||||||
return Try.of(() -> InetAddress.getByName(address))
|
return Try.of(() -> InetAddress.getByName(address))
|
||||||
.getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause));
|
.getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int validatePort(final int port) {
|
private static int validatePort(final int port) {
|
||||||
|
@ -53,10 +53,10 @@ public class ServerConfig {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return Try.of(() -> Integer.valueOf(portString))
|
return Try.of(() -> Integer.valueOf(portString))
|
||||||
.map(ServerConfig::validatePort)
|
.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 '" + SYSPROP_PORT + "': " + portString,
|
||||||
cause
|
cause
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,109 +0,0 @@
|
||||||
package ch.fritteli.labyrinth.server;
|
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
|
||||||
import com.sun.net.httpserver.HttpHandler;
|
|
||||||
import lombok.NonNull;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.pdfbox.io.IOUtils;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class StaticResourcesFileHandler implements HttpHandler {
|
|
||||||
|
|
||||||
public static final String WEBASSETS_DIRECTORY = "webassets";
|
|
||||||
private final ExecutorService executorService;
|
|
||||||
|
|
||||||
public StaticResourcesFileHandler(final ExecutorService executorService) {
|
|
||||||
this.executorService = executorService;
|
|
||||||
log.debug("Created {}", this.getClass().getSimpleName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handle(@NonNull final HttpExchange exchange) throws IOException {
|
|
||||||
this.executorService.submit(() -> {
|
|
||||||
try {
|
|
||||||
final URI requestURI = exchange.getRequestURI();
|
|
||||||
final String path = requestURI.getPath();
|
|
||||||
log.debug("Handling request to {}", path);
|
|
||||||
if ("/".equals(path)) {
|
|
||||||
redirect(exchange, "index.html");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!path.startsWith("/")) {
|
|
||||||
notFound(exchange, path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final String mimeType = getMimeType(path);
|
|
||||||
if (mimeType == null) {
|
|
||||||
notFound(exchange, path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final byte[] responseBytes = getBytes(path);
|
|
||||||
if (responseBytes.length == 0) {
|
|
||||||
notFound(exchange, path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.debug(
|
|
||||||
"Serving {}{} with mimetype {}: {} bytes",
|
|
||||||
WEBASSETS_DIRECTORY,
|
|
||||||
path,
|
|
||||||
mimeType,
|
|
||||||
responseBytes.length
|
|
||||||
);
|
|
||||||
exchange.getResponseHeaders().add("Content-type", mimeType);
|
|
||||||
exchange.sendResponseHeaders(200, 0);
|
|
||||||
exchange.getResponseBody().write(responseBytes);
|
|
||||||
exchange.getResponseBody().flush();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("FSCK!", e);
|
|
||||||
} finally {
|
|
||||||
exchange.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void redirect(@NonNull final HttpExchange exchange, @NonNull final String target) throws
|
|
||||||
IOException {
|
|
||||||
log.debug("Sending redirect to {}", target);
|
|
||||||
exchange.getResponseHeaders().add("Location", target);
|
|
||||||
exchange.sendResponseHeaders(302, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void notFound(@NonNull final HttpExchange exchange, @NonNull final String path) throws IOException {
|
|
||||||
log.debug("Resource '{}' not found, replying with HTTP 404", path);
|
|
||||||
exchange.getResponseHeaders().add("Content-type", "text/plain; charset=utf-8");
|
|
||||||
exchange.sendResponseHeaders(404, 0);
|
|
||||||
exchange.getResponseBody().write("404 - Not found".getBytes(StandardCharsets.UTF_8));
|
|
||||||
exchange.getResponseBody().flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static String getMimeType(@NonNull final String path) {
|
|
||||||
if (path.endsWith(".html")) {
|
|
||||||
return "text/html";
|
|
||||||
}
|
|
||||||
if (path.endsWith(".css")) {
|
|
||||||
return "text/css";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static byte[] getBytes(@NonNull final String path) throws IOException {
|
|
||||||
final InputStream stream = StaticResourcesFileHandler.class.getClassLoader()
|
|
||||||
.getResourceAsStream(WEBASSETS_DIRECTORY + path);
|
|
||||||
if (stream == null) {
|
|
||||||
log.debug("Resource '{}' not found in classpath.", path);
|
|
||||||
return new byte[0];
|
|
||||||
}
|
|
||||||
final byte[] response = IOUtils.toByteArray(stream);
|
|
||||||
log.debug("Sending reply; {} bytes", response.length);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,14 +2,18 @@ package ch.fritteli.labyrinth.server.handler;
|
||||||
|
|
||||||
import ch.fritteli.labyrinth.generator.model.Labyrinth;
|
import ch.fritteli.labyrinth.generator.model.Labyrinth;
|
||||||
import ch.fritteli.labyrinth.server.OutputType;
|
import ch.fritteli.labyrinth.server.OutputType;
|
||||||
import ch.fritteli.labyrinth.server.handler.AbstractHttpHandler;
|
|
||||||
import ch.fritteli.labyrinth.server.undertow_playground.UndertowPlayground;
|
|
||||||
import io.undertow.server.HttpServerExchange;
|
import io.undertow.server.HttpServerExchange;
|
||||||
import io.undertow.util.Headers;
|
import io.undertow.util.Headers;
|
||||||
import io.undertow.util.HttpString;
|
import io.undertow.util.HttpString;
|
||||||
|
import io.undertow.util.StatusCodes;
|
||||||
|
import io.vavr.Tuple;
|
||||||
|
import io.vavr.Tuple2;
|
||||||
|
import io.vavr.collection.*;
|
||||||
import io.vavr.control.Option;
|
import io.vavr.control.Option;
|
||||||
|
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 java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
|
@ -18,26 +22,69 @@ import java.util.Random;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CreateHandler extends AbstractHttpHandler {
|
public class CreateHandler extends AbstractHttpHandler {
|
||||||
@Override
|
public static final String PATH_TEMPLATE = "/create/{output}";
|
||||||
protected void handle(@NonNull final HttpServerExchange exchange) throws Exception {
|
|
||||||
final Map<String, Deque<String>> queryParameters = exchange.getQueryParameters();
|
|
||||||
final Option<String> output = UndertowPlayground.getFirstOption(queryParameters, "output");
|
|
||||||
final Option<Integer> width = UndertowPlayground.getIntOption(queryParameters, "width");
|
|
||||||
final Option<Integer> height = UndertowPlayground.getIntOption(queryParameters, "height");
|
|
||||||
final Option<Integer> id = UndertowPlayground.getIntOption(queryParameters, "id");
|
|
||||||
|
|
||||||
log.info("Output: {}", output);
|
@Override
|
||||||
log.info("Width: {}", width);
|
protected void handle(@NonNull final HttpServerExchange exchange) {
|
||||||
log.info("Height: {}", height);
|
this.createLabyrinthFromRequestParameters(exchange.getQueryParameters())
|
||||||
log.info("Id: {}", id);
|
.onFailure(e -> {
|
||||||
final Integer theId = id.getOrElse(() -> new Random().nextInt());
|
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
|
||||||
final Labyrinth labyrinth = new Labyrinth(width.get(), height.get(), theId);
|
exchange.setReasonPhrase(e.getMessage());
|
||||||
final OutputType outputType = output.flatMap(OutputType::ofString).get();
|
}).forEach(tuple -> {
|
||||||
final byte[] result = outputType.render(labyrinth);
|
final OutputType outputType = tuple._1();
|
||||||
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, outputType.getContentType());
|
final Labyrinth labyrinth = tuple._2();
|
||||||
exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(theId));
|
final byte[] bytes = outputType.render(labyrinth);
|
||||||
exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(width.get()));
|
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, outputType.getContentType());
|
||||||
exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(height.get()));
|
exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(labyrinth.getRandomSeed()));
|
||||||
exchange.getResponseSender().send(ByteBuffer.wrap(result));
|
exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth()));
|
||||||
|
exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(labyrinth.getHeight()));
|
||||||
|
exchange.getResponseSender().send(ByteBuffer.wrap(bytes));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull Try<Tuple2<OutputType, Labyrinth>> createLabyrinthFromRequestParameters(final Map<String, Deque<String>> queryParameters) {
|
||||||
|
return new ParametersToLabyrinthExtractor(queryParameters).createLabyrinth();
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ParametersToLabyrinthExtractor {
|
||||||
|
@NonNull
|
||||||
|
private final Multimap<String, String> queryParameters;
|
||||||
|
|
||||||
|
ParametersToLabyrinthExtractor(@NonNull final Map<String, Deque<String>> queryParameters) {
|
||||||
|
this.queryParameters = HashMap.ofAll(queryParameters).foldLeft(HashMultimap.<String>withSet().empty(), (map, tuple) -> {
|
||||||
|
final String key = tuple._1();
|
||||||
|
return Stream.ofAll(tuple._2()).foldLeft(map, (m, value) -> m.put(key, value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@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()))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Stream<String> getParameterValues(@NonNull final RequestParameter parameter) {
|
||||||
|
return parameter.names.toStream().flatMap(name -> Stream.ofAll(this.queryParameters.getOrElse(name, List.empty())));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package ch.fritteli.labyrinth.server.undertow_playground;
|
package ch.fritteli.labyrinth.server.handler;
|
||||||
|
|
||||||
import io.undertow.server.HttpHandler;
|
import io.undertow.server.HttpHandler;
|
||||||
import io.undertow.server.HttpServerExchange;
|
import io.undertow.server.HttpServerExchange;
|
|
@ -1,28 +0,0 @@
|
||||||
package ch.fritteli.labyrinth.server.undertow_playground;
|
|
||||||
|
|
||||||
import io.undertow.server.HttpHandler;
|
|
||||||
import io.undertow.server.HttpServerExchange;
|
|
||||||
import io.undertow.server.RoutingHandler;
|
|
||||||
import io.undertow.util.HeaderValues;
|
|
||||||
import io.undertow.util.Headers;
|
|
||||||
import io.vavr.control.Option;
|
|
||||||
import lombok.NonNull;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class UndertowPlayground {
|
|
||||||
@NonNull
|
|
||||||
public static Option<Integer> getIntOption(@NonNull final Map<String, Deque<String>> queryParams,
|
|
||||||
@NonNull final String paramName) {
|
|
||||||
return getFirstOption(queryParams, paramName).toTry().map(Integer::parseInt).toOption();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static Option<String> getFirstOption(@NonNull final Map<String, Deque<String>> queryParams,
|
|
||||||
@NonNull final String paramName) {
|
|
||||||
return Option.of(queryParams.get(paramName)).map(Deque::peek).flatMap(Option::of);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,5 +13,5 @@
|
||||||
<root level="info">
|
<root level="info">
|
||||||
<appender-ref ref="STDOUT"/>
|
<appender-ref ref="STDOUT"/>
|
||||||
</root>
|
</root>
|
||||||
<logger name="ch.fritteli.labyrinth.server.StaticResourcesFileHandler" level="debug"/>
|
<logger name="ch.fritteli.labyrinth.server.handler.CreateHandler" level="debug"/>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
Loading…
Reference in a new issue