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.  * TODO Allow light startup log
  29.  */
  30. class HttpServer(
  31.     private val adapter: HttpServerPort,
  32.     val handler: HttpHandler,
  33.     val settings: HttpServerSettings = HttpServerSettings()
  34. ) : Closeable {

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

  49.     override fun close() {
  50.         stop()
  51.     }

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

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

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

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

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

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

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

  102.         adapter.startUp(this)
  103.     }

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

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

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

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

  130.             Server Adapter: $server ($protocols)

  131.             🛠 Using $java
  132.             🌍 Locale: $locale Timezone: $timezone Charset: $charset

  133.             ⏱️ Started$startTime
  134.             🚀 Served at $bindingValue

  135.             """

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

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

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

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

  158.         return """

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

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

  165.             ⏱️ Started$startTime using $usedMemoryValue
  166.             🚀 Served at $bindingValue

  167.             """
  168.     }
  169. }