feature/undertow #4

Merged
manuel merged 15 commits from feature/undertow into master 2023-04-08 22:40:46 +02:00
6 changed files with 202 additions and 191 deletions
Showing only changes of commit dd571a190a - Show all commits

View file

@ -1,10 +1,11 @@
package ch.fritteli.labyrinth.server; package ch.fritteli.labyrinth.server;
import ch.fritteli.labyrinth.generator.model.Labyrinth; import ch.fritteli.labyrinth.server.handler.CreateHandler;
import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer; import ch.fritteli.labyrinth.server.undertow_playground.LanyrinthRenderHandler;
import com.sun.net.httpserver.Headers; import io.undertow.Undertow;
import com.sun.net.httpserver.HttpExchange; import io.undertow.server.RoutingHandler;
import com.sun.net.httpserver.HttpServer; import io.undertow.server.handlers.RedirectHandler;
import io.undertow.util.StatusCodes;
import io.vavr.collection.HashMap; import io.vavr.collection.HashMap;
import io.vavr.collection.HashSet; import io.vavr.collection.HashSet;
import io.vavr.collection.Map; import io.vavr.collection.Map;
@ -16,28 +17,13 @@ import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function; import java.util.function.Function;
@Slf4j @Slf4j
public class LabyrinthServer { public class LabyrinthServer {
@NonNull @NonNull
private final HttpServer httpServer; private final Undertow undertow;
@NonNull
private final ExecutorService executorService = new ThreadPoolExecutor(0,
1_000,
5,
TimeUnit.SECONDS,
new SynchronousQueue<>()
);
public static Option<LabyrinthServer> createAndStartServer() { public static Option<LabyrinthServer> createAndStartServer() {
final Option<LabyrinthServer> serverOption = Try.of(ServerConfig::init) final Option<LabyrinthServer> serverOption = Try.of(ServerConfig::init)
@ -51,147 +37,106 @@ public class LabyrinthServer {
return serverOption; return serverOption;
} }
public LabyrinthServer(@NonNull final ServerConfig config) throws IOException { public LabyrinthServer(@NonNull final ServerConfig config) {
this.httpServer = HttpServer.create(new InetSocketAddress(config.getAddress(), config.getPort()), 5); log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort());
this.httpServer.createContext("/", new StaticResourcesFileHandler(this.executorService)); final RoutingHandler routingHandler = new RoutingHandler().get("/", new RedirectHandler("/create/text"))
this.httpServer.createContext("/create", this::handleCreate); .get("/create", new RedirectHandler("/create/text"))
this.httpServer.createContext("/render", this::handleRender); .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();
} }
public void start() { public void start() {
Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper"));
this.httpServer.start(); this.undertow.start();
log.info("Listening on http://{}:{}", Try.of(() -> (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress())
this.httpServer.getAddress().getHostString(), .onFailure(e -> log.warn("Started server, unable to determine listeing address/port."))
this.httpServer.getAddress().getPort() .forEach(address -> log.info("Listening on http://{}:{}", address.getHostString(), address.getPort()));
);
}
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();
}
});
}
private void handleRender(final HttpExchange exchange) {
this.executorService.submit(() -> {
try {
log.debug("Handling request to {}", exchange.getRequestURI());
final String requestMethod = exchange.getRequestMethod();
if (!requestMethod.equals("POST")) {
exchange.getResponseBody().close();
exchange.sendResponseHeaders(405, -1);
return;
}
final byte[] bytes = exchange.getRequestBody().readAllBytes();
final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes);
final OutputType output = exchange.getRequestHeaders()
.get("Accept")
.contains(OutputType.HTML.getContentType()) ?
OutputType.HTML :
OutputType.TEXT_PLAIN;
final byte[] render;
final Headers responseHeaders = exchange.getResponseHeaders();
try {
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).getBytes(StandardCharsets.UTF_8));
responseBody.flush();
return;
}
responseHeaders.add("Content-type", output.getContentType());
exchange.sendResponseHeaders(200, 0);
final OutputStream responseBody = exchange.getResponseBody();
responseBody.write(render);
responseBody.flush();
} catch (Exception e) {
log.error("FSCK!", e);
} finally {
exchange.close();
}
});
} }
public void stop() { public void stop() {
log.info("Stopping server ..."); log.info("Stopping server ...");
this.httpServer.stop(5); this.undertow.stop();
this.executorService.shutdown();
try {
if (!this.executorService.awaitTermination(5, TimeUnit.SECONDS)) {
log.warn("Timeout occurred while awaiting termination of executor service");
}
} catch (final InterruptedException e) {
log.error("Failed to await termination of executor service", e);
}
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 @NonNull
private Map<RequestParameter, String> parseQueryString(@Nullable final String query) { private Map<RequestParameter, String> parseQueryString(@Nullable final String query) {
if (query == null) { if (query == null) {
@ -218,15 +163,59 @@ public class LabyrinthServer {
return result; return result;
} }
private RequestParameter normalizeParameterName(final String paramName) {
return RequestParameter.parseName(paramName).get();
}
private <T> T getOrDefault(@NonNull final Option<String> input, private <T> T getOrDefault(@NonNull final Option<String> input,
@NonNull final Function<String, T> mapper, @NonNull final Function<String, T> mapper,
@Nullable final T defaultValue) { @Nullable final T defaultValue) {
return input.toTry().map(mapper).getOrElse(defaultValue); return input.toTry().map(mapper).getOrElse(defaultValue);
} }
private RequestParameter normalizeParameterName(final String paramName) { // private void handleRender(final HttpExchange exchange) {
return RequestParameter.parseName(paramName).get(); // this.executorService.submit(() -> {
} // try {
// log.debug("Handling request to {}", exchange.getRequestURI());
// final String requestMethod = exchange.getRequestMethod();
// if (!requestMethod.equals("POST")) {
// exchange.getResponseBody().close();
// exchange.sendResponseHeaders(405, -1);
// return;
// }
// final byte[] bytes = exchange.getRequestBody().readAllBytes();
//
// final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes);
//
// final OutputType output = exchange.getRequestHeaders()
// .get("Accept")
// .contains(OutputType.HTML.getContentType()) ?
// OutputType.HTML :
// OutputType.TEXT_PLAIN;
// final byte[] render;
// final Headers responseHeaders = exchange.getResponseHeaders();
// try {
// 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).getBytes(StandardCharsets.UTF_8));
// responseBody.flush();
// return;
// }
// responseHeaders.add("Content-type", output.getContentType());
// exchange.sendResponseHeaders(200, 0);
// final OutputStream responseBody = exchange.getResponseBody();
// responseBody.write(render);
// responseBody.flush();
// } catch (Exception e) {
// log.error("FSCK!", e);
// } finally {
// exchange.close();
// }
// });
// }
private enum RequestParameter { private enum RequestParameter {
WIDTH("w", "width"), WIDTH("w", "width"),

View file

@ -1,21 +1,18 @@
package ch.fritteli.labyrinth.server; package ch.fritteli.labyrinth.server;
import ch.fritteli.labyrinth.server.undertow_playground.UndertowPlayground;
import io.undertow.Undertow;
import lombok.extern.slf4j.Slf4j; 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() LabyrinthServer.createAndStartServer().onEmpty(() -> log.error("Failed to create server. Stopping."));
// .onEmpty(() -> log.error("Failed to create server. Stopping."));
final ServerConfig config = ServerConfig.init(); // final ServerConfig config = ServerConfig.init();
log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort()); // log.info("Starting Server at http://{}:{}/", config.getAddress().getHostAddress(), config.getPort());
Undertow.builder() // Undertow.builder()
.addHttpListener(config.getPort(), config.getAddress().getHostAddress()) // .addHttpListener(config.getPort(), config.getAddress().getHostAddress())
.setHandler(UndertowPlayground.r) // .setHandler(UndertowPlayground.r)
.build() // .build()
.start(); // .start();
} }
} }

View file

@ -0,0 +1,28 @@
package ch.fritteli.labyrinth.server.handler;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.StatusCodes;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
@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);
}
}
protected abstract void handle(@NonNull final HttpServerExchange exchange) throws Exception;
}

View file

@ -1,12 +1,14 @@
package ch.fritteli.labyrinth.server.undertow_playground; 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 io.undertow.server.HttpHandler; 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.vavr.control.Option; import io.vavr.control.Option;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -15,13 +17,9 @@ import java.util.Map;
import java.util.Random; import java.util.Random;
@Slf4j @Slf4j
class LabyrinthHttpHandler implements HttpHandler { public class CreateHandler extends AbstractHttpHandler {
@Override @Override
public void handleRequest(HttpServerExchange exchange) throws Exception { protected void handle(@NonNull final HttpServerExchange exchange) throws Exception {
if (exchange.isInIoThread()) {
exchange.dispatch(this);
return;
}
final Map<String, Deque<String>> queryParameters = exchange.getQueryParameters(); final Map<String, Deque<String>> queryParameters = exchange.getQueryParameters();
final Option<String> output = UndertowPlayground.getFirstOption(queryParameters, "output"); final Option<String> output = UndertowPlayground.getFirstOption(queryParameters, "output");
final Option<Integer> width = UndertowPlayground.getIntOption(queryParameters, "width"); final Option<Integer> width = UndertowPlayground.getIntOption(queryParameters, "width");

View file

@ -0,0 +1,17 @@
package ch.fritteli.labyrinth.server.undertow_playground;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.StatusCodes;
public class LanyrinthRenderHandler implements HttpHandler {
@Override
public void handleRequest(final HttpServerExchange exchange) {
if (exchange.isInIoThread()) {
exchange.dispatch(this);
return;
}
exchange.setStatusCode(StatusCodes.NOT_IMPLEMENTED);
exchange.getResponseSender().send("Rendering binary data is not implemented yet.");
}
}

View file

@ -14,24 +14,6 @@ import java.util.Map;
@Slf4j @Slf4j
public class UndertowPlayground { public class UndertowPlayground {
public static final RoutingHandler r = new RoutingHandler().get("/create/{output}", new LabyrinthHttpHandler()).post("/render", new HttpHandler() {
@Override
public void handleRequest(final HttpServerExchange exchange) {
if (exchange.isInIoThread()) {
exchange.dispatch(this);
return;
}
exchange.getResponseSender().send("TODO: read body, render stuff");
}
}).setFallbackHandler(new HttpHandler() {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
exchange.getResponseSender().send("Request: " + exchange.getRequestURI());
final HeaderValues strings = exchange.getRequestHeaders().get(Headers.ACCEPT);
strings.peekFirst();
}
});
@NonNull @NonNull
public static Option<Integer> getIntOption(@NonNull final Map<String, Deque<String>> queryParams, public static Option<Integer> getIntOption(@NonNull final Map<String, Deque<String>> queryParams,
@NonNull final String paramName) { @NonNull final String paramName) {