HttpServer.kt

  1. package com.hexagontk.http.server

  2. import com.hexagontk.core.Platform.charset
  3. import com.hexagontk.core.Platform.cpuCount
  4. import com.hexagontk.core.Platform.hostName
  5. import com.hexagontk.core.Platform.name
  6. import com.hexagontk.core.Platform.version
  7. import com.hexagontk.core.Platform.localeCode

  8. import java.lang.Runtime.getRuntime
  9. import com.hexagontk.core.text.AnsiColor.BLUE
  10. import com.hexagontk.core.text.AnsiColor.CYAN
  11. import com.hexagontk.core.text.AnsiColor.MAGENTA
  12. import com.hexagontk.core.text.Ansi.RESET
  13. import com.hexagontk.core.text.AnsiEffect.BOLD
  14. import com.hexagontk.core.text.AnsiEffect.UNDERLINE
  15. import com.hexagontk.core.Platform.timeZone
  16. import com.hexagontk.core.Platform.totalMemory
  17. import com.hexagontk.core.Platform.usedMemory
  18. import com.hexagontk.core.text.prependIndent
  19. import com.hexagontk.http.HttpFeature.ZIP
  20. import com.hexagontk.http.handlers.HttpHandler
  21. import com.hexagontk.http.handlers.HandlerBuilder
  22. import com.hexagontk.http.handlers.path
  23. import java.io.Closeable
  24. import java.net.URI

  25. /**
  26.  * Server that listen to HTTP connections on a port and address and route requests to handlers.
  27.  */
  28. class HttpServer(
  29.     private val adapter: HttpServerPort,
  30.     val handler: HttpHandler,
  31.     val settings: HttpServerSettings = HttpServerSettings()
  32. ) : Closeable {

  33.     /**
  34.      * Create a server with a builder ([HandlerBuilder]) to set up handlers.
  35.      *
  36.      * @param adapter The server engine.
  37.      * @param settings Server settings. Port and address will be searched in this map.
  38.      * @param block Handlers' setup block.
  39.      * @return A new server with the configured handlers.
  40.      */
  41.     constructor(
  42.         adapter: HttpServerPort,
  43.         settings: HttpServerSettings = HttpServerSettings(),
  44.         block: HandlerBuilder.() -> Unit
  45.     ) :
  46.         this(adapter, path(block = block), settings)

  47.     override fun close() {
  48.         stop()
  49.     }

  50.     init {
  51.         val supportedProtocols = adapter.supportedProtocols()
  52.         check(settings.protocol in supportedProtocols) {
  53.             val supportedProtocolsText = supportedProtocols.joinToString(", ")
  54.             "Requesting unsupported protocol. Adapter's protocols: $supportedProtocolsText"
  55.         }

  56.         if (settings.zip)
  57.             check(adapter.supportedFeatures().contains(ZIP)) {
  58.                 val adapterName = adapter::class.qualifiedName
  59.                 "Requesting ZIP compression with an adapter without support: '$adapterName'"
  60.             }
  61.     }

  62.     /**
  63.      * Runtime port of the server.
  64.      *
  65.      * @exception IllegalStateException Throw an exception if the server hasn't been started.
  66.      */
  67.     val runtimePort: Int
  68.         get() = if (started()) adapter.runtimePort() else error("Server is not running")

  69.     /**
  70.      * Runtime binding of the server.
  71.      *
  72.      * @exception IllegalStateException Throw an exception if the server hasn't been started.
  73.      */
  74.     val binding: URI
  75.         get() = URI("${settings.bindUrl}:$runtimePort")

  76.     /**
  77.      * The port name of the server.
  78.      */
  79.     val portName: String = adapter.javaClass.simpleName

  80.     /**
  81.      * Check whether the server has been started.
  82.      *
  83.      * @return True if the server has started, else false.
  84.      */
  85.     fun started(): Boolean =
  86.         adapter.started()

  87.     /**
  88.      * Start the server with the adapter instance and adds a shutdown hook for stopping the server.
  89.      */
  90.     fun start() {
  91.         getRuntime().addShutdownHook(
  92.             Thread(
  93.                 {
  94.                     if (started())
  95.                         adapter.shutDown()
  96.                 },
  97.                 "shutdown-${settings.bindAddress.hostName}-${settings.bindPort}"
  98.             )
  99.         )

  100.         adapter.startUp(this)
  101.     }

  102.     /**
  103.      * Stop the server.
  104.      */
  105.     fun stop() {
  106.         adapter.shutDown()
  107.     }

  108.     fun createBanner(
  109.         startUpTimestamp: Long = -1, banner: String = serverBanner, detailed: Boolean = false
  110.     ): String {
  111.         val server = "$BOLD$CYAN$portName$RESET"
  112.         val protocol = settings.protocol
  113.         val protocols = adapter.supportedProtocols()
  114.             .joinToString("$RESET, $CYAN", CYAN, RESET) { if (it == protocol) "✅$it" else "$it" }

  115.         val java = "$BOLD${BLUE}Java $version$RESET [$BLUE$name$RESET]"
  116.         val locale = "$BLUE$localeCode$RESET"
  117.         val timezone = "$BLUE${timeZone.id}$RESET"
  118.         val charsetValue = "$BLUE$charset$RESET"
  119.         val start = if (startUpTimestamp < 0) "<undefined>" else startUpTimestamp.toString()
  120.         val startTime = "$BOLD$MAGENTA in $start ms$RESET"
  121.         val bindingValue = "$BLUE$UNDERLINE$binding$RESET"

  122.         val information = if (detailed)
  123.             detailBanner(
  124.                 server, protocols, java, locale, timezone, charsetValue, startTime, bindingValue
  125.             )
  126.         else
  127.             """

  128.             Server Adapter: $server ($protocols)

  129.             🛠 Using $java
  130.             🌍 Locale: $locale Timezone: $timezone Charset: $charset

  131.             ⏱️ Started$startTime
  132.             🚀 Served at $bindingValue

  133.             """

  134.         val fullBanner = banner + information.trimIndent()
  135.         return "\n" + fullBanner.prependIndent()
  136.     }

  137.     private fun detailBanner(
  138.         server: String,
  139.         protocols: String,
  140.         java: String,
  141.         locale: String,
  142.         timezone: String,
  143.         charsetValue: String,
  144.         startTime: String,
  145.         bindingValue: String
  146.     ): String {
  147.         val features = adapter.supportedFeatures()
  148.             .joinToString("$RESET, $CYAN", CYAN, RESET) { it.toString() }

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

  152.         val jvmMemoryValue = "$BLUE${totalMemory()} KB$RESET"
  153.         val usedMemoryValue = "$BOLD$MAGENTA${usedMemory()} KB$RESET"
  154.         val hostnameValue = "$BLUE$hostName$RESET"
  155.         val cpuCountValue = "$BLUE$cpuCount$RESET"

  156.         return """

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

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

  163.             ⏱️ Started$startTime using $usedMemoryValue
  164.             🚀 Served at $bindingValue

  165.             """
  166.     }
  167. }