This commit is contained in:
		
							parent
							
								
									41995979e1
								
							
						
					
					
						commit
						5ba689841f
					
				
					 7 changed files with 215 additions and 64 deletions
				
			
		|  | @ -13,12 +13,18 @@ | ||||||
|     <orderEntry type="inheritedJdk" /> |     <orderEntry type="inheritedJdk" /> | ||||||
|     <orderEntry type="sourceFolder" forTests="false" /> |     <orderEntry type="sourceFolder" forTests="false" /> | ||||||
|     <orderEntry type="library" name="Maven: ch.fritteli.labyrinth:labyrinth-generator:0.0.1" level="project" /> |     <orderEntry type="library" name="Maven: ch.fritteli.labyrinth:labyrinth-generator:0.0.1" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: org.jetbrains:annotations:19.0.0" level="project" /> |  | ||||||
|     <orderEntry type="library" name="Maven: org.apache.pdfbox:pdfbox:2.0.20" level="project" /> |     <orderEntry type="library" name="Maven: org.apache.pdfbox:pdfbox:2.0.20" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: org.apache.pdfbox:fontbox:2.0.20" level="project" /> |     <orderEntry type="library" name="Maven: org.apache.pdfbox:fontbox:2.0.20" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" /> |     <orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: io.vavr:vavr:0.10.2" level="project" /> |     <orderEntry type="library" name="Maven: io.vavr:vavr:0.10.2" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: io.vavr:vavr-match:0.10.2" level="project" /> |     <orderEntry type="library" name="Maven: io.vavr:vavr-match:0.10.2" level="project" /> | ||||||
|     <orderEntry type="library" scope="PROVIDED" name="Maven: org.projectlombok:lombok:1.18.12" level="project" /> |     <orderEntry type="library" scope="PROVIDED" name="Maven: org.projectlombok:lombok:1.18.12" level="project" /> | ||||||
|  |     <orderEntry type="library" name="Maven: org.jetbrains:annotations:19.0.0" level="project" /> | ||||||
|  |     <orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.30" level="project" /> | ||||||
|  |     <orderEntry type="library" name="Maven: org.slf4j:slf4j-simple:1.7.30" level="project" /> | ||||||
|  |     <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.6.1" level="project" /> | ||||||
|  |     <orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.0" level="project" /> | ||||||
|  |     <orderEntry type="library" scope="TEST" name="Maven: org.opentest4j:opentest4j:1.2.0" level="project" /> | ||||||
|  |     <orderEntry type="library" scope="TEST" name="Maven: org.junit.platform:junit-platform-commons:1.6.1" level="project" /> | ||||||
|   </component> |   </component> | ||||||
| </module> | </module> | ||||||
							
								
								
									
										19
									
								
								pom.xml
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								pom.xml
									
										
									
									
									
								
							|  | @ -28,6 +28,25 @@ | ||||||
| 			<groupId>org.projectlombok</groupId> | 			<groupId>org.projectlombok</groupId> | ||||||
| 			<artifactId>lombok</artifactId> | 			<artifactId>lombok</artifactId> | ||||||
| 		</dependency> | 		</dependency> | ||||||
|  | 		<dependency> | ||||||
|  | 			<groupId>org.jetbrains</groupId> | ||||||
|  | 			<artifactId>annotations</artifactId> | ||||||
|  | 		</dependency> | ||||||
|  | 		<dependency> | ||||||
|  | 			<groupId>org.slf4j</groupId> | ||||||
|  | 			<artifactId>slf4j-api</artifactId> | ||||||
|  | 			<version>1.7.30</version> | ||||||
|  | 		</dependency> | ||||||
|  | 		<dependency> | ||||||
|  | 			<groupId>org.slf4j</groupId> | ||||||
|  | 			<artifactId>slf4j-simple</artifactId> | ||||||
|  | 			<version>1.7.30</version> | ||||||
|  | 		</dependency> | ||||||
|  | 		<dependency> | ||||||
|  | 			<groupId>org.junit.jupiter</groupId> | ||||||
|  | 			<artifactId>junit-jupiter-api</artifactId> | ||||||
|  | 			<scope>test</scope> | ||||||
|  | 		</dependency> | ||||||
| 	</dependencies> | 	</dependencies> | ||||||
| 
 | 
 | ||||||
| 	<build> | 	<build> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,13 @@ | ||||||
|  | package ch.fritteli.labyrinth.server; | ||||||
|  | 
 | ||||||
|  | import org.jetbrains.annotations.Nullable; | ||||||
|  | 
 | ||||||
|  | public class ConfigurationException extends RuntimeException { | ||||||
|  |     public ConfigurationException(@Nullable final String message) { | ||||||
|  |         super(message); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public ConfigurationException(@Nullable final String message, @Nullable final Throwable cause) { | ||||||
|  |         super(message, cause); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -5,6 +5,7 @@ import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer; | ||||||
| import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer; | import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer; | ||||||
| import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer; | import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer; | ||||||
| import com.sun.net.httpserver.Headers; | import com.sun.net.httpserver.Headers; | ||||||
|  | import com.sun.net.httpserver.HttpExchange; | ||||||
| import com.sun.net.httpserver.HttpServer; | import com.sun.net.httpserver.HttpServer; | ||||||
| import io.vavr.collection.HashMap; | import io.vavr.collection.HashMap; | ||||||
| import io.vavr.collection.HashSet; | import io.vavr.collection.HashSet; | ||||||
|  | @ -16,79 +17,32 @@ import io.vavr.control.Option; | ||||||
| import io.vavr.control.Try; | import io.vavr.control.Try; | ||||||
| import lombok.Getter; | import lombok.Getter; | ||||||
| import lombok.NonNull; | import lombok.NonNull; | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
| import org.jetbrains.annotations.Nullable; | import org.jetbrains.annotations.Nullable; | ||||||
| 
 | 
 | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.OutputStream; | import java.io.OutputStream; | ||||||
| import java.net.InetSocketAddress; | import java.net.InetSocketAddress; | ||||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||||
|  | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
| import java.util.function.Function; | import java.util.function.Function; | ||||||
| 
 | 
 | ||||||
|  | @Slf4j | ||||||
| public class LabyrinthServer { | public class LabyrinthServer { | ||||||
| 
 |  | ||||||
|     private final HttpServer httpServer; |     private final HttpServer httpServer; | ||||||
| 
 | 
 | ||||||
|     public LabyrinthServer(final int port) throws IOException { |     public LabyrinthServer(@NonNull final ServerConfig config) throws IOException { | ||||||
|         this.httpServer = HttpServer.create(new InetSocketAddress(port), 0); |         this.httpServer = HttpServer.create(new InetSocketAddress(config.getAddress(), config.getPort()), 5); | ||||||
|         this.httpServer.createContext("/create", exchange -> { |         this.httpServer.createContext("/create", this::handleCreate); | ||||||
|             final String requestMethod = exchange.getRequestMethod(); |  | ||||||
|             if (!requestMethod.equals("GET")) { |  | ||||||
|                 exchange.getResponseBody().close(); |  | ||||||
|                 exchange.sendResponseHeaders(405, -1); |  | ||||||
|                 exchange.close(); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|             final Map<RequestParameter, String> requestParams = this.parseQueryString(exchange.getRequestURI().getQuery()); |  | ||||||
|             final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 1); |  | ||||||
|             final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 1); |  | ||||||
|             final Option<Long> seedOption = requestParams.get(RequestParameter.ID).toTry().map(Long::valueOf).toOption(); |  | ||||||
|             final long seed; |  | ||||||
|             final Option<OutputType> outputOption = requestParams.get(RequestParameter.OUTPUT).flatMap(OutputType::ofString); |  | ||||||
|             final OutputType output; |  | ||||||
|             final Headers responseHeaders = exchange.getResponseHeaders(); |  | ||||||
|             boolean needsRedirect = false; |  | ||||||
|             if (seedOption.isEmpty()) { |  | ||||||
|                 needsRedirect = true; |  | ||||||
|                 seed = System.nanoTime(); |  | ||||||
|             } else { |  | ||||||
|                 seed = seedOption.get(); |  | ||||||
|             } |  | ||||||
|             if (outputOption.isEmpty()) { |  | ||||||
|                 needsRedirect = true; |  | ||||||
|                 output = OutputType.HTML; |  | ||||||
|             } else { |  | ||||||
|                 output = outputOption.get(); |  | ||||||
|             } |  | ||||||
|             if (needsRedirect) { |  | ||||||
|                 responseHeaders.add("Location", "/create?width=" + width + "&height=" + height + "&output=" + output.toString() + "&id=" + seed); |  | ||||||
|                 exchange.sendResponseHeaders(302, -1); |  | ||||||
|                 exchange.close(); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|             responseHeaders.add("Content-type", output.getContentType()); |  | ||||||
|             exchange.sendResponseHeaders(200, 0); |  | ||||||
|             final Labyrinth labyrinth = new Labyrinth(width, height, seed); |  | ||||||
|             final byte[] render = output.render(labyrinth); |  | ||||||
|             final OutputStream responseBody = exchange.getResponseBody(); |  | ||||||
|             responseBody.write(render); |  | ||||||
|             responseBody.flush(); |  | ||||||
|             exchange.close(); |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static Option<LabyrinthServer> createListener() { |     public static Option<LabyrinthServer> createAndStartServer() { | ||||||
|         final String listenerPort = System.getProperty("fritteli.labyrinth.listenerport"); |         final Option<LabyrinthServer> serverOption = Try.of(ServerConfig::init) | ||||||
|         final Option<LabyrinthServer> listenerOption = Option.of(listenerPort) |  | ||||||
|                 .toTry() |  | ||||||
|                 .map(Integer::valueOf) |  | ||||||
|                 .onFailure(cause -> System.err.println("Invalid port specified via system property 'fritteli.labyrinth.listenerport': " |  | ||||||
|                         + listenerPort |  | ||||||
|                         + ". Not starting webserver.")) |  | ||||||
|                 .mapTry(LabyrinthServer::new) |                 .mapTry(LabyrinthServer::new) | ||||||
|                 .onFailure(cause -> System.err.println("Failed to create Listener: " + cause)) |                 .onFailure(cause -> log.error("Failed to create LabyrinthServer.", cause)) | ||||||
|                 .toOption(); |                 .toOption(); | ||||||
|         listenerOption.forEach(LabyrinthServer::start); |         serverOption.forEach(LabyrinthServer::start); | ||||||
|         return listenerOption; |         return serverOption; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private <T> T getOrDefault(@NonNull final Option<String> input, @NonNull final Function<String, T> mapper, @Nullable final T defaultValue) { |     private <T> T getOrDefault(@NonNull final Option<String> input, @NonNull final Function<String, T> mapper, @Nullable final T defaultValue) { | ||||||
|  | @ -128,13 +82,47 @@ public class LabyrinthServer { | ||||||
|     public void start() { |     public void start() { | ||||||
|         Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); |         Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); | ||||||
|         this.httpServer.start(); |         this.httpServer.start(); | ||||||
|         System.out.println("Listening on " + this.httpServer.getAddress()); |         log.info("Listening on {}", this.httpServer.getAddress()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void stop() { |     public void stop() { | ||||||
|         System.out.println("Stopping listener ..."); |         log.info("Stopping server ..."); | ||||||
|         this.httpServer.stop(5); |         this.httpServer.stop(5); | ||||||
|         System.out.println("Listener stopped."); |         log.info("Server stopped."); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void handleCreate(HttpExchange exchange) throws IOException { | ||||||
|  |         log.debug("Handling request to {}", exchange.getRequestURI()); | ||||||
|  |         final String requestMethod = exchange.getRequestMethod(); | ||||||
|  |         if (!requestMethod.equals("GET")) { | ||||||
|  |             exchange.getResponseBody().close(); | ||||||
|  |             exchange.sendResponseHeaders(405, -1); | ||||||
|  |             exchange.close(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         final Map<RequestParameter, String> requestParams = this.parseQueryString(exchange.getRequestURI().getQuery()); | ||||||
|  |         final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 1); | ||||||
|  |         final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 1); | ||||||
|  |         final Option<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); | ||||||
|  |             exchange.close(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         responseHeaders.add("Content-type", output.getContentType()); | ||||||
|  |         exchange.sendResponseHeaders(200, 0); | ||||||
|  |         final Labyrinth labyrinth = new Labyrinth(width, height, id); | ||||||
|  |         final byte[] render = output.render(labyrinth); | ||||||
|  |         final OutputStream responseBody = exchange.getResponseBody(); | ||||||
|  |         responseBody.write(render); | ||||||
|  |         responseBody.flush(); | ||||||
|  |         exchange.close(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private enum RequestParameter { |     private enum RequestParameter { | ||||||
|  |  | ||||||
|  | @ -1,8 +1,11 @@ | ||||||
| package ch.fritteli.labyrinth.server; | package ch.fritteli.labyrinth.server; | ||||||
| 
 | 
 | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
|  | 
 | ||||||
|  | @Slf4j | ||||||
| public class Main { | public class Main { | ||||||
|     public static void main(String[] args) { |     public static void main(String[] args) { | ||||||
|         LabyrinthServer.createListener() |         LabyrinthServer.createAndStartServer() | ||||||
|                 .onEmpty(() -> System.err.println("Failed to create server. Stopping.")); |                 .onEmpty(() -> log.error("Failed to create server. Stopping.")); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										61
									
								
								src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | ||||||
|  | package ch.fritteli.labyrinth.server; | ||||||
|  | 
 | ||||||
|  | import io.vavr.control.Try; | ||||||
|  | import lombok.AccessLevel; | ||||||
|  | import lombok.AllArgsConstructor; | ||||||
|  | import lombok.NonNull; | ||||||
|  | import lombok.Value; | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
|  | import org.jetbrains.annotations.Nullable; | ||||||
|  | 
 | ||||||
|  | import java.net.InetAddress; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||||||
|  | @Slf4j | ||||||
|  | @Value | ||||||
|  | public class ServerConfig { | ||||||
|  |     public static final String SYSPROP_HOST = "fritteli.labyrinth.server.host"; | ||||||
|  |     public static final String SYSPROP_PORT = "fritteli.labyrinth.server.port"; | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     InetAddress address; | ||||||
|  |     int port; | ||||||
|  | 
 | ||||||
|  |     public ServerConfig(@Nullable final String address, final int port) throws ConfigurationException { | ||||||
|  |         this.address = validateAddress(address); | ||||||
|  |         this.port = validatePort(port); | ||||||
|  |         log.debug("host={}, port={}", this.address, this.port); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     public static ServerConfig init() throws ConfigurationException { | ||||||
|  |         final String host = System.getProperty(SYSPROP_HOST); | ||||||
|  |         final String portString = System.getProperty(SYSPROP_PORT); | ||||||
|  |         final int port = validatePort(portString); | ||||||
|  |         return new ServerConfig(host, port); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     private static InetAddress validateAddress(@Nullable final String address) { | ||||||
|  |         return Try.of(() -> InetAddress.getByName(address)) | ||||||
|  |                 .getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static int validatePort(@Nullable final String portString) { | ||||||
|  |         if (portString == null) { | ||||||
|  |             log.info("No port configured; using default."); | ||||||
|  |             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; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,61 @@ | ||||||
|  | package ch.fritteli.labyrinth.server; | ||||||
|  | 
 | ||||||
|  | import org.junit.jupiter.api.BeforeEach; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | 
 | ||||||
|  | import java.net.UnknownHostException; | ||||||
|  | import java.util.Properties; | ||||||
|  | 
 | ||||||
|  | import static org.junit.jupiter.api.Assertions.assertEquals; | ||||||
|  | import static org.junit.jupiter.api.Assertions.assertThrows; | ||||||
|  | 
 | ||||||
|  | class ServerConfigTest { | ||||||
|  |     @BeforeEach | ||||||
|  |     void clearSysProperties() { | ||||||
|  |         System.setProperties(new Properties()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testInit_noProperties() throws ConfigurationException { | ||||||
|  | 
 | ||||||
|  |         // act | ||||||
|  |         final ServerConfig sut = ServerConfig.init(); | ||||||
|  | 
 | ||||||
|  |         // assert | ||||||
|  |         assertEquals("127.0.0.1", sut.getAddress().getHostAddress()); | ||||||
|  |         assertEquals("localhost", sut.getAddress().getHostName()); | ||||||
|  |         assertEquals(0, sut.getPort()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testInit_unparseablePort() { | ||||||
|  |         // arrange | ||||||
|  |         System.setProperty(ServerConfig.SYSPROP_PORT, "Hello World!"); | ||||||
|  | 
 | ||||||
|  |         // act / assert | ||||||
|  |         assertThrows(ConfigurationException.class, ServerConfig::init); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testInit_invalidPort() { | ||||||
|  |         // arrange | ||||||
|  |         System.setProperty(ServerConfig.SYSPROP_PORT, "99999"); | ||||||
|  | 
 | ||||||
|  |         // act / assert | ||||||
|  |         assertThrows(ConfigurationException.class, ServerConfig::init); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testInit() throws ConfigurationException, UnknownHostException { | ||||||
|  |         // arrange | ||||||
|  |         System.setProperty(ServerConfig.SYSPROP_HOST, "127.0.0.1"); | ||||||
|  |         System.setProperty(ServerConfig.SYSPROP_PORT, "12345"); | ||||||
|  | 
 | ||||||
|  |         // act | ||||||
|  |         final ServerConfig sut = ServerConfig.init(); | ||||||
|  | 
 | ||||||
|  |         // assert | ||||||
|  |         assertEquals("127.0.0.1", sut.getAddress().getHostAddress()); | ||||||
|  |         assertEquals(12345, sut.getPort()); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue