All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.http4k.core.Uri.kt Maven / Gradle / Ivy

package org.http4k.core

import org.http4k.appendIfNotBlank
import org.http4k.appendIfPresent
import java.net.URLDecoder
import java.net.URLEncoder

data class Uri(val scheme: String, val userInfo: String, val host: String, val port: Int?, val path: String, val query: String, val fragment: String) : Comparable {

    companion object {
        private val AUTHORITY = Regex("(?:([^@]+)@)?([^:]+)(?::([\\d]+))?")
        private val RFC3986 = Regex("^(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\\?([^#]*))?(?:#(.*))?")

        @JvmStatic
        fun of(value: String): Uri {
            val result = RFC3986.matchEntire(value) ?: throw RuntimeException("Invalid Uri: $value")
            val (scheme, authority, path, query, fragment) = result.destructured
            val (userInfo, host, port) = parseAuthority(authority)
            return Uri(scheme, userInfo, host, port, path, query, fragment)
        }

        private fun parseAuthority(authority: String): Triple = when {
            authority.isBlank() -> Triple("", "", null)
            else -> {
                val (userInfo, host, portString) = AUTHORITY.matchEntire(authority)?.destructured
                    ?: throw RuntimeException("Invalid authority: $authority")
                val port = portString.toIntOrNull()
                Triple(userInfo, host, port)
            }
        }
    }

    val authority = StringBuilder()
        .appendIfNotBlank(userInfo, userInfo, "@")
        .appendIfNotBlank(host, host)
        .appendIfPresent(port, ":", port.toString())
        .toString()

    fun scheme(scheme: String) = copy(scheme = scheme)
    fun userInfo(userInfo: String) = copy(userInfo = userInfo)
    fun host(host: String) = copy(host = host)
    fun port(port: Int?) = copy(port = port)
    fun path(path: String) = copy(path = path)
    fun query(query: String) = copy(query = query)
    fun fragment(fragment: String) = copy(fragment = fragment)

    fun authority(authority: String): Uri = parseAuthority(authority).let { (userInfo, host, port) ->
        copy(userInfo = userInfo, host = host, port = port)
    }

    override fun compareTo(other: Uri) = toString().compareTo(other.toString())

    override fun toString() = StringBuilder()
        .appendIfNotBlank(scheme, scheme, ":")
        .appendIfNotBlank(authority, "//", authority)
        .append(when {
            authority.isBlank() -> path
            path.isBlank() || path.startsWith("/") -> path
            else -> "/$path"
        })
        .appendIfNotBlank(query, "?", query)
        .appendIfNotBlank(fragment, "#", fragment).toString()
}

fun Uri.removeQuery(name: String) = copy(query = query.toParameters().filterNot { it.first == name }.toUrlFormEncoded())

fun Uri.removeQueries(prefix: String) =
    copy(query = query.toParameters().filterNot { it.first.startsWith(prefix) }.toUrlFormEncoded())

fun Uri.query(name: String, value: String?): Uri =
    copy(query = query.toParameters().plus(name to value).toUrlFormEncoded())

fun Uri.queryParametersEncoded(): Uri =
    copy(query = query.toParameters().toUrlFormEncoded())

/**
 * @see [RFC 3986, appendix A](https://www.ietf.org/rfc/rfc3986.txt)
 */
private val validPathSegmentChars = setOf(
    '~', '-', '.', '_',                                // unreserved
    '!', '$', '&', '\'', '(', ')', '+', ',', ';', '=', // sub-delims
    ':', '@'                                           // valid
)

private fun Char.isAsciiLetter() = this in 'a'..'z' || this in 'A'..'Z'

private fun Char.isValidSpecialPathSegmentChar() = validPathSegmentChars.contains(this)

fun String.toPathSegmentEncoded(): String =
    this.map {
        when {
            it.isAsciiLetter() || it.isDigit() || it.isValidSpecialPathSegmentChar() -> it
            it.isWhitespace() -> "%20"
            else -> URLEncoder.encode(it.toString(), "UTF-8")
        }
    }.joinToString(separator = "")

fun String.toPathSegmentDecoded(): String =
    URLDecoder.decode(this.replace("+", "%2B"), "UTF-8")

fun Uri.extend(uri: Uri): Uri =
    appendToPath(uri.path).copy(query = (query.toParameters() + uri.query.toParameters()).toUrlFormEncoded())

private fun Uri.appendToPath(newPath: String): Uri =
    if (newPath == "") this
    else copy(path = (path.removeSuffix("/") + "/" + newPath.removePrefix("/")))




© 2015 - 2025 Weber Informatics LLC | Privacy Policy