HttpContext.kt

package com.hexagontk.http.handlers

import com.hexagontk.handlers.Context
import com.hexagontk.core.assertEnabled
import com.hexagontk.core.media.TEXT_EVENT_STREAM
import com.hexagontk.core.media.TEXT_PLAIN
import com.hexagontk.core.toText
import com.hexagontk.handlers.Handler
import com.hexagontk.http.model.*
import com.hexagontk.http.model.INTERNAL_SERVER_ERROR_500
import com.hexagontk.http.model.ServerEvent
import com.hexagontk.http.model.ws.WsSession
import java.net.URI
import java.security.cert.X509Certificate
import java.util.concurrent.Flow.Publisher

class HttpContext(
    override var event: HttpCall,
    override var predicate: (Context<HttpCall>) -> Boolean,
    override var nextHandlers: Array<Handler<HttpCall>> = emptyArray(),
    override var nextHandler: Int = 0,
    override var exception: Exception? = null,
    override var attributes: Map<*, *> = emptyMap<Any, Any>(),
    override var handled: Boolean = false,
): Context<HttpCall> {
    val request: HttpRequestPort get() = event.request
    val response: HttpResponsePort get() = event.response

    val method: HttpMethod get() = request.method
    val protocol: HttpProtocol get() = request.protocol
    val host: String get() = request.host
    val port: Int get() = request.port
    val path: String get() = request.path
    val queryParameters: Parameters get() = request.queryParameters
    val parts: List<HttpPart> get() = request.parts
    val formParameters: Parameters get() = request.formParameters
    val accept: List<ContentType> get() = request.accept
    val authorization: Authorization? get() = request.authorization
    val certificateChain: List<X509Certificate> get() = request.certificateChain

    val partsMap: Map<String, HttpPart> get() = request.partsMap()
    val uri: URI get() = request.uri()
    val userAgent: String? get() = request.userAgent()
    val referer: String? get() = request.referer()
    val origin: String? get() = request.origin()
    val certificate: X509Certificate? get() = request.certificate()

    val status: Int get() = response.status

    val pathParameters: Map<String, String> get() {
        val httpHandler = predicate as HttpPredicate
        val pattern = httpHandler.pathPattern

        if (assertEnabled)
            check(!pattern.prefix) { "Loading path parameters not allowed for paths" }

        return pattern.extractParameters(request.path)
    }

    constructor(context: Context<HttpCall>) : this(
        event = context.event,
        predicate = context.predicate,
        nextHandlers = context.nextHandlers,
        nextHandler = context.nextHandler,
        exception = context.exception,
        attributes = context.attributes,
    )

    constructor(
        event: HttpCall,
        predicate: (Context<HttpCall>) -> Boolean,
        nextHandlers: List<Handler<HttpCall>>,
        nextHandler: Int = 0,
        exception: Exception? = null,
        attributes: Map<*, *> = emptyMap<Any, Any>(),
        handled: Boolean = false,
    ) : this(
        event, predicate, nextHandlers.toTypedArray(), nextHandler, exception, attributes, handled
    )

    constructor(
        request: HttpRequestPort = HttpRequest(),
        response: HttpResponsePort = HttpResponse(),
        predicate: HttpPredicate = HttpPredicate(),
        attributes: Map<*, *> = emptyMap<Any, Any>(),
    ) : this(HttpCall(request, response), predicate, attributes = attributes)

    override fun next(): HttpContext {
        for (index in nextHandler until nextHandlers.size) {
            val handler = nextHandlers[index]
            val p = handler.predicate
            if (handler is OnHandler) {
                if ((!handled) && p(this))
                    return handler.process(with(predicate = p, nextHandler = index + 1)) as HttpContext
            }
            else {
                if (p(this))
                    return handler.process(with(predicate = p, nextHandler = index + 1)) as HttpContext
            }
        }

        return this
    }

    override fun with(
        event: HttpCall,
        predicate: (Context<HttpCall>) -> Boolean,
        nextHandlers: Array<Handler<HttpCall>>,
        nextHandler: Int,
        exception: Exception?,
        attributes: Map<*, *>,
        handled: Boolean,
    ): Context<HttpCall> =
        apply {
            this.event = event
            this.predicate = predicate
            this.nextHandlers = nextHandlers
            this.nextHandler = nextHandler
            this.exception = exception
            this.attributes = attributes
            this.handled = handled
        }

    fun unauthorized(
        body: Any = response.body,
        headers: Headers = response.headers,
        contentType: ContentType? = response.contentType,
        cookies: List<Cookie> = response.cookies,
        attributes: Map<*, *> = this.attributes,
    ): HttpContext =
        send(UNAUTHORIZED_401, body, headers, contentType, cookies, attributes)

    fun forbidden(
        body: Any = response.body,
        headers: Headers = response.headers,
        contentType: ContentType? = response.contentType,
        cookies: List<Cookie> = response.cookies,
        attributes: Map<*, *> = this.attributes,
    ): HttpContext =
        send(FORBIDDEN_403, body, headers, contentType, cookies, attributes)

    fun internalServerError(
        body: Any = response.body,
        headers: Headers = response.headers,
        contentType: ContentType? = response.contentType,
        cookies: List<Cookie> = response.cookies,
        attributes: Map<*, *> = this.attributes,
    ): HttpContext =
        send(INTERNAL_SERVER_ERROR_500, body, headers, contentType, cookies, attributes)

    fun serverError(
        status: Int,
        exception: Exception,
        headers: Headers = response.headers,
        attributes: Map<*, *> = this.attributes,
    ): HttpContext =
        send(
            status = status,
            body = exception.toText(),
            headers = headers,
            contentType = ContentType(TEXT_PLAIN),
            attributes = attributes,
        )

    fun internalServerError(
        exception: Exception,
        headers: Headers = response.headers,
        attributes: Map<*, *> = this.attributes,
    ): HttpContext =
        serverError(INTERNAL_SERVER_ERROR_500, exception, headers, attributes)

    fun ok(
        body: Any = response.body,
        headers: Headers = response.headers,
        contentType: ContentType? = response.contentType,
        cookies: List<Cookie> = response.cookies,
        attributes: Map<*, *> = this.attributes,
    ): HttpContext =
        send(OK_200, body, headers, contentType, cookies, attributes)

    fun sse(body: Publisher<ServerEvent>): HttpContext =
        ok(
            body = body,
            headers = response.headers + Header("cache-control", "no-cache"),
            contentType = ContentType(TEXT_EVENT_STREAM)
        )

    fun badRequest(
        body: Any = response.body,
        headers: Headers = response.headers,
        contentType: ContentType? = response.contentType,
        cookies: List<Cookie> = response.cookies,
        attributes: Map<*, *> = this.attributes,
    ): HttpContext =
        send(BAD_REQUEST_400, body, headers, contentType, cookies, attributes)

    fun notFound(
        body: Any = response.body,
        headers: Headers = response.headers,
        contentType: ContentType? = response.contentType,
        cookies: List<Cookie> = response.cookies,
        attributes: Map<*, *> = this.attributes,
    ): HttpContext =
        send(NOT_FOUND_404, body, headers, contentType, cookies, attributes)

    fun created(
        body: Any = response.body,
        headers: Headers = response.headers,
        contentType: ContentType? = response.contentType,
        cookies: List<Cookie> = response.cookies,
        attributes: Map<*, *> = this.attributes,
    ): HttpContext =
        send(CREATED_201, body, headers, contentType, cookies, attributes)

    fun redirect(
        status: Int,
        location: String,
        headers: Headers = response.headers,
        cookies: List<Cookie> = response.cookies,
        attributes: Map<*, *> = this.attributes,
    ): HttpContext =
        send(
            status,
            headers = headers + Header("location", location),
            cookies = cookies,
            attributes = attributes
        )

    fun found(
        location: String,
        headers: Headers = response.headers,
        cookies: List<Cookie> = response.cookies,
        attributes: Map<*, *> = this.attributes,
    ): HttpContext =
        redirect(FOUND_302, location, headers, cookies, attributes)

    fun accepted(
        onConnect: WsSession.() -> Unit = {},
        onBinary: WsSession.(data: ByteArray) -> Unit = {},
        onText: WsSession.(text: String) -> Unit = {},
        onPing: WsSession.(data: ByteArray) -> Unit = {},
        onPong: WsSession.(data: ByteArray) -> Unit = {},
        onClose: WsSession.(status: Int, reason: String) -> Unit = { _, _ -> },
    ): HttpContext =
        send(
            event.response.with(
                status = ACCEPTED_202,
                onConnect = onConnect,
                onBinary = onBinary,
                onText = onText,
                onPing = onPing,
                onPong = onPong,
                onClose = onClose,
            )
        )

    fun send(
        status: Int = response.status,
        body: Any = response.body,
        headers: Headers = response.headers,
        contentType: ContentType? = response.contentType,
        cookies: List<Cookie> = response.cookies,
        attributes: Map<*, *> = this.attributes,
    ): HttpContext =
        send(
            response.with(
                body = body,
                headers = headers,
                contentType = contentType,
                cookies = cookies,
                status = status,
            ),
            attributes
        )

    fun send(response: HttpResponsePort, attributes: Map<*, *> = this.attributes): HttpContext =
        apply {
            this.event.response = response
            this.attributes = attributes
        }

    fun send(request: HttpRequestPort, attributes: Map<*, *> = this.attributes): HttpContext =
        apply {
            this.event.request = request
            this.attributes = attributes
        }

    fun receive(
        body: Any = request.body,
        headers: Headers = request.headers,
        contentType: ContentType? = request.contentType,
        accept: List<ContentType> = request.accept,
        cookies: List<Cookie> = request.cookies,
        attributes: Map<*, *> = this.attributes,
    ): HttpContext =
        send(
            request.with(
                body = body,
                headers = headers,
                contentType = contentType,
                accept = accept,
                cookies = cookies,
            ),
            attributes
        )
}