diff --git a/src/main/java/ch/fritteli/labyrinth/Main.java b/src/main/java/ch/fritteli/labyrinth/Main.java index e62d6d3..b64a270 100644 --- a/src/main/java/ch/fritteli/labyrinth/Main.java +++ b/src/main/java/ch/fritteli/labyrinth/Main.java @@ -1,46 +1,62 @@ package ch.fritteli.labyrinth; import ch.fritteli.labyrinth.model.Labyrinth; +import ch.fritteli.labyrinth.net.TheListener; import ch.fritteli.labyrinth.renderer.html.HTMLRenderer; import ch.fritteli.labyrinth.renderer.htmlfile.HTMLFileRenderer; import ch.fritteli.labyrinth.renderer.pdffile.PDFFileRenderer; import ch.fritteli.labyrinth.renderer.text.TextRenderer; import ch.fritteli.labyrinth.renderer.textfile.TextFileRenderer; +import io.vavr.control.Try; import lombok.NonNull; +import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; public class Main { - public static void main(@NonNull final String[] args) { - int width = 100; - int height = 100; - final Labyrinth labyrinth = new Labyrinth(width, height/*, 0*/); - final TextRenderer textRenderer = TextRenderer.newInstance(); - final HTMLRenderer htmlRenderer = HTMLRenderer.newInstance(); - final Path userHome = Paths.get(System.getProperty("user.home")); - final String baseFilename = getBaseFilename(labyrinth); - final TextFileRenderer textFileRenderer = TextFileRenderer.newInstance() - .setTargetLabyrinthFile(userHome.resolve(baseFilename + ".txt")) - .setTargetSolutionFile(userHome.resolve(baseFilename + "-solution.txt")); - final HTMLFileRenderer htmlFileRenderer = HTMLFileRenderer.newInstance() - .setTargetFile(userHome.resolve(baseFilename + ".html")); - final PDFFileRenderer pdfFileRenderer = PDFFileRenderer.newInstance() - .setTargetFile(userHome.resolve(baseFilename + ".pdf")); + public static void main(@NonNull final String[] args) throws IOException { + final String listenerPort = System.getProperty("fritteli.labyrinth.listenerport"); + if (listenerPort != null) { + final Try portTry = Try.of(() -> Integer.valueOf(listenerPort)); + if (portTry.isFailure()) { + System.err.println("Invalid port specified via sysprop 'fritteli.labyrinth.listenerport': " + listenerPort + ". Not starting webserver."); + } else { + final TheListener listener = new TheListener(portTry.get()); + listener.start(); + System.out.println("Listening on port " + portTry.get()); + } + } else { + System.out.println("In order to start the webserver, specify the port to listen to via the system property 'fritteli.labyrinth.listenerport', i.e.: -Dfritteli.labyrinth.listenerport=12345"); + int width = 100; + int height = 100; + final Labyrinth labyrinth = new Labyrinth(width, height/*, 0*/); + final TextRenderer textRenderer = TextRenderer.newInstance(); + final HTMLRenderer htmlRenderer = HTMLRenderer.newInstance(); + final Path userHome = Paths.get(System.getProperty("user.home")); + final String baseFilename = getBaseFilename(labyrinth); + final TextFileRenderer textFileRenderer = TextFileRenderer.newInstance() + .setTargetLabyrinthFile(userHome.resolve(baseFilename + ".txt")) + .setTargetSolutionFile(userHome.resolve(baseFilename + "-solution.txt")); + final HTMLFileRenderer htmlFileRenderer = HTMLFileRenderer.newInstance() + .setTargetFile(userHome.resolve(baseFilename + ".html")); + final PDFFileRenderer pdfFileRenderer = PDFFileRenderer.newInstance() + .setTargetFile(userHome.resolve(baseFilename + ".pdf")); - System.out.println("Labyrinth-ID: " + labyrinth.getRandomSeed()); - // Render Labyrinth to stdout - System.out.println(textRenderer.render(labyrinth)); - // Render Labyrinth solution to stdout - System.out.println(textRenderer.setRenderSolution(true).render(labyrinth)); - // Render HTML to stdout - System.out.println(htmlRenderer.render(labyrinth)); - // Render Labyrinth and solution to (separate) files - System.out.println(textFileRenderer.render(labyrinth)); - // Render HTML to file - System.out.println(htmlFileRenderer.render(labyrinth)); - // Render PDF to file - System.out.println(pdfFileRenderer.render(labyrinth)); + System.out.println("Labyrinth-ID: " + labyrinth.getRandomSeed()); + // Render Labyrinth to stdout + System.out.println(textRenderer.render(labyrinth)); + // Render Labyrinth solution to stdout + System.out.println(textRenderer.setRenderSolution(true).render(labyrinth)); + // Render HTML to stdout + System.out.println(htmlRenderer.render(labyrinth)); + // Render Labyrinth and solution to (separate) files + System.out.println(textFileRenderer.render(labyrinth)); + // Render HTML to file + System.out.println(htmlFileRenderer.render(labyrinth)); + // Render PDF to file + System.out.println(pdfFileRenderer.render(labyrinth)); + } } private static String getBaseFilename(@NonNull final Labyrinth labyrinth) { diff --git a/src/main/java/ch/fritteli/labyrinth/net/TheListener.java b/src/main/java/ch/fritteli/labyrinth/net/TheListener.java new file mode 100644 index 0000000..d5f61c9 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/net/TheListener.java @@ -0,0 +1,179 @@ +package ch.fritteli.labyrinth.net; + +import ch.fritteli.labyrinth.model.Labyrinth; +import ch.fritteli.labyrinth.renderer.html.HTMLRenderer; +import ch.fritteli.labyrinth.renderer.pdf.PDFRenderer; +import ch.fritteli.labyrinth.renderer.text.TextRenderer; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpServer; +import io.vavr.collection.HashMap; +import io.vavr.collection.HashSet; +import io.vavr.collection.List; +import io.vavr.collection.Map; +import io.vavr.collection.Set; +import io.vavr.collection.Stream; +import io.vavr.control.Option; +import io.vavr.control.Try; +import lombok.Getter; +import lombok.NonNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.function.Function; + +public class TheListener { + + private final HttpServer httpServer; + + public TheListener(final int port) throws IOException { + this.httpServer = HttpServer.create(new InetSocketAddress(port), 0); + this.httpServer.createContext("/create", exchange -> { + final String requestMethod = exchange.getRequestMethod(); + if (!requestMethod.equals("GET")) { + exchange.getResponseBody().close(); + exchange.sendResponseHeaders(405, -1); + exchange.close(); + return; + } + final Map requestParams = this.parseQueryString(exchange.getRequestURI().getQuery()); + final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 1); + final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 1); + final Option seedOption = requestParams.get(RequestParameter.ID).toTry().map(Long::valueOf).toOption(); + final long seed; + final Option outputOption = requestParams.get(RequestParameter.OUTPUT).flatMap(OutputType::ofString); + final OutputType output; + final Headers responseHeaders = exchange.getResponseHeaders(); + boolean needsRedirect = false; + if (seedOption.isEmpty()) { + needsRedirect = true; + seed = System.nanoTime(); + } else { + seed = seedOption.get(); + } + if (outputOption.isEmpty()) { + needsRedirect = true; + output = OutputType.HTML; + } else { + output = outputOption.get(); + } + if (needsRedirect) { + responseHeaders.add("Location", "/create?width=" + width + "&height=" + height + "&output=" + output.toString() + "&id=" + seed); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + return; + } + responseHeaders.add("Content-type", output.getContentType()); + exchange.sendResponseHeaders(200, 0); + final Labyrinth labyrinth = new Labyrinth(width, height, seed); + final byte[] render = output.render(labyrinth); + final OutputStream responseBody = exchange.getResponseBody(); + responseBody.write(render); + responseBody.flush(); + exchange.close(); + }); + } + + private T getOrDefault(@NonNull final Option input, @NonNull final Function mapper, @Nullable final T defaultValue) { + return input.toTry().map(mapper).getOrElse(defaultValue); + } + + @NonNull + private Map parseQueryString(@Nullable final String query) { + if (query == null) { + return HashMap.empty(); + } + HashMap result = HashMap.empty(); + final String[] parts = query.split("&"); + for (final String part : parts) { + final int split = part.indexOf('='); + if (split == -1) { + final Try 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 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(); + } + + public void start() { + this.httpServer.start(); + } + + public void stop() { + this.httpServer.stop(10); + } + + private enum RequestParameter { + WIDTH("w", "width"), + HEIGHT("h", "height"), + ID("i", "id"), + OUTPUT("o", "output"); + private final Set names; + + RequestParameter(@NonNull final String... names) { + this.names = HashSet.of(names); + } + + static Option parseName(@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)); + } + } + + private enum OutputType { + TEXT_PLAIN("text/plain; charset=UTF-8", labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), "t", "text"), + HTML("text/html", labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), "h", "html"), + PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "p", "pdf"); + @Getter + @NonNull + private final String contentType; + @NonNull + private final List names; + @NonNull + private final Function render; + + OutputType(@NonNull final String contentType, @NonNull final Function render, @NonNull final String... names) { + this.contentType = contentType; + this.render = render; + this.names = List.of(names); + } + + static Option 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)); + + } + + @Override + public String toString() { + return this.names.last(); + } + + byte[] render(@NonNull final Labyrinth labyrinth) { + return this.render.apply(labyrinth); + } + } +}