Skip to content

Instantly share code, notes, and snippets.

@svaponi
Last active January 9, 2024 09:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save svaponi/03153b194417ff4d2cd75090f0eff80e to your computer and use it in GitHub Desktop.
Save svaponi/03153b194417ff4d2cd75090f0eff80e to your computer and use it in GitHub Desktop.
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