Merge pull request 'feature/undertow' (#4) from feature/undertow into master
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is failing
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	continuous-integration/drone/push Build is failing
				
			Reviewed-on: java/labyrinth-server#4
This commit is contained in:
		
						commit
						7abee9b556
					
				
					 15 changed files with 632 additions and 641 deletions
				
			
		|  | @ -1,12 +1,12 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <settings xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd" | ||||
| 		  xmlns="http://maven.apache.org/SETTINGS/1.1.0" | ||||
| 		  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | ||||
| 	<servers> | ||||
| 		<server> | ||||
| 			<id>repo.gittr.ch</id> | ||||
| 			<username>ci</username> | ||||
| 			<password>${env.REPO_TOKEN}</password> | ||||
| 		</server> | ||||
| 	</servers> | ||||
| <settings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||
|           xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd" | ||||
|           xmlns="http://maven.apache.org/SETTINGS/1.1.0"> | ||||
|     <servers> | ||||
|         <server> | ||||
|             <id>repo.gittr.ch</id> | ||||
|             <username>ci</username> | ||||
|             <password>${env.REPO_TOKEN}</password> | ||||
|         </server> | ||||
|     </servers> | ||||
| </settings> | ||||
|  |  | |||
							
								
								
									
										293
									
								
								pom.xml
									
										
									
									
									
								
							
							
						
						
									
										293
									
								
								pom.xml
									
										
									
									
									
								
							|  | @ -1,126 +1,181 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||
| 	<modelVersion>4.0.0</modelVersion> | ||||
| <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" | ||||
|          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||
|     <modelVersion>4.0.0</modelVersion> | ||||
| 
 | ||||
| 	<parent> | ||||
| 		<groupId>ch.fritteli</groupId> | ||||
| 		<artifactId>fritteli-build-parent</artifactId> | ||||
| 		<version>2.0.4</version> | ||||
| 	</parent> | ||||
|     <parent> | ||||
|         <groupId>ch.fritteli</groupId> | ||||
|         <artifactId>fritteli-build-parent</artifactId> | ||||
|         <version>2.0.4</version> | ||||
|     </parent> | ||||
| 
 | ||||
| 	<groupId>ch.fritteli.labyrinth</groupId> | ||||
| 	<artifactId>labyrinth-server</artifactId> | ||||
| 	<version>0.0.2-SNAPSHOT</version> | ||||
|     <groupId>ch.fritteli.labyrinth</groupId> | ||||
|     <artifactId>labyrinth-server</artifactId> | ||||
|     <version>0.0.2-SNAPSHOT</version> | ||||
|     <description>The Labyrinth server, offering a REST endpoint to access the Labyrinth Generator.</description> | ||||
|     <url>https://manuel.friedli.info/labyrinth.html</url> | ||||
| 
 | ||||
| 	<properties> | ||||
| 		<logback.version>1.4.6</logback.version> | ||||
| 		<lombok.version>1.18.26</lombok.version> | ||||
| 		<slf4j.version>2.0.5</slf4j.version> | ||||
| 		<java.source.version>17</java.source.version> | ||||
| 		<java.target.version>17</java.target.version> | ||||
| 	</properties> | ||||
|     <properties> | ||||
|         <java.source.version>17</java.source.version> | ||||
|         <java.target.version>17</java.target.version> | ||||
|         <jetbrains-annotations.version>24.0.1</jetbrains-annotations.version> | ||||
|         <junit-jupiter.version>5.9.2</junit-jupiter.version> | ||||
|         <labyrinth-generator.version>0.0.4</labyrinth-generator.version> | ||||
|         <logback.version>1.4.6</logback.version> | ||||
|         <lombok.version>1.18.26</lombok.version> | ||||
|         <slf4j.version>2.0.7</slf4j.version> | ||||
|         <undertow.version>2.3.5.Final</undertow.version> | ||||
|         <vavr.version>0.10.4</vavr.version> | ||||
|     </properties> | ||||
| 
 | ||||
| 	<dependencies> | ||||
| 		<dependency> | ||||
| 			<groupId>ch.fritteli.labyrinth</groupId> | ||||
| 			<artifactId>labyrinth-generator</artifactId> | ||||
| 			<version>0.0.2</version> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>io.vavr</groupId> | ||||
| 			<artifactId>vavr</artifactId> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>org.projectlombok</groupId> | ||||
| 			<artifactId>lombok</artifactId> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>org.jetbrains</groupId> | ||||
| 			<artifactId>annotations</artifactId> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>org.slf4j</groupId> | ||||
| 			<artifactId>slf4j-api</artifactId> | ||||
| 			<version>${slf4j.version}</version> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>ch.qos.logback</groupId> | ||||
| 			<artifactId>logback-classic</artifactId> | ||||
| 			<version>${logback.version}</version> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>org.junit.jupiter</groupId> | ||||
| 			<artifactId>junit-jupiter-api</artifactId> | ||||
| 			<scope>test</scope> | ||||
| 		</dependency> | ||||
| 	</dependencies> | ||||
|     <developers> | ||||
|         <developer> | ||||
|             <name>Manuel Friedli</name> | ||||
|             <id>manuel</id> | ||||
|             <url>https://www.fritteli.ch/</url> | ||||
|             <email>manuel@fritteli.ch</email> | ||||
|             <timezone>Europe/Zurich</timezone> | ||||
|             <roles> | ||||
|                 <role>Project Lead</role> | ||||
|                 <role>Software Architect</role> | ||||
|                 <role>Software Engineer</role> | ||||
|                 <role>Operations Manager</role> | ||||
|             </roles> | ||||
|         </developer> | ||||
|     </developers> | ||||
| 
 | ||||
| 	<build> | ||||
| 		<plugins> | ||||
| 			<plugin> | ||||
| 				<groupId>org.apache.maven.plugins</groupId> | ||||
| 				<artifactId>maven-shade-plugin</artifactId> | ||||
| 				<version>3.2.4</version> | ||||
| 				<configuration> | ||||
| 					<transformers> | ||||
| 						<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> | ||||
| 							<mainClass>ch.fritteli.labyrinth.server.Main</mainClass> | ||||
| 						</transformer> | ||||
| 					</transformers> | ||||
| 				</configuration> | ||||
| 				<executions> | ||||
| 					<execution> | ||||
| 						<phase>package</phase> | ||||
| 						<goals> | ||||
| 							<goal>shade</goal> | ||||
| 						</goals> | ||||
| 					</execution> | ||||
| 				</executions> | ||||
| 			</plugin> | ||||
| 		</plugins> | ||||
| 	</build> | ||||
| 	<scm> | ||||
| 		<connection>scm:git:git://gittr.ch/java/labyrinth-server.git</connection> | ||||
| 		<developerConnection>scm:git:ssh://git@gittr.ch/java/labyrinth-server.git</developerConnection> | ||||
| 		<url>https://gittr.ch/java/labyrinth-server</url> | ||||
| 		<tag>v0.0.1</tag> | ||||
| 	</scm> | ||||
| 	<distributionManagement> | ||||
| 		<repository> | ||||
| 			<id>repo.gittr.ch</id> | ||||
| 			<name>gittr.ch</name> | ||||
| 			<url>https://repo.gittr.ch/releases/</url> | ||||
| 		</repository> | ||||
| 		<snapshotRepository> | ||||
| 			<id>repo.gittr.ch</id> | ||||
| 			<name>gittr.ch</name> | ||||
| 			<url>https://repo.gittr.ch/snapshots/</url> | ||||
| 		</snapshotRepository> | ||||
| 	</distributionManagement> | ||||
| 	<repositories> | ||||
| 		<repository> | ||||
| 			<id>repo.gittr.ch.releases</id> | ||||
| 			<url>https://repo.gittr.ch/releases/</url> | ||||
| 			<releases> | ||||
| 				<enabled>true</enabled> | ||||
| 				<updatePolicy>never</updatePolicy> | ||||
| 			</releases> | ||||
| 			<snapshots> | ||||
| 				<enabled>false</enabled> | ||||
| 				<updatePolicy>never</updatePolicy> | ||||
| 			</snapshots> | ||||
| 		</repository> | ||||
| 		<repository> | ||||
| 			<id>repo.gittr.ch.snapshots</id> | ||||
| 			<url>https://repo.gittr.ch/snapshots/</url> | ||||
| 			<releases> | ||||
| 				<enabled>false</enabled> | ||||
| 				<updatePolicy>never</updatePolicy> | ||||
| 			</releases> | ||||
| 			<snapshots> | ||||
| 				<enabled>true</enabled> | ||||
| 				<updatePolicy>always</updatePolicy> | ||||
| 			</snapshots> | ||||
| 		</repository> | ||||
| 	</repositories> | ||||
|     <dependencies> | ||||
|         <dependency> | ||||
|             <groupId>ch.fritteli.labyrinth</groupId> | ||||
|             <artifactId>labyrinth-generator</artifactId> | ||||
|             <version>${labyrinth-generator.version}</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>io.vavr</groupId> | ||||
|             <artifactId>vavr</artifactId> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>org.projectlombok</groupId> | ||||
|             <artifactId>lombok</artifactId> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>org.jetbrains</groupId> | ||||
|             <artifactId>annotations</artifactId> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>org.slf4j</groupId> | ||||
|             <artifactId>slf4j-api</artifactId> | ||||
|             <version>${slf4j.version}</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>ch.qos.logback</groupId> | ||||
|             <artifactId>logback-classic</artifactId> | ||||
|             <version>${logback.version}</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>io.undertow</groupId> | ||||
|             <artifactId>undertow-core</artifactId> | ||||
|             <version>${undertow.version}</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>org.junit.jupiter</groupId> | ||||
|             <artifactId>junit-jupiter-api</artifactId> | ||||
|         </dependency> | ||||
|     </dependencies> | ||||
| 
 | ||||
|     <build> | ||||
|         <plugins> | ||||
|             <plugin> | ||||
|                 <groupId>org.apache.maven.plugins</groupId> | ||||
|                 <artifactId>maven-shade-plugin</artifactId> | ||||
|                 <version>3.4.1</version> | ||||
|                 <configuration> | ||||
|                     <transformers> | ||||
|                         <transformer | ||||
|                                 implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> | ||||
|                             <mainClass>ch.fritteli.labyrinth.server.Main</mainClass> | ||||
|                         </transformer> | ||||
|                     </transformers> | ||||
|                     <filters> | ||||
|                         <filter> | ||||
|                             <artifact>ch.fritteli.labyrinth:labyrinth-generator</artifact> | ||||
|                             <excludes> | ||||
|                                 <exclude>logback.xml</exclude> | ||||
|                             </excludes> | ||||
|                         </filter> | ||||
|                     </filters> | ||||
|                 </configuration> | ||||
|                 <executions> | ||||
|                     <execution> | ||||
|                         <phase>package</phase> | ||||
|                         <goals> | ||||
|                             <goal>shade</goal> | ||||
|                         </goals> | ||||
|                     </execution> | ||||
|                 </executions> | ||||
|             </plugin> | ||||
|             <plugin> | ||||
|                 <groupId>org.apache.maven.plugins</groupId> | ||||
|                 <artifactId>maven-source-plugin</artifactId> | ||||
|                 <executions> | ||||
|                     <execution> | ||||
|                         <id>attach-sources</id> | ||||
|                         <phase>verify</phase> | ||||
|                         <goals> | ||||
|                             <goal>jar-no-fork</goal> | ||||
|                         </goals> | ||||
|                     </execution> | ||||
|                 </executions> | ||||
|             </plugin> | ||||
|             <plugin> | ||||
|                 <groupId>org.apache.maven.plugins</groupId> | ||||
|                 <artifactId>maven-site-plugin</artifactId> | ||||
|                 <version>4.0.0-M6</version> | ||||
|             </plugin> | ||||
|         </plugins> | ||||
|     </build> | ||||
|     <scm> | ||||
|         <connection>scm:git:https://gittr.ch/java/labyrinth-server.git</connection> | ||||
|         <developerConnection>scm:git:ssh://git@gittr.ch/java/labyrinth-server.git</developerConnection> | ||||
|         <url>https://gittr.ch/java/labyrinth-server</url> | ||||
|         <tag>HEAD</tag> | ||||
|     </scm> | ||||
|     <distributionManagement> | ||||
|         <repository> | ||||
|             <id>repo.gittr.ch</id> | ||||
|             <name>gittr.ch</name> | ||||
|             <url>https://repo.gittr.ch/releases/</url> | ||||
|         </repository> | ||||
|         <snapshotRepository> | ||||
|             <id>repo.gittr.ch</id> | ||||
|             <name>gittr.ch</name> | ||||
|             <url>https://repo.gittr.ch/snapshots/</url> | ||||
|         </snapshotRepository> | ||||
|     </distributionManagement> | ||||
|     <repositories> | ||||
|         <repository> | ||||
|             <id>repo.gittr.ch.releases</id> | ||||
|             <url>https://repo.gittr.ch/releases/</url> | ||||
|             <releases> | ||||
|                 <enabled>true</enabled> | ||||
|                 <updatePolicy>never</updatePolicy> | ||||
|             </releases> | ||||
|             <snapshots> | ||||
|                 <enabled>false</enabled> | ||||
|                 <updatePolicy>never</updatePolicy> | ||||
|             </snapshots> | ||||
|         </repository> | ||||
|         <repository> | ||||
|             <id>repo.gittr.ch.snapshots</id> | ||||
|             <url>https://repo.gittr.ch/snapshots/</url> | ||||
|             <releases> | ||||
|                 <enabled>false</enabled> | ||||
|                 <updatePolicy>never</updatePolicy> | ||||
|             </releases> | ||||
|             <snapshots> | ||||
|                 <enabled>true</enabled> | ||||
|                 <updatePolicy>always</updatePolicy> | ||||
|             </snapshots> | ||||
|         </repository> | ||||
|     </repositories> | ||||
| </project> | ||||
|  |  | |||
|  | @ -1,323 +1,59 @@ | |||
| package ch.fritteli.labyrinth.server; | ||||
| 
 | ||||
| import ch.fritteli.labyrinth.generator.model.Labyrinth; | ||||
| import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer; | ||||
| import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer; | ||||
| import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer; | ||||
| import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer; | ||||
| import com.sun.net.httpserver.Headers; | ||||
| import com.sun.net.httpserver.HttpExchange; | ||||
| 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 ch.fritteli.labyrinth.server.handler.CreateHandler; | ||||
| import ch.fritteli.labyrinth.server.handler.RenderHandler; | ||||
| import io.undertow.Undertow; | ||||
| import io.undertow.server.RoutingHandler; | ||||
| import io.vavr.control.Try; | ||||
| import lombok.Getter; | ||||
| import java.net.InetSocketAddress; | ||||
| import lombok.NonNull; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| 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.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; | ||||
| 
 | ||||
| @Slf4j | ||||
| public class LabyrinthServer { | ||||
|     @NonNull | ||||
|     private final HttpServer httpServer; | ||||
|     @NonNull | ||||
|     private final ExecutorService executorService = new ThreadPoolExecutor(0, | ||||
|                                                                            1_000, | ||||
|                                                                            5, | ||||
|                                                                            TimeUnit.SECONDS, | ||||
|                                                                            new SynchronousQueue<>() | ||||
|     ); | ||||
| 
 | ||||
|     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; | ||||
|     @NonNull | ||||
|     private final Undertow undertow; | ||||
| 
 | ||||
|     private LabyrinthServer(@NonNull final ServerConfig config) { | ||||
|         final String hostAddress = config.getAddress().getHostAddress(); | ||||
|         final int port = config.getPort(); | ||||
|         log.info("Starting Server at http://{}:{}/", hostAddress, port); | ||||
|         final RoutingHandler routingHandler = new RoutingHandler() | ||||
|                 .get(CreateHandler.PATH_TEMPLATE, new CreateHandler()) | ||||
|                 .post(RenderHandler.PATH_TEMPLATE, new RenderHandler()); | ||||
| 
 | ||||
|         this.undertow = Undertow.builder() | ||||
|                 .addHttpListener(port, hostAddress) | ||||
|                 .setHandler(routingHandler) | ||||
|                 .build(); | ||||
|     } | ||||
| 
 | ||||
|     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); | ||||
|     @NonNull | ||||
|     public static Try<LabyrinthServer> createAndStartServer() { | ||||
|         return Try.of(ServerConfig::init) | ||||
|                 .flatMapTry(LabyrinthServer::createAndStartServer); | ||||
|     } | ||||
| 
 | ||||
|     public void start() { | ||||
|     @NonNull | ||||
|     public static Try<LabyrinthServer> createAndStartServer(@NonNull final ServerConfig config) { | ||||
|         return Try.of(() -> new LabyrinthServer(config)) | ||||
|                 .peek(LabyrinthServer::start); | ||||
|     } | ||||
| 
 | ||||
|     private 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() | ||||
|         ); | ||||
|         this.undertow.start(); | ||||
|         final InetSocketAddress address = (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress(); | ||||
|         final String hostAddress = address.getAddress().getHostAddress(); | ||||
|         final int port = address.getPort(); | ||||
| 
 | ||||
|         log.info("Listening on http://{}:{}", hostAddress, port); | ||||
|     } | ||||
| 
 | ||||
|     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() { | ||||
|     private 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); | ||||
|         } | ||||
|         this.undertow.stop(); | ||||
|         log.info("Server stopped."); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private Map<RequestParameter, String> parseQueryString(@Nullable final String query) { | ||||
|         if (query == null) { | ||||
|             return HashMap.empty(); | ||||
|         } | ||||
|         HashMap<RequestParameter, String> result = HashMap.empty(); | ||||
|         final String[] parts = query.split("&"); | ||||
|         for (final String part : parts) { | ||||
|             final int split = part.indexOf('='); | ||||
|             if (split == -1) { | ||||
|                 final Try<RequestParameter> 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<RequestParameter> tryKey = Try.of(() -> this.normalizeParameterName(key)); | ||||
|                 if (tryKey.isSuccess()) { | ||||
|                     result = result.put(tryKey.get(), value); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         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(); | ||||
|     } | ||||
| 
 | ||||
|     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 enum OutputType { | ||||
|         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; | ||||
|         @NonNull | ||||
|         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 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); | ||||
|         } | ||||
| 
 | ||||
|         static Option<OutputType> 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,11 +1,14 @@ | |||
| package ch.fritteli.labyrinth.server; | ||||
| 
 | ||||
| import lombok.experimental.UtilityClass; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| 
 | ||||
| @Slf4j | ||||
| @UtilityClass | ||||
| public class Main { | ||||
| 
 | ||||
|     public static void main(String[] args) { | ||||
|         LabyrinthServer.createAndStartServer() | ||||
|                 .onEmpty(() -> log.error("Failed to create server. Stopping.")); | ||||
|                 .onFailure(e -> log.error("Failed to create server. Stopping.", e)); | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										94
									
								
								src/main/java/ch/fritteli/labyrinth/server/OutputType.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/main/java/ch/fritteli/labyrinth/server/OutputType.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,94 @@ | |||
| package ch.fritteli.labyrinth.server; | ||||
| 
 | ||||
| import ch.fritteli.labyrinth.generator.model.Labyrinth; | ||||
| import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer; | ||||
| import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer; | ||||
| import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer; | ||||
| import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer; | ||||
| import io.vavr.collection.List; | ||||
| import io.vavr.collection.Stream; | ||||
| import io.vavr.control.Option; | ||||
| import lombok.Getter; | ||||
| import lombok.NonNull; | ||||
| import org.jetbrains.annotations.Nullable; | ||||
| 
 | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.util.function.Function; | ||||
| 
 | ||||
| public enum OutputType { | ||||
|     TEXT_PLAIN("text/plain; charset=UTF-8", | ||||
|             "txt", | ||||
|             labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), | ||||
|             false, | ||||
|             "t", | ||||
|             "text"), | ||||
|     HTML("text/html", | ||||
|             "html", | ||||
|             labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), | ||||
|             false, | ||||
|             "h", | ||||
|             "html"), | ||||
|     PDF("application/pdf", | ||||
|             "pdf", | ||||
|             labyrinth -> PDFRenderer.newInstance().render(labyrinth).toByteArray(), | ||||
|             false, | ||||
|             "p", | ||||
|             "pdf"), | ||||
|     PDFFILE("application/pdf", | ||||
|             "pdf", | ||||
|             labyrinth -> PDFRenderer.newInstance().render(labyrinth).toByteArray(), | ||||
|             true, | ||||
|             "f", | ||||
|             "pdffile"), | ||||
|     BINARY("application/octet-stream", | ||||
|             "laby", | ||||
|             SerializerDeserializer::serialize, | ||||
|             true, | ||||
|             "b", | ||||
|             "binary"); | ||||
|     @Getter | ||||
|     @NonNull | ||||
|     private final String contentType; | ||||
|     @Getter | ||||
|     @NonNull | ||||
|     private final String fileExtension; | ||||
|     @NonNull | ||||
|     private final Function<Labyrinth, byte[]> render; | ||||
|     @Getter | ||||
|     private final boolean attachment; | ||||
|     @Getter | ||||
|     @NonNull | ||||
|     private final List<String> names; | ||||
| 
 | ||||
|     OutputType(@NonNull final String contentType, | ||||
|                @NonNull final String fileExtension, | ||||
|                @NonNull final Function<Labyrinth, byte[]> render, | ||||
|                final boolean attachment, | ||||
|                @NonNull final String... names) { | ||||
|         this.contentType = contentType; | ||||
|         this.render = render; | ||||
|         this.fileExtension = fileExtension; | ||||
|         this.attachment = attachment; | ||||
|         this.names = List.of(names); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static Option<OutputType> ofString(@Nullable final String name) { | ||||
|         return Option.of(name) | ||||
|                 .map(String::toLowerCase) | ||||
|                 .flatMap(nameLC -> Stream.of(values()) | ||||
|                         .find(param -> param.names.contains(nameLC))); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public String toString() { | ||||
|         return this.names.last(); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public byte[] render(@NonNull final Labyrinth labyrinth) { | ||||
|         return this.render.apply(labyrinth); | ||||
|     } | ||||
| } | ||||
|  | @ -10,7 +10,6 @@ import org.jetbrains.annotations.Nullable; | |||
| 
 | ||||
| import java.net.InetAddress; | ||||
| 
 | ||||
| 
 | ||||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||||
| @Slf4j | ||||
| @Value | ||||
|  | @ -39,7 +38,17 @@ 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: %s".formatted(address), | ||||
|                         cause | ||||
|                 )); | ||||
|     } | ||||
| 
 | ||||
|     private static int validatePort(final int port) { | ||||
|         if (port < 0 || port > 0xFFFF) { | ||||
|             throw new ConfigurationException("Port out of range (0..65535): %s".formatted(port)); | ||||
|         } | ||||
|         return port; | ||||
|     } | ||||
| 
 | ||||
|     private static int validatePort(@Nullable final String portString) { | ||||
|  | @ -48,14 +57,9 @@ 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)); | ||||
|     } | ||||
| 
 | ||||
|     private static int validatePort(final int port) { | ||||
|         if (port < 0 || port > 0xFFFF) { | ||||
|             throw new ConfigurationException("Port out of range (0..65535): " + port); | ||||
|         } | ||||
|         return port; | ||||
|                 .getOrElseThrow(cause -> new ConfigurationException( | ||||
|                         "Failed to parse port specified in system property '%s': %s".formatted(SYSPROP_PORT, portString), | ||||
|                         cause | ||||
|                 )); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,101 +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()); | ||||
|     } | ||||
| 
 | ||||
|     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(); | ||||
|     } | ||||
| 
 | ||||
|     @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; | ||||
|     } | ||||
| 
 | ||||
|     @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; | ||||
|     } | ||||
| 
 | ||||
|     @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(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,39 @@ | |||
| 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; | ||||
| import org.slf4j.MDC; | ||||
| 
 | ||||
| import java.time.Instant; | ||||
| import java.time.temporal.ChronoUnit; | ||||
| import java.util.UUID; | ||||
| 
 | ||||
| @Slf4j | ||||
| public abstract class AbstractHttpHandler implements HttpHandler { | ||||
|     @Override | ||||
|     public final void handleRequest(@NonNull final HttpServerExchange exchange) { | ||||
|         final Instant start = Instant.now(); | ||||
|         try (final MDC.MDCCloseable closeable = MDC.putCloseable("correlationId", UUID.randomUUID().toString())) { | ||||
| 
 | ||||
|             if (exchange.isInIoThread()) { | ||||
|                 log.debug("Dispatching request"); | ||||
|                 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); | ||||
|             } | ||||
|             log.debug("Completed request in {}ms.", start.until(Instant.now(), ChronoUnit.MILLIS)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     protected abstract void handle(@NonNull final HttpServerExchange exchange) throws Exception; | ||||
| } | ||||
|  | @ -0,0 +1,76 @@ | |||
| package ch.fritteli.labyrinth.server.handler; | ||||
| 
 | ||||
| import ch.fritteli.labyrinth.generator.model.Labyrinth; | ||||
| import ch.fritteli.labyrinth.server.OutputType; | ||||
| import io.undertow.server.HttpServerExchange; | ||||
| import io.undertow.util.Headers; | ||||
| import io.undertow.util.HttpString; | ||||
| import io.undertow.util.StatusCodes; | ||||
| import io.vavr.Tuple2; | ||||
| import io.vavr.control.Try; | ||||
| import java.nio.ByteBuffer; | ||||
| import java.time.Instant; | ||||
| import java.time.temporal.ChronoUnit; | ||||
| import java.util.Deque; | ||||
| import java.util.Map; | ||||
| import lombok.NonNull; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.slf4j.MDC; | ||||
| 
 | ||||
| @Slf4j | ||||
| public class CreateHandler extends AbstractHttpHandler { | ||||
| 
 | ||||
|     public static final String PATH_TEMPLATE = "/create/{output}"; | ||||
| 
 | ||||
|     @Override | ||||
|     protected void handle(@NonNull final HttpServerExchange exchange) { | ||||
|         final Instant start = Instant.now(); | ||||
|         log.debug("Handling create request"); | ||||
|         this.createLabyrinthFromRequestParameters(exchange.getQueryParameters()) | ||||
|                 .onFailure(e -> { | ||||
|                     log.error("Error creating Labyrinth from request", e); | ||||
|                     exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) | ||||
|                             .setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING) | ||||
|                             .getResponseSender() | ||||
|                             .send(e.getMessage()); | ||||
|                 }) | ||||
|                 .forEach(tuple -> { | ||||
|                     final OutputType outputType = tuple._1(); | ||||
|                     final Labyrinth labyrinth = tuple._2(); | ||||
|                     final byte[] bytes; | ||||
|                     try { | ||||
|                         bytes = outputType.render(labyrinth); | ||||
|                     } catch (@NonNull final Exception e) { | ||||
|                         log.error("Error rendering Labyrinth", e); | ||||
|                         exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) | ||||
|                                 .setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING) | ||||
|                                 .getResponseSender() | ||||
|                                 .send("Error creating the Labyrinth. Please contact the administrator. Request id=%s".formatted(MDC.get("correlationId"))); | ||||
|                         return; | ||||
|                     } | ||||
|                     final long durationMillis = start.until(Instant.now(), ChronoUnit.MILLIS); | ||||
|                     exchange.getResponseHeaders() | ||||
|                             .put(Headers.CONTENT_TYPE, outputType.getContentType()) | ||||
|                             .put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(labyrinth.getRandomSeed())) | ||||
|                             .put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth())) | ||||
|                             .put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(labyrinth.getHeight())) | ||||
|                             .put(HttpString.tryFromString("X-Labyrinth-Generation-Duration-millis"), String.valueOf(durationMillis)); | ||||
|                     if (outputType.isAttachment()) { | ||||
|                         exchange.getResponseHeaders() | ||||
|                                 .put(Headers.CONTENT_DISPOSITION, "attachment; filename=\"labyrinth-%dx%d-%d.%s\"".formatted( | ||||
|                                         labyrinth.getWidth(), | ||||
|                                         labyrinth.getHeight(), | ||||
|                                         labyrinth.getRandomSeed(), | ||||
|                                         outputType.getFileExtension() | ||||
|                                 )); | ||||
|                     } | ||||
|                     exchange.getResponseSender().send(ByteBuffer.wrap(bytes)); | ||||
|                     log.debug("Create request handled in {}ms.", durationMillis); | ||||
|                 }); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private Try<Tuple2<OutputType, Labyrinth>> createLabyrinthFromRequestParameters(final Map<String, Deque<String>> queryParameters) { | ||||
|         return new ParametersToLabyrinthExtractor(queryParameters).createLabyrinth(); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,55 @@ | |||
| package ch.fritteli.labyrinth.server.handler; | ||||
| 
 | ||||
| import ch.fritteli.labyrinth.generator.model.Labyrinth; | ||||
| import ch.fritteli.labyrinth.server.OutputType; | ||||
| import io.vavr.Tuple; | ||||
| import io.vavr.Tuple2; | ||||
| import io.vavr.collection.Stream; | ||||
| import io.vavr.control.Option; | ||||
| import io.vavr.control.Try; | ||||
| import java.util.Deque; | ||||
| import java.util.Map; | ||||
| import java.util.Random; | ||||
| import lombok.NonNull; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| 
 | ||||
| @RequiredArgsConstructor | ||||
| class ParametersToLabyrinthExtractor { | ||||
| 
 | ||||
|     @NonNull | ||||
|     private final Map<String, Deque<String>> queryParameters; | ||||
| 
 | ||||
|     @NonNull | ||||
|     Try<Tuple2<OutputType, Labyrinth>> createLabyrinth() { | ||||
|         final Option<OutputType> output = getParameterValue(RequestParameter.OUTPUT); | ||||
|         final Option<Integer> width = getParameterValue(RequestParameter.WIDTH); | ||||
|         final Option<Integer> height = getParameterValue(RequestParameter.HEIGHT); | ||||
|         final Option<Long> id = getParameterValue(RequestParameter.ID); | ||||
| 
 | ||||
|         if (output.isEmpty()) { | ||||
|             return Try.failure(new IllegalArgumentException("Path parameter %s is required and must be one of: %s".formatted( | ||||
|                     RequestParameter.OUTPUT.getNames().mkString("'", " / ", "'"), | ||||
|                     Stream.of(OutputType.values()) | ||||
|                             .flatMap(OutputType::getNames) | ||||
|                             .mkString(", ") | ||||
|             ))); | ||||
|         } | ||||
|         if (width.isEmpty()) { | ||||
|             return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( | ||||
|                     RequestParameter.WIDTH.getNames().mkString("'", " / ", "'") | ||||
|             ))); | ||||
|         } | ||||
|         if (height.isEmpty()) { | ||||
|             return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( | ||||
|                     RequestParameter.HEIGHT.getNames().mkString("'", " / ", "'") | ||||
|             ))); | ||||
|         } | ||||
| 
 | ||||
|         return Try.of(() -> Tuple.of(output.get(), new Labyrinth(width.get(), height.get(), id.getOrElse(() -> new Random().nextLong())))); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private <T> Option<T> getParameterValue(@NonNull final RequestParameter parameter) { | ||||
|         return parameter.getParameterValue(this.queryParameters); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,60 @@ | |||
| package ch.fritteli.labyrinth.server.handler; | ||||
| 
 | ||||
| import ch.fritteli.labyrinth.generator.model.Labyrinth; | ||||
| import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer; | ||||
| import ch.fritteli.labyrinth.server.OutputType; | ||||
| import io.undertow.server.HttpServerExchange; | ||||
| import io.undertow.util.HeaderValues; | ||||
| import io.undertow.util.Headers; | ||||
| import io.undertow.util.StatusCodes; | ||||
| import java.nio.ByteBuffer; | ||||
| import lombok.NonNull; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| 
 | ||||
| @Slf4j | ||||
| public class RenderHandler extends AbstractHttpHandler { | ||||
| 
 | ||||
|     public static final String PATH_TEMPLATE = "/render/{output}"; | ||||
| 
 | ||||
|     @Override | ||||
|     public void handle(final HttpServerExchange exchange) { | ||||
|         log.debug("Handling render request"); | ||||
| 
 | ||||
|         if (exchange.isInIoThread()) { | ||||
|             exchange.dispatch(this); | ||||
|             return; | ||||
|         } | ||||
|         exchange.getRequestReceiver().receiveFullBytes((httpServerExchange, bytes) -> { | ||||
|             final OutputType output = this.getOutputType(httpServerExchange); | ||||
|             final byte[] render; | ||||
|             try { | ||||
|                 final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); | ||||
|                 render = output.render(labyrinth); | ||||
|             } catch (final Exception e) { | ||||
|                 log.error("Error rendering binary labyrinth data", e); | ||||
|                 httpServerExchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) | ||||
|                         .getResponseSender() | ||||
|                         .send("Error rendering labyrinth: %s".formatted(e.getMessage())); | ||||
|                 return; | ||||
|             } | ||||
|             httpServerExchange | ||||
|                     .setStatusCode(StatusCodes.OK) | ||||
|                     .getResponseHeaders() | ||||
|                     .put(Headers.CONTENT_TYPE, output.getContentType()); | ||||
|             httpServerExchange.getResponseSender() | ||||
|                     .send(ByteBuffer.wrap(render)); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private OutputType getOutputType(@NonNull final HttpServerExchange httpServerExchange) { | ||||
|         return RequestParameter.OUTPUT.<OutputType>getParameterValue(httpServerExchange.getQueryParameters()) | ||||
|                 .getOrElse(() -> { | ||||
|                     final HeaderValues accept = httpServerExchange.getRequestHeaders().get(Headers.ACCEPT); | ||||
|                     if (accept.contains(OutputType.HTML.getContentType())) { | ||||
|                         return OutputType.HTML; | ||||
|                     } | ||||
|                     return OutputType.TEXT_PLAIN; | ||||
|                 }); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,54 @@ | |||
| package ch.fritteli.labyrinth.server.handler; | ||||
| 
 | ||||
| import ch.fritteli.labyrinth.server.OutputType; | ||||
| import io.vavr.Tuple2; | ||||
| import io.vavr.collection.HashMap; | ||||
| import io.vavr.collection.HashSet; | ||||
| import io.vavr.collection.Set; | ||||
| import io.vavr.control.Option; | ||||
| import io.vavr.control.Try; | ||||
| import java.util.Deque; | ||||
| import java.util.Map; | ||||
| import java.util.function.Function; | ||||
| import lombok.Getter; | ||||
| import lombok.NonNull; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| 
 | ||||
| @Slf4j | ||||
| enum RequestParameter { | ||||
|     WIDTH(p -> Try.of(() -> Integer.parseInt(p)) | ||||
|             .toOption() | ||||
|             .onEmpty(() -> log.debug("Unparseable value for parameter 'width': '{}'", p)), "w", "width"), | ||||
|     HEIGHT(p -> Try.of(() -> Integer.parseInt(p)) | ||||
|             .toOption() | ||||
|             .onEmpty(() -> log.debug("Unparseable value for parameter 'height': '{}'", p)), "h", "height"), | ||||
|     ID(p -> Try.of(() -> Long.parseLong(p)) | ||||
|             .toOption() | ||||
|             .onEmpty(() -> log.debug("Unparseable value for parameter 'id': '{}'", p)), "i", "id"), | ||||
|     OUTPUT(p -> OutputType.ofString(p) | ||||
|             .onEmpty(() -> log.debug("Unparseable value for parameter 'output': '{}'", p)), "o", "output"); | ||||
|     @NonNull | ||||
|     private final Function<String, Option<?>> extractor; | ||||
|     @Getter | ||||
|     @NonNull | ||||
|     private final Set<String> names; | ||||
| 
 | ||||
|     RequestParameter(@NonNull final Function<String, Option<?>> extractor, @NonNull final String... names) { | ||||
|         this.extractor = extractor; | ||||
|         this.names = HashSet.of(names); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     Option<?> extractParameterValue(@NonNull final String parameter) { | ||||
|         return this.extractor.apply(parameter); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public <T> Option<T> getParameterValue(@NonNull final Map<String, Deque<String>> queryParameters) { | ||||
|         return (Option<T>) HashMap.ofAll(queryParameters) | ||||
|                 .filterKeys(this.names::contains) | ||||
|                 .flatMap(Tuple2::_2) | ||||
|                 .flatMap(this::extractParameterValue) | ||||
|                 .headOption(); | ||||
|     } | ||||
| } | ||||
|  | @ -1,17 +1,17 @@ | |||
| <?xml version="1.0" encoding="utf-8" ?> | ||||
| <configuration> | ||||
| 	<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook"/> | ||||
|     <shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook"/> | ||||
| 
 | ||||
| 	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | ||||
| 		<!-- encoders are by default assigned the type | ||||
| 		ch.qos.logback.classic.encoder.PatternLayoutEncoder --> | ||||
| 		<encoder> | ||||
| 			<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | ||||
| 		</encoder> | ||||
| 	</appender> | ||||
|     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | ||||
|         <!-- encoders are by default assigned the type | ||||
|         ch.qos.logback.classic.encoder.PatternLayoutEncoder --> | ||||
|         <encoder> | ||||
|             <pattern>%d{HH:mm:ss.SSS} %-5level %X{correlationId} [%thread] %logger{36} - %msg%n</pattern> | ||||
|         </encoder> | ||||
|     </appender> | ||||
| 
 | ||||
| 	<root level="info"> | ||||
| 		<appender-ref ref="STDOUT"/> | ||||
| 	</root> | ||||
| 	<logger name="ch.fritteli.labyrinth.server.StaticResourcesFileHandler" level="debug"/> | ||||
|     <root level="info"> | ||||
|         <appender-ref ref="STDOUT"/> | ||||
|     </root> | ||||
|     <logger name="ch.fritteli.labyrinth.*" level="debug"/> | ||||
| </configuration> | ||||
|  |  | |||
|  | @ -1,33 +0,0 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <title>Labyrinth Generator</title> | ||||
|     <link rel="stylesheet" href="style.css"> | ||||
| </head> | ||||
| <body> | ||||
| <div class="content"> | ||||
|     <h1>Labyrinth Generator</h1> | ||||
|     <p>Enter some values, click the "Create!" button and see what happens!</p> | ||||
|     <form action="/create" method="get"> | ||||
|         <div class="inputs-wrapper"> | ||||
|             <label for="width">Width:</label><input id="width" name="width" type="number" min="1" required> | ||||
|             <label for="height">Height:</label><input id="height" name="height" type="number" min="1" required> | ||||
|             <label for="output">Output format:</label> | ||||
|             <select id="output" name="output" required> | ||||
|                 <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"> | ||||
|         </div> | ||||
|         <div class="controls-wrapper"> | ||||
|             <button type="submit" class="primary">Create!</button> | ||||
|             <button type="reset">Reset form</button> | ||||
|         </div> | ||||
|     </form> | ||||
| </div> | ||||
| </body> | ||||
| </html> | ||||
|  | @ -1,51 +0,0 @@ | |||
| :root { | ||||
|     --color-background: #181a1b; | ||||
|     --color-foreground: #e8e6e3; | ||||
|     --color-border: #5d6164; | ||||
|     --color-background-highlight: #292b2c; | ||||
|     --color-foreground-highlight: #f8f7f4; | ||||
|     --color-border-highlight: #6e7275; | ||||
| } | ||||
| 
 | ||||
| body { | ||||
|     display: flex; | ||||
|     font-family: sans-serif; | ||||
|     justify-content: center; | ||||
| } | ||||
| 
 | ||||
| body, button, input, select { | ||||
|     background-color: var(--color-background); | ||||
|     color: var(--color-foreground); | ||||
| } | ||||
| 
 | ||||
| button, input, select { | ||||
|     border: 1px solid var(--color-border); | ||||
| } | ||||
| 
 | ||||
| button:active, input:active, select:active, | ||||
| button:focus, input:focus, select:focus, | ||||
| button:hover, input:hover, select:hover { | ||||
|     background-color: var(--color-background-highlight); | ||||
|     border-color: var(--color-border-highlight); | ||||
|     color: var(--color-foreground-highlight); | ||||
| } | ||||
| 
 | ||||
| button.primary { | ||||
|     background-color: #ffcc00; | ||||
|     border-color: #eebb00; | ||||
|     color: var(--color-background); | ||||
| } | ||||
| 
 | ||||
| .content { | ||||
|     width: 75%; | ||||
| } | ||||
| 
 | ||||
| .content h1 { | ||||
|     text-align: center; | ||||
| } | ||||
| 
 | ||||
| .inputs-wrapper { | ||||
|     display: grid; | ||||
|     grid-row-gap: 1em; | ||||
|     grid-template-columns: 0.5fr 1fr; | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue