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

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

package org.http4k.core

import org.http4k.asString
import org.http4k.core.Body.Companion.EMPTY
import org.http4k.core.HttpMessage.Companion.HTTP_1_1
import org.http4k.length
import org.http4k.lens.WebForm
import org.http4k.routing.RoutedRequest
import java.io.Closeable
import java.io.InputStream
import java.nio.ByteBuffer

typealias Headers = Parameters

/**
 * If this Body is NOT being returned to the caller (via a Server implementation or otherwise), close() should be
 * called.
 */
interface Body : Closeable {
    val stream: InputStream
    val payload: ByteBuffer

    /**
     * Will be `null` for bodies where it's impossible to a priori determine - e.g. StreamBody
     */
    val length: Long?

    companion object {
        @JvmStatic
        @JvmName("create")
        operator fun invoke(body: String): Body = MemoryBody(body)

        @JvmStatic
        @JvmName("create")
        operator fun invoke(body: ByteBuffer): Body = when {
            body.hasArray() -> MemoryBody(body)
            else -> MemoryBody(ByteArray(body.remaining()).also { body.get(it) })
        }

        @JvmStatic
        @JvmName("create")
        operator fun invoke(body: InputStream, length: Long? = null): Body = StreamBody(body, length)

        @JvmField
        val EMPTY: Body = MemoryBody(ByteBuffer.allocate(0))
    }
}

/**
 * Represents a body that is backed by an in-memory ByteBuffer. Closing this has no effect.
 **/
data class MemoryBody(override val payload: ByteBuffer) : Body {
    constructor(payload: String) : this(ByteBuffer.wrap(payload.toByteArray()))
    constructor(payload: ByteArray) : this(ByteBuffer.wrap(payload))

    override val length get() = payload.length().toLong()
    override fun close() {}
    override val stream get() = payload.array().inputStream(payload.position(), payload.length())
    override fun toString() = payload.asString()
}

/**
 * Represents a body that is backed by a (lazy) InputStream. Operating with StreamBody has a number of potential
 * gotchas:
 * 1. Attempts to consume the stream will pull all of the contents into memory, and should thus be avoided.
 * This includes calling `equals()` and `payload`
 * 2. If this Body is NOT being returned to the caller (via a Server implementation or otherwise), close() should be called.
 * 3. Depending on the source of the stream, this body may or may not contain a known length.
 */
class StreamBody(override val stream: InputStream, override val length: Long? = null) : Body {
    override val payload: ByteBuffer by lazy { stream.use { ByteBuffer.wrap(it.readBytes()) } }

    override fun close() {
        stream.close()
    }

    override fun toString() = "<>"

    override fun equals(other: Any?) =
        when {
            this === other -> true
            other !is Body? -> false
            else -> payload == other?.payload
        }

    override fun hashCode() = payload.hashCode()
}

/**
 * HttpMessages are designed to be immutable, so any mutation methods return a modified copy of the message.
 */
interface HttpMessage : Closeable {
    val headers: Headers
    val body: Body
    val version: String

    /**
     * Returns a formatted wire representation of this message.
     */
    fun toMessage(): String

    /**
     * Retrieves the first header value with this name.
     */
    fun header(name: String): String? = headers.headerValue(name)

    /**
     * (Copy &) Adds a header value with this name.
     */
    fun header(name: String, value: String?): HttpMessage

    /**
     * (Copy &) Add all passed headers.
     */
    fun headers(headers: Headers): HttpMessage

    /**
     * Replace all headers with ones passed.
     */
    fun replaceHeaders(source: Headers): HttpMessage

    /**
     * (Copy &) Adds a header value with this name, replacing any previously set values.
     */
    fun replaceHeader(name: String, value: String?): HttpMessage

    /**
     * (Copy &) remove headers with this name.
     */
    fun removeHeader(name: String): HttpMessage

    /**
     * (Copy &) remove headers with this prefix. Default removes all headers.
     */
    fun removeHeaders(prefix: String = ""): HttpMessage

    /**
     * (Copy &) sets the body content.
     */
    fun body(body: Body): HttpMessage

    /**
     * (Copy &) sets the body content.
     */
    fun body(body: String): HttpMessage

    /**
     * (Copy &) sets the body content.
     */
    fun body(body: InputStream, length: Long? = null): HttpMessage

    /**
     * Retrieves all header values with this name.
     */
    fun headerValues(name: String): List = headers.headerValues(name)

    /**
     * This will realise any underlying stream.
     */
    fun bodyString(): String = String(body.payload.array())

    companion object {
        const val HTTP_1_1 = "HTTP/1.1"
        const val HTTP_2 = "HTTP/2"
    }

    /**
     * Closes the request. For server generated messages, this is called by all backend/client implementations,
     * but when consuming external responses in streaming mode, this should be used.
     */
    override fun close() = body.close()
}

enum class Method { GET, POST, PUT, DELETE, OPTIONS, TRACE, PATCH, PURGE, HEAD }

interface Request : HttpMessage {
    val method: Method
    val uri: Uri
    val source: RequestSource?

    /**
     * (Copy &) sets the method.
     */
    fun method(method: Method): Request

    /**
     * (Copy &) sets the Uri.
     */
    fun uri(uri: Uri): Request

    /**
     * (Copy &) Adds a query value with this name.
     */
    fun query(name: String, value: String?): Request

    /**
     * Retrieves the first query value with this name.
     */
    fun query(name: String): String?

    /**
     * Retrieves all query values with this name.
     */
    fun queries(name: String): List

    /**
     * (Copy &) remove queries with this name.
     */
    fun removeQuery(name: String): Request

    /**
     * (Copy &) remove queries with this prefix. Default removes all queries.
     */
    fun removeQueries(prefix: String = ""): Request

    /**
     * (Copy &) sets request source.
     */
    fun source(source: RequestSource): Request

    override fun header(name: String, value: String?): Request

    override fun headers(headers: Headers): Request

    override fun replaceHeader(name: String, value: String?): Request

    override fun replaceHeaders(source: Headers): Request

    override fun removeHeader(name: String): Request

    override fun removeHeaders(prefix: String): Request

    override fun body(body: Body): Request

    override fun body(body: String): Request

    override fun body(body: InputStream, length: Long?): Request

    override fun toMessage() = listOf("$method $uri $version", headers.toHeaderMessage(), bodyString()).joinToString("\r\n")

    companion object {
        @JvmStatic
        @JvmOverloads
        @JvmName("create")
        operator fun invoke(method: Method, uri: Uri, version: String = HTTP_1_1): Request = MemoryRequest(method, uri, listOf(), EMPTY, version)

        @JvmStatic
        @JvmOverloads
        @JvmName("create")
        operator fun invoke(method: Method, uri: String, version: String = HTTP_1_1): Request = Request(method, Uri.of(uri), version)

        operator fun invoke(method: Method, template: UriTemplate, version: String = HTTP_1_1): Request = RoutedRequest(Request(method, template.toString(), version), template)
    }
}

@Suppress("EqualsOrHashCode")
data class MemoryRequest(
    override val method: Method,
    override val uri: Uri,
    override val headers: Headers = listOf(),
    override val body: Body = EMPTY,
    override val version: String = HTTP_1_1,
    override val source: RequestSource? = null
) : Request {
    override fun method(method: Method): Request = copy(method = method)

    override fun uri(uri: Uri) = copy(uri = uri)

    override fun query(name: String, value: String?) = copy(uri = uri.query(name, value))

    override fun query(name: String): String? = uri.queries().findSingle(name)

    override fun queries(name: String): List = uri.queries().findMultiple(name)

    override fun header(name: String, value: String?) = copy(headers = headers.plus(name to value))

    override fun replaceHeaders(source: Headers) = copy(headers = source)

    override fun headers(headers: Headers) = copy(headers = this.headers + headers)

    override fun replaceHeader(name: String, value: String?) = copy(headers = headers.replaceHeader(name, value))

    override fun source(source: RequestSource) = copy(source = source)

    override fun removeHeader(name: String) = copy(headers = headers.removeHeader(name))

    override fun removeHeaders(prefix: String) = copy(headers = headers.removeHeaders(prefix))

    override fun removeQuery(name: String) = copy(uri = uri.removeQuery(name))

    override fun removeQueries(prefix: String) = copy(uri = uri.removeQueries(prefix))

    override fun body(body: Body) = copy(body = body)

    override fun body(body: String) = copy(body = Body(body))

    override fun body(body: InputStream, length: Long?) = copy(body = Body(body, length))

    override fun toString(): String = toMessage()

    override fun equals(other: Any?) = (other is Request
        && headers.areSameHeadersAs(other.headers)
        && method == other.method
        && uri == other.uri
        && body == other.body)
}

@Suppress("EqualsOrHashCode")
interface Response : HttpMessage {
    val status: Status

    override fun header(name: String, value: String?): Response

    override fun headers(headers: Headers): Response

    override fun replaceHeader(name: String, value: String?): Response

    override fun replaceHeaders(source: Headers): Response

    override fun removeHeader(name: String): Response

    override fun removeHeaders(prefix: String): Response

    override fun body(body: Body): Response

    override fun body(body: String): Response

    override fun body(body: InputStream, length: Long?): Response

    fun status(new: Status): Response

    override fun toMessage(): String = listOf("$version $status", headers.toHeaderMessage(), bodyString()).joinToString("\r\n")

    companion object {
        @JvmStatic
        @JvmOverloads
        @JvmName("create")
        operator fun invoke(status: Status, version: String = HTTP_1_1): Response = MemoryResponse(status, listOf(), EMPTY, version)
    }
}

@Suppress("EqualsOrHashCode")
data class MemoryResponse(override val status: Status, override val headers: Headers = listOf(), override val body: Body = EMPTY, override val version: String = HTTP_1_1) : Response {
    override fun header(name: String, value: String?) = copy(headers = headers + (name to value))

    override fun headers(headers: Headers) = copy(headers = this.headers + headers)

    override fun replaceHeader(name: String, value: String?) = copy(headers = headers.replaceHeader(name, value))

    override fun replaceHeaders(source: Headers) = copy(headers = source)

    override fun removeHeader(name: String) = copy(headers = headers.removeHeader(name))

    override fun removeHeaders(prefix: String) = copy(headers = headers.removeHeaders(prefix))

    override fun body(body: Body) = copy(body = body)

    override fun body(body: String) = copy(body = Body(body))

    override fun status(new: Status) = copy(status = new)

    override fun body(body: InputStream, length: Long?) = copy(body = Body(body, length))

    override fun toString(): String = toMessage()

    override fun equals(other: Any?) = (other is Response
        && headers.areSameHeadersAs(other.headers)
        && status == other.status
        && body == other.body)
}

data class RequestSource(val address: String, val port: Int? = 0, val scheme: String? = null)

fun  T.with(vararg modifiers: (T) -> T): T = modifiers.fold(this) { memo, next -> next(memo) }

fun WebForm.with(vararg modifiers: (WebForm) -> WebForm) = modifiers.fold(this) { memo, next -> next(memo) }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy