HttpServer.kt
package com.hexagontk.http.server
import com.hexagontk.core.Platform.charset
import com.hexagontk.core.Platform.cpuCount
import com.hexagontk.core.Platform.hostName
import com.hexagontk.core.Platform.name
import com.hexagontk.core.Platform.version
import com.hexagontk.core.Platform.localeCode
import java.lang.Runtime.getRuntime
import com.hexagontk.core.text.AnsiColor.BLUE
import com.hexagontk.core.text.AnsiColor.CYAN
import com.hexagontk.core.text.AnsiColor.MAGENTA
import com.hexagontk.core.text.Ansi.RESET
import com.hexagontk.core.text.AnsiEffect.BOLD
import com.hexagontk.core.text.AnsiEffect.UNDERLINE
import com.hexagontk.core.Platform.timeZone
import com.hexagontk.core.Platform.totalMemory
import com.hexagontk.core.Platform.usedMemory
import com.hexagontk.core.text.prependIndent
import com.hexagontk.http.HttpFeature.ZIP
import com.hexagontk.http.handlers.HttpHandler
import com.hexagontk.http.handlers.HandlerBuilder
import com.hexagontk.http.handlers.path
import java.io.Closeable
import java.net.URI
/**
* Server that listen to HTTP connections on a port and address and route requests to handlers.
*
* TODO Allow light startup log
*/
class HttpServer(
private val adapter: HttpServerPort,
val handler: HttpHandler,
val settings: HttpServerSettings = HttpServerSettings()
) : Closeable {
/**
* Create a server with a builder ([HandlerBuilder]) to set up handlers.
*
* @param adapter The server engine.
* @param settings Server settings. Port and address will be searched in this map.
* @param block Handlers' setup block.
* @return A new server with the configured handlers.
*/
constructor(
adapter: HttpServerPort,
settings: HttpServerSettings = HttpServerSettings(),
block: HandlerBuilder.() -> Unit
) :
this(adapter, path(block = block), settings)
override fun close() {
stop()
}
init {
val supportedProtocols = adapter.supportedProtocols()
check(settings.protocol in supportedProtocols) {
val supportedProtocolsText = supportedProtocols.joinToString(", ")
"Requesting unsupported protocol. Adapter's protocols: $supportedProtocolsText"
}
if (settings.zip)
check(adapter.supportedFeatures().contains(ZIP)) {
val adapterName = adapter::class.qualifiedName
"Requesting ZIP compression with an adapter without support: '$adapterName'"
}
}
/**
* Runtime port of the server.
*
* @exception IllegalStateException Throw an exception if the server hasn't been started.
*/
val runtimePort: Int
get() = if (started()) adapter.runtimePort() else error("Server is not running")
/**
* Runtime binding of the server.
*
* @exception IllegalStateException Throw an exception if the server hasn't been started.
*/
val binding: URI
get() = URI("${settings.bindUrl}:$runtimePort")
/**
* The port name of the server.
*/
val portName: String = adapter.javaClass.simpleName
/**
* Check whether the server has been started.
*
* @return True if the server has started, else false.
*/
fun started(): Boolean =
adapter.started()
/**
* Start the server with the adapter instance and adds a shutdown hook for stopping the server.
*/
fun start() {
getRuntime().addShutdownHook(
Thread(
{
if (started())
adapter.shutDown()
},
"shutdown-${settings.bindAddress.hostName}-${settings.bindPort}"
)
)
adapter.startUp(this)
}
/**
* Stop the server.
*/
fun stop() {
adapter.shutDown()
}
fun createBanner(
startUpTimestamp: Long = -1, banner: String = serverBanner, detailed: Boolean = false
): String {
val server = "$BOLD$CYAN$portName$RESET"
val protocol = settings.protocol
val protocols = adapter.supportedProtocols()
.joinToString("$RESET, $CYAN", CYAN, RESET) { if (it == protocol) "✅$it" else "$it" }
val java = "$BOLD${BLUE}Java $version$RESET [$BLUE$name$RESET]"
val locale = "$BLUE$localeCode$RESET"
val timezone = "$BLUE${timeZone.id}$RESET"
val charsetValue = "$BLUE$charset$RESET"
val start = if (startUpTimestamp < 0) "<undefined>" else startUpTimestamp.toString()
val startTime = "$BOLD$MAGENTA in $start ms$RESET"
val bindingValue = "$BLUE$UNDERLINE$binding$RESET"
val information = if (detailed)
detailBanner(
server, protocols, java, locale, timezone, charsetValue, startTime, bindingValue
)
else
"""
Server Adapter: $server ($protocols)
🛠 Using $java
🌍 Locale: $locale Timezone: $timezone Charset: $charset
⏱️ Started$startTime
🚀 Served at $bindingValue
"""
val fullBanner = banner + information.trimIndent()
return "\n" + fullBanner.prependIndent()
}
private fun detailBanner(
server: String,
protocols: String,
java: String,
locale: String,
timezone: String,
charsetValue: String,
startTime: String,
bindingValue: String
): String {
val features = adapter.supportedFeatures()
.joinToString("$RESET, $CYAN", CYAN, RESET) { it.toString() }
val options = adapter.options()
.map { (k, v) -> "$k($v)" }
.joinToString("$RESET, $CYAN", CYAN, RESET)
val jvmMemoryValue = "$BLUE${totalMemory()} KB$RESET"
val usedMemoryValue = "$BOLD$MAGENTA${usedMemory()} KB$RESET"
val hostnameValue = "$BLUE$hostName$RESET"
val cpuCountValue = "$BLUE$cpuCount$RESET"
return """
Server Adapter: $server ($protocols)
Supported Features: $features
Configuration Options: $options
🖥️️ Running in '$hostnameValue' with $cpuCountValue CPUs $jvmMemoryValue of memory
🛠 Using $java
🌍 Locale: $locale Timezone: $timezone Charset: $charsetValue
⏱️ Started$startTime using $usedMemoryValue
🚀 Served at $bindingValue
"""
}
}