feature/undertow #4
					 8 changed files with 105 additions and 199 deletions
				
			
		|  | @ -1,10 +1,9 @@ | |||
| package ch.fritteli.labyrinth.server; | ||||
| 
 | ||||
| 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.server.RoutingHandler; | ||||
| import io.undertow.server.handlers.RedirectHandler; | ||||
| import io.undertow.util.StatusCodes; | ||||
| import io.vavr.control.Try; | ||||
| import lombok.NonNull; | ||||
|  | @ -17,36 +16,33 @@ public class LabyrinthServer { | |||
|     @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.undertow = Undertow.builder() | ||||
|                 .addHttpListener(config.getPort(), config.getAddress().getHostAddress()) | ||||
|                 .setHandler(routingHandler) | ||||
|                 .build(); | ||||
|     } | ||||
| 
 | ||||
|     public static Try<LabyrinthServer> createAndStartServer() { | ||||
|         final Try<LabyrinthServer> serverOption = Try.of(ServerConfig::init).mapTry(LabyrinthServer::new); | ||||
|         serverOption.forEach(LabyrinthServer::start); | ||||
|         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() { | ||||
|         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())); | ||||
|                 .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() { | ||||
|  |  | |||
|  | @ -17,18 +17,18 @@ 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", | ||||
|                false, | ||||
|                "t", | ||||
|                "text" | ||||
|             labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), | ||||
|             "txt", | ||||
|             false, | ||||
|             "t", | ||||
|             "text" | ||||
|     ), | ||||
|     HTML("text/html", | ||||
|          labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), | ||||
|          "html", | ||||
|          false, | ||||
|          "h", | ||||
|          "html" | ||||
|             labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), | ||||
|             "html", | ||||
|             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"), | ||||
|  |  | |||
|  | @ -29,7 +29,7 @@ public class ServerConfig { | |||
|     @NonNull | ||||
|     private static InetAddress validateAddress(@Nullable final String 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) { | ||||
|  | @ -53,10 +53,10 @@ public class ServerConfig { | |||
|             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, | ||||
|                           cause | ||||
|                   )); | ||||
|                 .map(ServerConfig::validatePort) | ||||
|                 .getOrElseThrow(cause -> new ConfigurationException( | ||||
|                         "Failed to parse port specified in system property '" + SYSPROP_PORT + "': " + portString, | ||||
|                         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.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.util.Headers; | ||||
| 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.Try; | ||||
| import lombok.NonNull; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.jetbrains.annotations.Nullable; | ||||
| 
 | ||||
| import java.nio.ByteBuffer; | ||||
| import java.util.Deque; | ||||
|  | @ -18,26 +22,69 @@ import java.util.Random; | |||
| 
 | ||||
| @Slf4j | ||||
| public class CreateHandler extends AbstractHttpHandler { | ||||
|     @Override | ||||
|     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"); | ||||
|     public static final String PATH_TEMPLATE = "/create/{output}"; | ||||
| 
 | ||||
|         log.info("Output: {}", output); | ||||
|         log.info("Width:  {}", width); | ||||
|         log.info("Height: {}", height); | ||||
|         log.info("Id:     {}", id); | ||||
|         final Integer theId = id.getOrElse(() -> new Random().nextInt()); | ||||
|         final Labyrinth labyrinth = new Labyrinth(width.get(), height.get(), theId); | ||||
|         final OutputType outputType = output.flatMap(OutputType::ofString).get(); | ||||
|         final byte[] result = outputType.render(labyrinth); | ||||
|         exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, outputType.getContentType()); | ||||
|         exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(theId)); | ||||
|         exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(width.get())); | ||||
|         exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(height.get())); | ||||
|         exchange.getResponseSender().send(ByteBuffer.wrap(result)); | ||||
|     @Override | ||||
|     protected void handle(@NonNull final HttpServerExchange exchange) { | ||||
|         this.createLabyrinthFromRequestParameters(exchange.getQueryParameters()) | ||||
|                 .onFailure(e -> { | ||||
|                     exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); | ||||
|                     exchange.setReasonPhrase(e.getMessage()); | ||||
|                 }).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())); | ||||
|                     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.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"> | ||||
|         <appender-ref ref="STDOUT"/> | ||||
|     </root> | ||||
|     <logger name="ch.fritteli.labyrinth.server.StaticResourcesFileHandler" level="debug"/> | ||||
|     <logger name="ch.fritteli.labyrinth.server.handler.CreateHandler" level="debug"/> | ||||
| </configuration> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue