Http.kt

  1. package com.hexagontk.http

  2. import com.hexagontk.core.GMT_ZONE
  3. import com.hexagontk.core.Platform
  4. import com.hexagontk.core.media.MediaType
  5. import com.hexagontk.core.text.encodeToBase64
  6. import com.hexagontk.http.model.*
  7. import java.net.URLDecoder
  8. import java.net.URLEncoder
  9. import java.nio.charset.Charset
  10. import java.time.*
  11. import java.time.ZoneOffset.UTC
  12. import java.time.format.DateTimeFormatter
  13. import java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME

  14. internal val HTTP_DATE_FORMATTER: DateTimeFormatter by lazy { RFC_1123_DATE_TIME.withZone(UTC) }

  15. fun basicAuth(user: String, password: String = ""): String =
  16.     "$user:$password".encodeToBase64()

  17. /**
  18.  * Parse query string such as `paramA=valueA&paramB=valueB` into a map of several key-value pairs
  19.  * separated by '&' where *key* is the param name before '=' as String and *value* is the string
  20.  * after '=' as a list of String (as a query parameter may have many values).
  21.  *
  22.  * Note: Missing the '=' sign, or missing value after '=' (e.g. `foo=` or `foo`) will result into an
  23.  * empty string value.
  24.  *
  25.  * @param query URL query string. E.g.: `param=value&foo=bar`.
  26.  * @return Map with query parameter keys bound to a list with their values.
  27.  *
  28.  */
  29. fun parseQueryString(query: String): Parameters =
  30.     if (query.isBlank())
  31.         Parameters()
  32.     else
  33.         Parameters(
  34.             query
  35.                 .split("&".toRegex())
  36.                 .map {
  37.                     val keyValue = it.split("=").map(String::trim)
  38.                     val key = keyValue[0]
  39.                     val value = if (keyValue.size == 2) keyValue[1] else ""
  40.                     key.urlDecode() to value.urlDecode()
  41.                 }
  42.                 .filter { it.first.isNotBlank() }
  43.                 .map { (k, v) -> Parameter(k, v) }
  44.         )

  45. fun formatQueryString(parameters: Parameters): String =
  46.     parameters
  47.         .filter { it.name.isNotBlank() }
  48.         .joinToString("&") {
  49.             if (it.text.isBlank()) it.name.urlEncode()
  50.             else "${it.name.urlEncode()}=${it.text.urlEncode()}"
  51.         }

  52. fun String.urlDecode(): String =
  53.     URLDecoder.decode(this, Platform.charset)

  54. fun String.urlEncode(): String =
  55.     URLEncoder.encode(this, Platform.charset)

  56. fun LocalDateTime.toHttpFormat(): String =
  57.     HTTP_DATE_FORMATTER
  58.         .format(ZonedDateTime.of(this, Platform.zoneId).withZoneSameInstant(GMT_ZONE))

  59. fun Instant.toHttpFormat(): String =
  60.     HTTP_DATE_FORMATTER.format(this)

  61. fun parseContentType(contentType: String): ContentType {
  62.     val typeParameter = contentType.split(";")
  63.     val fullType = typeParameter.first().trim()
  64.     val mimeType = MediaType(fullType)

  65.     return when (typeParameter.size) {
  66.         1 -> ContentType(mimeType)
  67.         2 -> {
  68.             val parameter = typeParameter.last()
  69.             val nameValue = parameter.split("=")
  70.             if (nameValue.size != 2)
  71.                 error("Invalid content type format: $contentType")

  72.             val name = nameValue.first().trim()
  73.             val value = nameValue.last().trim()

  74.             when (name.trim().lowercase()) {
  75.                 "boundary" -> ContentType(mimeType, boundary = value)
  76.                 "charset" -> ContentType(mimeType, charset = Charset.forName(value))
  77.                 "q" -> ContentType(mimeType, q = value.toDouble())
  78.                 else -> error("Invalid content type format: $contentType")
  79.             }
  80.         }
  81.         else -> error("Invalid content type format: $contentType")
  82.     }
  83. }