HttpServer.kt

package com.hexagonkt.http.server

import com.hexagonkt.core.logging.Logger
import com.hexagonkt.core.Jvm.charset
import com.hexagonkt.core.Jvm.cpuCount
import com.hexagonkt.core.Jvm.hostname
import com.hexagonkt.core.Jvm.ip
import com.hexagonkt.core.Jvm.name
import com.hexagonkt.core.Jvm.version
import com.hexagonkt.core.Jvm.localeCode
import com.hexagonkt.core.Jvm.timezone
import com.hexagonkt.http.model.HttpProtocol.HTTP2
import com.hexagonkt.http.model.HttpProtocol.HTTP

import java.lang.Runtime.getRuntime
import java.lang.management.ManagementFactory.getMemoryMXBean
import java.lang.management.ManagementFactory.getRuntimeMXBean
import com.hexagonkt.core.Ansi.BLUE
import com.hexagonkt.core.Ansi.BOLD
import com.hexagonkt.core.Ansi.CYAN
import com.hexagonkt.core.Ansi.DEFAULT
import com.hexagonkt.core.Ansi.MAGENTA
import com.hexagonkt.core.Ansi.RESET
import com.hexagonkt.core.Ansi.UNDERLINE
import com.hexagonkt.core.prependIndent
import com.hexagonkt.http.server.handlers.ServerBuilder
import com.hexagonkt.http.server.handlers.ServerHandler
import com.hexagonkt.http.server.handlers.path
import java.io.Closeable
import java.lang.System.nanoTime

/**
 * Server that listen to HTTP connections on a port and address and route requests to handlers.
 */
data class HttpServer(
    private val adapter: HttpServerPort,
    val handlers: List<ServerHandler>,
    val settings: HttpServerSettings = HttpServerSettings()
) : Closeable {

    companion object {
        val banner: String = """
        $CYAN          _________
        $CYAN         /         \
        $CYAN        /   ____   /
        $CYAN       /   /   /  /
        $CYAN      /   /   /__/$BLUE   /\$BOLD    H E X A G O N$RESET
        $CYAN     /   /$BLUE          /  \$DEFAULT        ___
        $CYAN     \  /$BLUE   ___    /   /
        $CYAN      \/$BLUE   /  /   /   /$CYAN    T O O L K I T$RESET
        $BLUE          /  /___/   /
        $BLUE         /          /
        $BLUE         \_________/       https://hexagonkt.com/http_server
        $RESET
        """.trimIndent()
    }

    private val logger: Logger = Logger(this::class)

    /**
     * Create a server with a builder ([ServerBuilder]) 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: ServerBuilder.() -> Unit
    ) :
        this(adapter, listOf(path(block = block)), settings)

    /**
     * Utility constructor for the common case of having a single root handler.
     *
     * @param adapter The server engine.
     * @param handler The only handler used for this server.
     * @param settings Server settings. Port and address will be searched in this map.
     */
    constructor(
        adapter: HttpServerPort,
        handler: ServerHandler,
        settings: HttpServerSettings = HttpServerSettings(),
    ) : this(adapter, listOf(handler), 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"
        }

        val supportedFeatures = adapter.supportedFeatures()
        check(settings.features.all { it in supportedFeatures }) {
            val supportedFeaturesText = supportedFeatures.joinToString(", ")
            "Requesting unsupported feature. Adapter's features: $supportedFeaturesText"
        }
    }

    /**
     * Runtime port of the server.
     *
     * @exception IllegalStateException Throw an exception if the server hasn't been started.
     */
    val runtimePort
        get() = if (started()) adapter.runtimePort() else error("Server is not running")

    /**
     * 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() {
        val startTimestamp = nanoTime()

        getRuntime().addShutdownHook(
            Thread(
                {
                    if (started())
                        adapter.shutDown()
                },
                "shutdown-${settings.bindAddress.hostName}-${settings.bindPort}"
            )
        )

        adapter.startUp(this)
        logger.info { "Server started\n${createBanner(nanoTime() - startTimestamp)}" }
    }

    /**
     * Stop the server.
     */
    fun stop() {
        adapter.shutDown()
        logger.info { "Server stopped" }
    }

    internal fun createBanner(startUpTimestamp: Long): String {

        val heap = getMemoryMXBean().heapMemoryUsage
        val jvmMemory = "%,d".format(heap.init / 1024)
        val usedMemory = "%,d".format(heap.used / 1024)
        val bootTime = "%01.3f".format(getRuntimeMXBean().uptime / 1e3)
        val startUpTime = "%,.0f".format(startUpTimestamp / 1e6)
        val bindAddress = settings.bindAddress
        val protocol = settings.protocol
        val hostName = if (bindAddress.isAnyLocalAddress) ip else bindAddress.canonicalHostName
        val scheme = if (protocol == HTTP) "http" else "https"
        val binding = "$scheme://$hostName:$runtimePort"

        val serverAdapterValue = "$BOLD$CYAN$portName$RESET"

        val protocols = adapter.supportedProtocols()
            .joinToString("$RESET, $CYAN", CYAN, RESET) {
                if (it == settings.protocol) "✅$it" else "$it"
            }

        val features = adapter.supportedFeatures()
            .joinToString("$RESET, $CYAN", CYAN, RESET) {
                if (settings.features.contains(it)) "✅$it" else "$it"
            }

        val options = adapter.options()
            .map { (k, v) -> "$k($v)" }
            .joinToString("$RESET, $CYAN", CYAN, RESET)

        val hostnameValue = "$BLUE$hostname$RESET"
        val cpuCountValue = "$BLUE$cpuCount$RESET"
        val jvmMemoryValue = "$BLUE$jvmMemory$RESET"

        val javaVersionValue = "$BOLD${BLUE}Java $version$RESET [$BLUE$name$RESET]"

        val localeValue = "$BLUE$localeCode$RESET"
        val timezoneValue = "$BLUE$timezone$RESET"
        val charsetValue = "$BLUE$charset$RESET"

        val bootTimeValue = "$BOLD$MAGENTA$bootTime s$RESET"
        val startUpTimeValue = "$BOLD$MAGENTA$startUpTime ms$RESET"
        val usedMemoryValue = "$BOLD$MAGENTA$usedMemory KB$RESET"
        val bindingValue = "$BLUE$UNDERLINE$binding$RESET"

        val information = """

            Server Adapter: $serverAdapterValue ($protocols)
            Supported Features: $features
            Configuration Options: $options

            🖥️️ Running in '$hostnameValue' with $cpuCountValue CPUs $jvmMemoryValue KB
            🛠 Using $javaVersionValue
            🌍 Locale: $localeValue Timezone: $timezoneValue Charset: $charsetValue

            ⏱ Started in $bootTimeValue (server: $startUpTimeValue) using $usedMemoryValue
            🚀 Served at $bindingValue${if (protocol == HTTP2) " (HTTP/2)" else "" }

        """.trimIndent()

        val banner = (settings.banner?.let { "$it\n" } ?: banner) + information
        return banner.prependIndent()
    }
}