This commit is contained in:
		
							parent
							
								
									ed0e387dd3
								
							
						
					
					
						commit
						728349b57c
					
				
					 8 changed files with 215 additions and 194 deletions
				
			
		|  | @ -1,5 +0,0 @@ | |||
| FROM openjdk:11 | ||||
| 
 | ||||
| ADD https://repo.gittr.ch/snapshots/ch/fritteli/labyrinth/labyrinth-server/0.0.1-SNAPSHOT/labyrinth-server-0.0.1-SNAPSHOT-jar-with-dependencies.jar /app/app.jar | ||||
| 
 | ||||
| CMD java -Dfritteli.labyrinth.server.host=0.0.0.0 -Dfritteli.labyrinth.server.port=80 -jar /app/app.jar | ||||
|  | @ -24,7 +24,6 @@ import org.jetbrains.annotations.Nullable; | |||
| import java.io.IOException; | ||||
| import java.io.OutputStream; | ||||
| import java.net.InetSocketAddress; | ||||
| import java.net.URISyntaxException; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.util.concurrent.ExecutorService; | ||||
| import java.util.concurrent.SynchronousQueue; | ||||
|  | @ -38,32 +37,166 @@ public class LabyrinthServer { | |||
|     @NonNull | ||||
|     private final HttpServer httpServer; | ||||
|     @NonNull | ||||
|     private final ExecutorService executorService = new ThreadPoolExecutor( | ||||
|             0, | ||||
|             1_000, | ||||
|             5, | ||||
|             TimeUnit.SECONDS, | ||||
|             new SynchronousQueue<>() | ||||
|     private final ExecutorService executorService = new ThreadPoolExecutor(0, | ||||
|                                                                            1_000, | ||||
|                                                                            5, | ||||
|                                                                            TimeUnit.SECONDS, | ||||
|                                                                            new SynchronousQueue<>() | ||||
|     ); | ||||
| 
 | ||||
|     public LabyrinthServer(@NonNull final ServerConfig config) throws IOException, URISyntaxException { | ||||
|     public static Option<LabyrinthServer> createAndStartServer() { | ||||
|         final Option<LabyrinthServer> serverOption = Try.of(ServerConfig::init) | ||||
|                                                         .mapTry(LabyrinthServer::new) | ||||
|                                                         .onFailure(cause -> log.error( | ||||
|                                                                 "Failed to create LabyrinthServer.", | ||||
|                                                                 cause | ||||
|                                                         )) | ||||
|                                                         .toOption(); | ||||
|         serverOption.forEach(LabyrinthServer::start); | ||||
|         return serverOption; | ||||
|     } | ||||
| 
 | ||||
|     public LabyrinthServer(@NonNull final ServerConfig config) throws IOException { | ||||
|         this.httpServer = HttpServer.create(new InetSocketAddress(config.getAddress(), config.getPort()), 5); | ||||
|         this.httpServer.createContext("/", new StaticResourcesFileHandler(this.executorService)); | ||||
|         this.httpServer.createContext("/create", this::handleCreate); | ||||
|         this.httpServer.createContext("/render", this::handleRender); | ||||
|     } | ||||
| 
 | ||||
|     public static Option<LabyrinthServer> createAndStartServer() { | ||||
|         final Option<LabyrinthServer> serverOption = Try.of(ServerConfig::init) | ||||
|                 .mapTry(LabyrinthServer::new) | ||||
|                 .onFailure(cause -> log.error("Failed to create LabyrinthServer.", cause)) | ||||
|                 .toOption(); | ||||
|         serverOption.forEach(LabyrinthServer::start); | ||||
|         return serverOption; | ||||
|     public void start() { | ||||
|         Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); | ||||
|         this.httpServer.start(); | ||||
|         log.info("Listening on http://{}:{}", | ||||
|                  this.httpServer.getAddress().getHostString(), | ||||
|                  this.httpServer.getAddress().getPort() | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     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 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 Labyrinth labyrinth = new Labyrinth(width, height, id); | ||||
|                 final byte[] render; | ||||
|                 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()); | ||||
|                 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() { | ||||
|         log.info("Stopping server ..."); | ||||
|         this.httpServer.stop(5); | ||||
|         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."); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|  | @ -92,126 +225,16 @@ public class LabyrinthServer { | |||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     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 RequestParameter normalizeParameterName(final String paramName) { | ||||
|         return RequestParameter.parseName(paramName).get(); | ||||
|     } | ||||
| 
 | ||||
|     public void start() { | ||||
|         Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); | ||||
|         this.httpServer.start(); | ||||
|         log.info("Listening on http://{}:{}", this.httpServer.getAddress().getHostString(), this.httpServer.getAddress().getPort()); | ||||
|     } | ||||
| 
 | ||||
|     public void stop() { | ||||
|         log.info("Stopping server ..."); | ||||
|         this.httpServer.stop(5); | ||||
|         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."); | ||||
|     } | ||||
| 
 | ||||
|     private void handleCreate(HttpExchange exchange) throws IOException { | ||||
|         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, 5); | ||||
|                 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 Labyrinth labyrinth = new Labyrinth(width, height, id); | ||||
|                 final byte[] render; | ||||
|                 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()); | ||||
|                 if (output.equals(OutputType.BINARY)) { | ||||
|                     responseHeaders.add("Content-disposition", "attachment; filename=\"labyrinth-" + width + "x" + height + "-" + id + ".laby\""); | ||||
|                 } | ||||
|                 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) throws IOException { | ||||
|         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("text/html") ? | ||||
|                         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 { | ||||
|         WIDTH("w", "width"), | ||||
|         HEIGHT("h", "height"), | ||||
|  | @ -227,16 +250,34 @@ public class LabyrinthServer { | |||
|             if (name == null) { | ||||
|                 return Option.none(); | ||||
|             } | ||||
|             return Stream.of(values()) | ||||
|                     .find(param -> param.names.exists(name::equalsIgnoreCase)); | ||||
|             return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     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"), | ||||
|         BINARY("application/octet-stream", SerializerDeserializer::serialize, "b", "binary"); | ||||
|         TEXT_PLAIN("text/plain; charset=UTF-8", | ||||
|                    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" | ||||
|         ), | ||||
|         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"); | ||||
|         @Getter | ||||
|         @NonNull | ||||
|         private final String contentType; | ||||
|  | @ -244,10 +285,21 @@ public class LabyrinthServer { | |||
|         private final List<String> names; | ||||
|         @NonNull | ||||
|         private final Function<Labyrinth, byte[]> render; | ||||
|         @Getter | ||||
|         private final boolean attachment; | ||||
|         @Getter | ||||
|         @NonNull | ||||
|         private final String fileExtension; | ||||
| 
 | ||||
|         OutputType(@NonNull final String contentType, @NonNull final Function<Labyrinth, byte[]> render, @NonNull final String... names) { | ||||
|         OutputType(@NonNull final String contentType, | ||||
|                    @NonNull final Function<Labyrinth, byte[]> render, | ||||
|                    @NonNull final String fileExtension, | ||||
|                    final boolean attachment, | ||||
|                    @NonNull final String... names) { | ||||
|             this.contentType = contentType; | ||||
|             this.render = render; | ||||
|             this.fileExtension = fileExtension; | ||||
|             this.attachment = attachment; | ||||
|             this.names = List.of(names); | ||||
|         } | ||||
| 
 | ||||
|  | @ -256,9 +308,7 @@ public class LabyrinthServer { | |||
|                 return Option.none(); | ||||
|             } | ||||
|             final String nameLC = name.toLowerCase(); | ||||
|             return Stream.of(values()) | ||||
|                     .find(param -> param.names.contains(nameLC)); | ||||
| 
 | ||||
|             return Stream.of(values()).find(param -> param.names.contains(nameLC)); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ public class StaticResourcesFileHandler implements HttpHandler { | |||
| 
 | ||||
|     public StaticResourcesFileHandler(final ExecutorService executorService) { | ||||
|         this.executorService = executorService; | ||||
|         log.debug("Created {}", this.getClass().getSimpleName()); | ||||
|     } | ||||
| 
 | ||||
|     private static void redirect(@NonNull final HttpExchange exchange, @NonNull final String target) throws IOException { | ||||
|  | @ -44,7 +45,9 @@ public class StaticResourcesFileHandler implements HttpHandler { | |||
|             log.debug("Resource '{}' not found in classpath.", path); | ||||
|             return new byte[0]; | ||||
|         } | ||||
|         return IOUtils.toByteArray(stream); | ||||
|         final byte[] response = IOUtils.toByteArray(stream); | ||||
|         log.debug("Sending reply; {} bytes", response.length); | ||||
|         return response; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|  | @ -66,7 +69,7 @@ public class StaticResourcesFileHandler implements HttpHandler { | |||
|                 final String path = requestURI.getPath(); | ||||
|                 log.debug("Handling request to {}", path); | ||||
|                 if ("/".equals(path)) { | ||||
|                     redirect(exchange, "/index.html"); | ||||
|                     redirect(exchange, "index.html"); | ||||
|                     return; | ||||
|                 } | ||||
|                 if (!path.startsWith("/")) { | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <?xml version="1.0" encoding="utf-8" ?> | ||||
| <configuration> | ||||
| 	<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/> | ||||
| 	<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook"/> | ||||
| 
 | ||||
| 	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | ||||
| 		<!-- encoders are by default assigned the type | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ | |||
|                 <option label="HTML Document" value="html"></option> | ||||
|                 <option label="Plain text" value="text"></option> | ||||
|                 <option label="PDF Document" value="pdf"></option> | ||||
|                 <option label="PDF Document (Download)" value="pdffile"></option> | ||||
|                 <option label="Binary" value="binary"></option> | ||||
|             </select> | ||||
|             <label for="id">Seed (optional):</label><input id="id" name="id" type="number"> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue