HttpServer.kt

  1. package com.hexagonkt.http.server

  2. import com.hexagonkt.core.logging.Logger
  3. import com.hexagonkt.core.Jvm.charset
  4. import com.hexagonkt.core.Jvm.cpuCount
  5. import com.hexagonkt.core.Jvm.hostName
  6. import com.hexagonkt.core.Jvm.name
  7. import com.hexagonkt.core.Jvm.version
  8. import com.hexagonkt.core.Jvm.localeCode
  9. import com.hexagonkt.http.model.HttpProtocol.HTTP2

  10. import java.lang.Runtime.getRuntime
  11. import com.hexagonkt.core.text.AnsiColor.BLUE
  12. import com.hexagonkt.core.text.AnsiColor.CYAN
  13. import com.hexagonkt.core.text.AnsiColor.DEFAULT
  14. import com.hexagonkt.core.text.AnsiColor.MAGENTA
  15. import com.hexagonkt.core.text.Ansi.RESET
  16. import com.hexagonkt.core.text.AnsiEffect.BOLD
  17. import com.hexagonkt.core.text.AnsiEffect.UNDERLINE
  18. import com.hexagonkt.core.Jvm.timeZone
  19. import com.hexagonkt.core.Jvm.totalMemory
  20. import com.hexagonkt.core.Jvm.usedMemory
  21. import com.hexagonkt.core.text.prependIndent
  22. import com.hexagonkt.core.urlOf
  23. import com.hexagonkt.http.server.HttpServerFeature.ZIP
  24. import com.hexagonkt.http.handlers.HttpHandler
  25. import com.hexagonkt.http.handlers.HandlerBuilder
  26. import com.hexagonkt.http.handlers.path
  27. import java.io.Closeable
  28. import java.lang.System.nanoTime
  29. import java.net.URL

  30. /**
  31.  * Server that listen to HTTP connections on a port and address and route requests to handlers.
  32.  */
  33. data class HttpServer(
  34.     private val adapter: HttpServerPort,
  35.     val handler: HttpHandler,
  36.     val settings: HttpServerSettings = HttpServerSettings()
  37. ) : Closeable {

  38.     companion object {
  39.         private val logger: Logger = Logger(this::class)

  40.         val banner: String = """
  41.         $CYAN          _________
  42.         $CYAN         /         \
  43.         $CYAN        /   ____   /
  44.         $CYAN       /   /   /  /
  45.         $CYAN      /   /   /__/$BLUE   /\$BOLD    H E X A G O N$RESET
  46.         $CYAN     /   /$BLUE          /  \$DEFAULT        ___
  47.         $CYAN     \  /$BLUE   ___    /   /
  48.         $CYAN      \/$BLUE   /  /   /   /$CYAN    T O O L K I T$RESET
  49.         $BLUE          /  /___/   /
  50.         $BLUE         /          /
  51.         $BLUE         \_________/       https://hexagontk.com/http_server
  52.         $RESET
  53.         """.trimIndent()
  54.     }

  55.     /**
  56.      * Create a server with a builder ([HandlerBuilder]) to set up handlers.
  57.      *
  58.      * @param adapter The server engine.
  59.      * @param settings Server settings. Port and address will be searched in this map.
  60.      * @param block Handlers' setup block.
  61.      * @return A new server with the configured handlers.
  62.      */
  63.     constructor(
  64.         adapter: HttpServerPort,
  65.         settings: HttpServerSettings = HttpServerSettings(),
  66.         block: HandlerBuilder.() -> Unit
  67.     ) :
  68.         this(adapter, path(block = block), settings)

  69.     override fun close() {
  70.         stop()
  71.     }

  72.     init {
  73.         val supportedProtocols = adapter.supportedProtocols()
  74.         check(settings.protocol in supportedProtocols) {
  75.             val supportedProtocolsText = supportedProtocols.joinToString(", ")
  76.             "Requesting unsupported protocol. Adapter's protocols: $supportedProtocolsText"
  77.         }

  78.         if (settings.zip)
  79.             check(adapter.supportedFeatures().contains(ZIP)) {
  80.                 val adapterName = adapter::class.qualifiedName
  81.                 "Requesting ZIP compression with an adapter without support: '$adapterName'"
  82.             }
  83.     }

  84.     /**
  85.      * Runtime port of the server.
  86.      *
  87.      * @exception IllegalStateException Throw an exception if the server hasn't been started.
  88.      */
  89.     val runtimePort: Int
  90.         get() = if (started()) adapter.runtimePort() else error("Server is not running")

  91.     /**
  92.      * Runtime binding of the server.
  93.      *
  94.      * @exception IllegalStateException Throw an exception if the server hasn't been started.
  95.      */
  96.     val binding: URL
  97.         get() = urlOf("${settings.bindUrl}:$runtimePort")

  98.     /**
  99.      * The port name of the server.
  100.      */
  101.     val portName: String = adapter.javaClass.simpleName

  102.     /**
  103.      * Check whether the server has been started.
  104.      *
  105.      * @return True if the server has started, else false.
  106.      */
  107.     fun started(): Boolean =
  108.         adapter.started()

  109.     /**
  110.      * Start the server with the adapter instance and adds a shutdown hook for stopping the server.
  111.      */
  112.     fun start() {
  113.         val startTimestamp = nanoTime()

  114.         getRuntime().addShutdownHook(
  115.             Thread(
  116.                 {
  117.                     if (started())
  118.                         adapter.shutDown()
  119.                 },
  120.                 "shutdown-${settings.bindAddress.hostName}-${settings.bindPort}"
  121.             )
  122.         )

  123.         adapter.startUp(this)
  124.         logger.info { "Server started${createBanner(nanoTime() - startTimestamp)}" }
  125.     }

  126.     /**
  127.      * Stop the server.
  128.      */
  129.     fun stop() {
  130.         adapter.shutDown()
  131.         logger.info { "Server stopped" }
  132.     }

  133.     internal fun createBanner(startUpTimestamp: Long): String {

  134.         val startUpTime = "%,.0f".format(startUpTimestamp / 1e6)
  135.         val protocol = settings.protocol
  136.         val banner = settings.banner ?: return " at $binding ($startUpTime ms)"

  137.         val jvmMemoryValue = "$BLUE${totalMemory()} KB$RESET"
  138.         val usedMemoryValue = "$BOLD$MAGENTA${usedMemory()} KB$RESET"
  139.         val serverAdapterValue = "$BOLD$CYAN$portName$RESET"

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

  142.         val features = adapter.supportedFeatures()
  143.             .joinToString("$RESET, $CYAN", CYAN, RESET) { it.toString() }

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

  147.         val hostnameValue = "$BLUE$hostName$RESET"
  148.         val cpuCountValue = "$BLUE$cpuCount$RESET"

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

  150.         val localeValue = "$BLUE$localeCode$RESET"
  151.         val timezoneValue = "$BLUE${timeZone.id}$RESET"
  152.         val charsetValue = "$BLUE$charset$RESET"

  153.         val startUpTimeValue = "$BOLD$MAGENTA$startUpTime ms$RESET"
  154.         val bindingValue = "$BLUE$UNDERLINE$binding$RESET"

  155.         val information =
  156.             """

  157.             Server Adapter: $serverAdapterValue ($protocols)
  158.             Supported Features: $features
  159.             Configuration Options: $options

  160.             🖥️️ Running in '$hostnameValue' with $cpuCountValue CPUs $jvmMemoryValue of memory
  161.             🛠 Using $javaVersionValue
  162.             🌍 Locale: $localeValue Timezone: $timezoneValue Charset: $charsetValue

  163.             ⏱️ Started in $startUpTimeValue (excluding VM) using $usedMemoryValue
  164.             🚀 Served at $bindingValue${if (protocol == HTTP2) " (HTTP/2)" else "" }

  165.             """

  166.         val fullBanner = banner + information.trimIndent()
  167.         return "\n" + fullBanner.prependIndent()
  168.     }
  169. }