io.javalin.http.Context.kt Maven / Gradle / Ivy
The newest version!
/*
* Javalin - https://javalin.io
* Copyright 2017 David Åse
* Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
*/
package io.javalin.http
import io.javalin.core.security.BasicAuthCredentials
import io.javalin.core.util.Header
import io.javalin.core.validation.BodyValidator
import io.javalin.core.validation.Validator
import io.javalin.http.util.ContextUtil
import io.javalin.http.util.ContextUtil.throwPayloadTooLargeIfPayloadTooLarge
import io.javalin.http.util.CookieStore
import io.javalin.http.util.MultipartUtil
import io.javalin.http.util.SeekableWriter
import io.javalin.plugin.json.jsonMapper
import io.javalin.plugin.rendering.JavalinRenderer
import java.io.InputStream
import java.nio.charset.Charset
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicReference
import java.util.function.Consumer
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
/**
* Provides access to functions for handling the request and response
*
* @see Context in docs
*/
// don't suppress warnings, since annotated classes are ignored by dokka (yeah...)
open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: HttpServletResponse, internal val appAttributes: Map = mapOf()) {
// @formatter:off
@get:JvmSynthetic @set:JvmSynthetic internal var matchedPath = ""
@get:JvmSynthetic @set:JvmSynthetic internal var endpointHandlerPath = ""
@get:JvmSynthetic @set:JvmSynthetic internal var pathParamMap = mapOf()
@get:JvmSynthetic @set:JvmSynthetic internal var handlerType = HandlerType.BEFORE
@get:JvmSynthetic @set:JvmSynthetic internal var resultReference = AtomicReference(Result())
// @formatter:on
private val cookieStore by lazy { CookieStore(this.jsonMapper(), cookie(CookieStore.COOKIE_NAME)) }
private val characterEncoding by lazy { ContextUtil.getRequestCharset(this) ?: "UTF-8" }
private val body by lazy {
this.throwPayloadTooLargeIfPayloadTooLarge()
req.inputStream.readBytes()
}
/** Gets the handler type of the current handler */
fun handlerType(): HandlerType = handlerType
/** Gets an attribute from the Javalin instance serving the request */
fun appAttribute(key: String): T = appAttributes[key] as T
/**
* Gets cookie store value for specified key.
* @see Cookie store in docs
*/
fun cookieStore(key: String): T = cookieStore[key]
/**
* Sets cookie store value for specified key.
* Values are made available for other handlers, requests, and servers.
* @see Cookie store in docs
*/
fun cookieStore(key: String, value: Any) {
cookieStore[key] = value
cookie(cookieStore.serializeToCookie())
}
/**
* Clears cookie store in the context and from the response.
* @see Cookie store in docs
*/
fun clearCookieStore() {
cookieStore.clear()
removeCookie(CookieStore.COOKIE_NAME)
}
/**
* Gets the path that was used to match request (also includes before/after paths)
*/
fun matchedPath() = matchedPath
/**
* Gets the endpoint path that was used to match request (null in before, available in endpoint/after)
*/
fun endpointHandlerPath() = if (handlerType != HandlerType.BEFORE) {
endpointHandlerPath
} else {
throw IllegalStateException("Cannot access the endpoint handler path in a 'BEFORE' handler")
}
///////////////////////////////////////////////////////////////
// Request-ish methods
///////////////////////////////////////////////////////////////
/** Gets the request body as a [String]. */
fun body(): String = bodyAsBytes().toString(Charset.forName(characterEncoding))
/** Gets the request body as a [ByteArray].
* Calling this method returns the body as a [ByteArray]. If [io.javalin.core.JavalinConfig.maxRequestSize]
* is set and body is bigger than its value, a [io.javalin.http.HttpResponseException] is throw,
* with status 413 PAYLOAD_TOO_LARGE.
*/
fun bodyAsBytes(): ByteArray = body
/** Maps a JSON body to a Java/Kotlin class using the registered [io.javalin.plugin.json.JsonMapper] */
fun bodyAsClass(clazz: Class): T = jsonMapper().fromJsonString(body(), clazz)
/** Reified version of [bodyAsClass] (Kotlin only) */
inline fun bodyAsClass(): T = bodyAsClass(T::class.java)
/** Maps a JSON body to a Java/Kotlin class using the registered [io.javalin.plugin.json.JsonMapper] */
fun bodyStreamAsClass(clazz: Class): T = jsonMapper().fromJsonStream(req.inputStream, clazz)
/** Reified version of [bodyStreamAsClass] (Kotlin only) */
inline fun bodyStreamAsClass(): T = bodyStreamAsClass(T::class.java)
/** Gets the request body as a [InputStream] */
fun bodyAsInputStream(): InputStream = req.inputStream
/** Creates a typed [BodyValidator] for the body() value */
fun bodyValidator(clazz: Class) = BodyValidator(body(), clazz, this.jsonMapper())
/** Reified version of [bodyValidator] (Kotlin only) */
inline fun bodyValidator() = bodyValidator(T::class.java)
/** Gets first [UploadedFile] for the specified name, or null. */
fun uploadedFile(fileName: String): UploadedFile? = uploadedFiles(fileName).firstOrNull()
/** Gets a list of [UploadedFile]s for the specified name, or empty list. */
fun uploadedFiles(fileName: String): List {
return if (isMultipartFormData()) MultipartUtil.getUploadedFiles(req, fileName) else listOf()
}
/** Gets a list of [UploadedFile]s, or empty list. */
fun uploadedFiles(): List {
return if (isMultipartFormData()) MultipartUtil.getUploadedFiles(req) else listOf()
}
/** Gets a form param if it exists, else null */
fun formParam(key: String): String? = formParams(key).firstOrNull()
/** Creates a typed [Validator] for the formParam() value */
fun formParamAsClass(key: String, clazz: Class) = Validator.create(clazz, formParam(key), key)
/** Reified version of [formParamAsClass] (Kotlin only) */
inline fun formParamAsClass(key: String) = formParamAsClass(key, T::class.java)
/** Gets a list of form params for the specified key, or empty list. */
fun formParams(key: String): List = formParamMap()[key] ?: emptyList()
/** using an additional map lazily so no new objects are created whenever ctx.formParam*() is called */
private val formParams by lazy {
if (isMultipartFormData()) MultipartUtil.getFieldMap(req)
else ContextUtil.splitKeyValueStringAndGroupByKey(body(), characterEncoding)
}
/** Gets a map with all the form param keys and values. */
fun formParamMap(): Map> = formParams
/**
* Gets a path param by name (ex: pathParam("param").
*
* Ex: If the handler path is /users/{user-id},
* and a browser GETs /users/123,
* pathParam("user-id") will return "123"
*/
fun pathParam(key: String): String = ContextUtil.pathParamOrThrow(pathParamMap, key, matchedPath)
/** Creates a typed [Validator] for the pathParam() value */
fun pathParamAsClass(key: String, clazz: Class) = Validator.create(clazz, pathParam(key), key)
/** Reified version of [pathParamAsClass] (Kotlin only) */
inline fun pathParamAsClass(key: String) = pathParamAsClass(key, T::class.java)
/** Gets a map of all the [pathParamAsClass] keys and values. */
fun pathParamMap(): Map = Collections.unmodifiableMap(pathParamMap)
/**
* Checks whether or not basic-auth credentials from the request exists.
*
* Returns a Boolean which is true if there is an Authorization header with
* Basic auth credentials. Returns false otherwise.
*/
fun basicAuthCredentialsExist(): Boolean = ContextUtil.hasBasicAuthCredentials(header(Header.AUTHORIZATION))
/**
* Gets basic-auth credentials from the request, or throws.
*
* Returns a wrapper object [BasicAuthCredentials] which contains the
* Base64 decoded username and password from the Authorization header.
*/
fun basicAuthCredentials(): BasicAuthCredentials = ContextUtil.getBasicAuthCredentials(header(Header.AUTHORIZATION))
/** Sets an attribute on the request. Attributes are available to other handlers in the request lifecycle */
fun attribute(key: String, value: Any?) = req.setAttribute(key, value)
/** Gets the specified attribute from the request. */
fun attribute(key: String): T? = req.getAttribute(key) as? T
/** Gets a map with all the attribute keys and values on the request. */
fun attributeMap(): Map = req.attributeNames.asSequence().associateWith { attribute(it) as Any? }
/** Gets the request content length. */
fun contentLength(): Int = req.contentLength
/** Gets the request content type, or null. */
fun contentType(): String? = req.contentType
/** Gets a request cookie by name, or null. */
fun cookie(name: String): String? = req.cookies?.find { name == it.name }?.value
/** Gets a map with all the cookie keys and values on the request. */
fun cookieMap(): Map = req.cookies?.associate { it.name to it.value } ?: emptyMap()
/** Gets a request header by name, or null. */
fun header(header: String): String? = req.getHeader(header)
/** Creates a typed [Validator] for the header() value */
fun headerAsClass(header: String, clazz: Class): Validator = Validator.create(clazz, header(header), header)
/** Reified version of [headerAsClass] (Kotlin only) */
inline fun headerAsClass(header: String) = headerAsClass(header, T::class.java)
/** Gets a map with all the header keys and values on the request. */
fun headerMap(): Map = req.headerNames.asSequence().associateWith { header(it)!! }
/** Gets the request host, or null. */
fun host(): String? = contextResolver().host.invoke(this)
/** Gets the request ip. */
fun ip(): String = contextResolver().ip.invoke(this)
/** Returns true if request is multipart. */
fun isMultipart(): Boolean = header(Header.CONTENT_TYPE)?.lowercase(Locale.ROOT)?.contains("multipart/") == true
/** Returns true if request is multipart/form-data. */
fun isMultipartFormData(): Boolean = header(Header.CONTENT_TYPE)?.lowercase(Locale.ROOT)?.contains("multipart/form-data") == true
/** Gets the request method. */
fun method(): String = req.method
/** Gets the request path. */
fun path(): String = req.requestURI
/** Gets the request port. */
fun port(): Int = req.serverPort
/** Gets the request protocol. */
fun protocol(): String = req.protocol
/** Gets a query param if it exists, else null */
fun queryParam(key: String): String? = queryParams(key).firstOrNull()
/** Creates a typed [Validator] for the queryParam() value */
fun queryParamAsClass(key: String, clazz: Class) = Validator.create(clazz, queryParam(key), key)
/** Reified version of [queryParamAsClass] (Kotlin only) */
inline fun queryParamAsClass(key: String) = queryParamAsClass(key, T::class.java)
/** Gets a list of query params for the specified key, or empty list. */
fun queryParams(key: String): List = queryParamMap()[key] ?: emptyList()
/** using an additional map lazily so no new objects are created whenever ctx.formParam*() is called */
private val queryParams by lazy {
ContextUtil.splitKeyValueStringAndGroupByKey(queryString() ?: "", characterEncoding)
}
/** Gets a map with all the query param keys and values. */
fun queryParamMap(): Map> = queryParams
/** Gets the request query string, or null. */
fun queryString(): String? = req.queryString
/** Gets the request scheme. */
fun scheme(): String = contextResolver().scheme.invoke(this)
/** Sets an attribute for the user session. */
fun sessionAttribute(key: String, value: Any?) = req.session.setAttribute(key, value)
/** Gets specified attribute from the user session, or null. */
fun sessionAttribute(key: String): T? = req.getSession(false)?.getAttribute(key) as? T
fun consumeSessionAttribute(key: String) = sessionAttribute(key).also { this.sessionAttribute(key, null) }
/** Sets an attribute for the user session, and caches it on the request */
fun cachedSessionAttribute(key: String, value: Any?) = ContextUtil.cacheAndSetSessionAttribute(key, value, req)
/** Gets specified attribute from the request attribute cache, or the user session, or null. */
fun cachedSessionAttribute(key: String): T? = ContextUtil.getCachedRequestAttributeOrSessionAttribute(key, req)
/** Gets specified attribute from the request attribute cache, or the user session, or computes the value from callback. */
fun cachedSessionAttributeOrCompute(key: String, callback: (Context) -> T): T? = ContextUtil.cachedSessionAttributeOrCompute(callback, key, this)
/** Gets a map of all the attributes in the user session. */
fun sessionAttributeMap(): Map = req.session.attributeNames.asSequence().associateWith { sessionAttribute(it) }
/** Gets the request url. */
fun url(): String = contextResolver().url.invoke(this)
/** Gets the full request url, including query string (if present) */
fun fullUrl(): String = contextResolver().fullUrl.invoke(this)
/** Gets the request context path. */
fun contextPath(): String = req.contextPath
/** Gets the request user agent, or null. */
fun userAgent(): String? = req.getHeader(Header.USER_AGENT)
///////////////////////////////////////////////////////////////
// Response-ish methods
///////////////////////////////////////////////////////////////
/** Gets the current response [Charset]. */
private fun responseCharset() = try {
Charset.forName(res.characterEncoding)
} catch (e: Exception) {
Charset.defaultCharset()
}
/**
* Sets context result to the specified [String].
* Will overwrite the current result if there is one.
*/
fun result(resultString: String) = result(resultString.byteInputStream(responseCharset()))
/**
* Sets context result to the specified array of bytes.
* Will overwrite the current result if there is one.
*/
fun result(resultBytes: ByteArray) = result(resultBytes.inputStream())
/** Gets the current [resultReference] as a [String] (if possible), and reset the underlying stream */
fun resultString() = ContextUtil.readAndResetStreamIfPossible(resultStream(), responseCharset())
/**
* Sets context result to the specified [InputStream].
* Will overwrite the current result if there is one.
*/
fun result(resultStream: InputStream): Context {
runCatching { resultStream()?.close() } // avoid memory leaks for multiple result() calls
return this.future(CompletableFuture.completedFuture(resultStream))
}
/** Writes the specified inputStream as a seekable stream */
@JvmOverloads
fun seekableStream(inputStream: InputStream, contentType: String, size: Long = inputStream.available().toLong()) =
SeekableWriter.write(this, inputStream, contentType, size)
fun resultStream(): InputStream? = resultReference.get().let { result ->
result.future.takeIf { it.isDone }?.get() as InputStream? ?: result.previous
}
/** The default callback (used if no callback is provided) can be configured through [ContextResolver.defaultFutureCallback] */
@JvmOverloads
fun future(future: CompletableFuture<*>, callback: Consumer? = null): Context {
resultReference.updateAndGet { oldResult ->
oldResult.future.cancel(true)
Result(oldResult.previous, future, callback)
}
return this
}
/** Gets the current context result as a [CompletableFuture] (if set). */
fun resultFuture(): CompletableFuture<*>? = resultReference.get().future
/** Sets response content type to specified [String] value. */
fun contentType(contentType: String): Context {
res.contentType = contentType
return this
}
/** Sets response content type to specified [ContentType] value. */
fun contentType(contentType: ContentType): Context =
contentType(contentType.mimeType)
/** Sets response header by name and value. */
fun header(name: String, value: String): Context {
res.setHeader(name, value)
return this
}
/** Sets the response status code and redirects to the specified location. */
@JvmOverloads
fun redirect(location: String, httpStatusCode: Int = HttpServletResponse.SC_MOVED_TEMPORARILY) {
res.setHeader(Header.LOCATION, location)
status(httpStatusCode)
if (handlerType == HandlerType.BEFORE) {
throw RedirectResponse(httpStatusCode)
}
}
/** Sets the response status. */
fun status(httpCode: HttpCode): Context =
status(httpCode.status)
/** Sets the response status. */
fun status(statusCode: Int): Context {
res.status = statusCode
return this
}
/** Gets the response status. */
fun status(): Int = res.status
/** Sets a cookie with name, value, and (overloaded) max-age. */
@JvmOverloads
fun cookie(name: String, value: String, maxAge: Int = -1) = cookie(Cookie(name = name, value = value, maxAge = maxAge))
/** Sets a Cookie. */
fun cookie(cookie: Cookie): Context {
res.setJavalinCookie(cookie)
return this
}
/** Removes cookie specified by name and path (optional). */
@JvmOverloads
fun removeCookie(name: String, path: String? = "/"): Context {
res.addCookie(javax.servlet.http.Cookie(name, "").apply {
this.path = path
this.maxAge = 0
})
return this
}
/** Sets context result to specified html string and sets content-type to text/html. */
fun html(html: String): Context = contentType(ContentType.TEXT_HTML).result(html)
/**
* Serializes object to a JSON-string using the registered [io.javalin.plugin.json.JsonMapper] and sets it as the context result.
* Also sets content type to application/json.
*/
fun json(obj: Any): Context = contentType(ContentType.APPLICATION_JSON).result(jsonMapper().toJsonString(obj))
/**
* Serializes object to a JSON-stream using the registered [io.javalin.plugin.json.JsonMapper] and sets it as the context result.
* Also sets content type to application/json.
*/
fun jsonStream(obj: Any): Context = contentType(ContentType.APPLICATION_JSON).result(jsonMapper().toJsonStream(obj))
/**
* Renders a file with specified values and sets it as the context result.
* Also sets content-type to text/html.
* Determines the correct rendering-function based on the file extension.
*/
@JvmOverloads
fun render(filePath: String, model: Map = mutableMapOf()): Context {
return html(JavalinRenderer.renderBasedOnExtension(filePath, model, this))
}
}