Last active
January 9, 2024 09:09
-
-
Save svaponi/03153b194417ff4d2cd75090f0eff80e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package io.github.svaponi.http | |
import com.sun.net.httpserver.HttpExchange | |
import com.sun.net.httpserver.HttpHandler | |
import com.sun.net.httpserver.HttpServer | |
import mu.KLogging | |
import java.io.IOException | |
import java.net.InetSocketAddress | |
import java.net.ServerSocket | |
import java.util.regex.Pattern | |
/** | |
* Starts a simple web application. Register endpoints using: | |
* | |
* @see SimpleServer.registerResponseProvider | |
*/ | |
class SimpleServer(port: Int? = null) { | |
data class Response( | |
val status: Int = 0, | |
val body: String? = null, | |
val headers: Map<String, List<String>>? = null | |
) | |
private val pathPatternToResponseProvider: MutableMap<Pattern, java.util.function.Function<HttpExchange, Response>> = | |
HashMap() | |
private val address: InetSocketAddress by lazy { InetSocketAddress(port ?: findFreePort()) } | |
private var httpServer: HttpServer? = null | |
private fun findFreePort(): Int = try { | |
ServerSocket(0).use { it.getLocalPort() } | |
} catch (ignore: IOException) { | |
findFreePort() | |
} | |
/** | |
* Recreates an instance of HttpServer. From Javadoc: "Once stopped, a HttpServer cannot be re-used." | |
* | |
* @see HttpServer.stop | |
*/ | |
private fun createHttpServer(): HttpServer { | |
return try { | |
System.setProperty("sun.net.httpserver.maxReqTime", "1000") | |
System.setProperty("sun.net.httpserver.maxRspTime", "1000") | |
val httpServer = HttpServer.create(address, 0) | |
httpServer.createContext("/", HttpHandlerImpl()) // handles all requests /** | |
httpServer | |
} catch (e: IOException) { | |
throw IllegalStateException("Impossible to start server: ${e.message}", e) | |
} | |
} | |
private inner class HttpHandlerImpl : HttpHandler { | |
@Throws(IOException::class) | |
override fun handle(httpExchange: HttpExchange) { | |
val path: String = httpExchange.requestURI.getRawSchemeSpecificPart() | |
try { | |
val responseProvider: java.util.function.Function<HttpExchange, Response>? = | |
pathPatternToResponseProvider.keys.stream() | |
.filter { pathPattern: Pattern -> pathPattern.matcher(path).matches() } | |
.findFirst() | |
.map<java.util.function.Function<HttpExchange, Response>?> { pathPattern: Pattern -> pathPatternToResponseProvider[pathPattern] } | |
.orElse(null) | |
if (responseProvider != null) { | |
val response: Response = responseProvider.apply(httpExchange) | |
if (response.headers != null) { | |
httpExchange.responseHeaders.putAll(response.headers) | |
} | |
if (response.body == null || response.status == 204) { | |
httpExchange.sendResponseHeaders(response.status, -1) | |
} else { | |
val body = response.body | |
httpExchange.sendResponseHeaders(response.status, body.length.toLong()) | |
httpExchange.responseBody.use { os -> os.write(body.toByteArray()) } | |
logger.info("${httpExchange.requestMethod} $path >>> ${response.status} $body") | |
return | |
} | |
} | |
httpExchange.sendResponseHeaders(404, -1) | |
logger.warn("${httpExchange.requestMethod} $path >>> 404 NOT_FOUND") | |
} catch (e: Exception) { | |
httpExchange.sendResponseHeaders(500, -1) | |
logger.error("${httpExchange.requestMethod} $path >>> 500 SERVER_ERROR ${e.javaClass.getSimpleName()} ${e.message}") | |
} | |
} | |
} | |
val port: Int | |
get() = address.port | |
fun registerResponseProvider( | |
pathPattern: Pattern, | |
responseProvider: java.util.function.Function<HttpExchange, Response> | |
): SimpleServer { | |
pathPatternToResponseProvider[pathPattern] = responseProvider | |
return this | |
} | |
fun start(): SimpleServer { | |
if (httpServer == null) { | |
logger.debug("Starting on http://localhost:{}", this.port) | |
httpServer = createHttpServer() | |
httpServer!!.start() | |
logger.info("Started on http://localhost:{}", this.port) | |
for (pathPattern in pathPatternToResponseProvider.keys) { | |
logger.debug("Active url http://localhost:{}{}", this.port, pathPattern.toString()) | |
} | |
} else { | |
logger.debug("Already running on http://localhost:{}", this.port) | |
} | |
return this | |
} | |
fun stop() { | |
if (httpServer != null) { | |
logger.debug("Stopping on http://localhost:{}", this.port) | |
httpServer!!.stop(0) | |
httpServer = null | |
logger.info("Stopped on http://localhost:{}", this.port) | |
} else { | |
logger.debug("Already stopped on http://localhost:{}", this.port) | |
} | |
} | |
companion object : KLogging() | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment