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

io.javalin.core.JavalinServlet.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.core

import io.javalin.*
import io.javalin.core.util.*
import io.javalin.staticfiles.ResourceHandler
import java.io.InputStream
import java.util.zip.GZIPOutputStream
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

class JavalinServlet(
        val javalin: Javalin,
        val matcher: PathMatcher,
        val exceptionMapper: ExceptionMapper,
        val errorMapper: ErrorMapper,
        val debugLogging: Boolean,
        val requestLogger: RequestLogger?,
        val dynamicGzipEnabled: Boolean,
        val autogeneratedEtagsEnabled: Boolean,
        val defaultContentType: String,
        val maxRequestCacheBodySize: Long,
        val prefer405over404: Boolean,
        val singlePageHandler: SinglePageHandler,
        val resourceHandler: ResourceHandler?) {

    fun service(servletRequest: HttpServletRequest, res: HttpServletResponse) {

        val req = CachedRequestWrapper(servletRequest, maxRequestCacheBodySize) // cached for reading multiple times
        val type = HandlerType.fromServletRequest(req)
        val requestUri = req.requestURI
        val ctx = Context(req, res, javalin)

        fun tryWithExceptionMapper(func: () -> Unit) = exceptionMapper.catchException(ctx, func)

        fun tryBeforeAndEndpointHandlers() = tryWithExceptionMapper {
            matcher.findEntries(HandlerType.BEFORE, requestUri).forEach { entry ->
                entry.handler.handle(ContextUtil.update(ctx, entry, requestUri))
            }
            matcher.findEntries(type, requestUri).forEach { entry ->
                entry.handler.handle(ContextUtil.update(ctx, entry, requestUri))
                return@tryWithExceptionMapper // return after first match
            }
            if (type == HandlerType.HEAD && hasGetHandlerMapped(requestUri)) {
                return@tryWithExceptionMapper // return 200, there is a get handler
            }
            if (type == HandlerType.HEAD || type == HandlerType.GET) { // let Jetty check for static resources
                if (resourceHandler?.handle(req, res) == true) return@tryWithExceptionMapper
                if (singlePageHandler.handle(ctx)) return@tryWithExceptionMapper
            }
            val availableHandlerTypes = MethodNotAllowedUtil.findAvailableHttpHandlerTypes(matcher, requestUri)
            if (prefer405over404 && availableHandlerTypes.isNotEmpty()) {
                throw MethodNotAllowedResponse(details = MethodNotAllowedUtil.getAvailableHandlerTypes(ctx, availableHandlerTypes))
            }
            throw NotFoundResponse()
        }

        fun tryErrorHandlers() = tryWithExceptionMapper {
            errorMapper.handle(ctx.status(), ctx)
        }

        fun tryAfterHandlers() = tryWithExceptionMapper {
            matcher.findEntries(HandlerType.AFTER, requestUri).forEach { entry ->
                entry.handler.handle(ContextUtil.update(ctx, entry, requestUri))
            }
        }

        fun writeResult(res: HttpServletResponse) { // can be sync or async
            if (res.isCommitted || ctx.resultStream() == null) return // nothing to write
            val resultStream = ctx.resultStream()!!
            if (res.getHeader(Header.ETAG) != null || (autogeneratedEtagsEnabled && type == HandlerType.GET)) {
                val serverEtag = res.getHeader(Header.ETAG) ?: Util.getChecksumAndReset(resultStream) // calculate if not set
                res.setHeader(Header.ETAG, serverEtag)
                if (serverEtag == req.getHeader(Header.IF_NONE_MATCH)) {
                    res.status = 304
                    return // don't write body
                }
            }
            if (gzipShouldBeDone(ctx)) {
                GZIPOutputStream(res.outputStream, true).use { gzippedStream ->
                    res.setHeader(Header.CONTENT_ENCODING, "gzip")
                    resultStream.copyTo(gzippedStream)
                }
                resultStream.close()
                return
            }
            resultStream.copyTo(res.outputStream) // no gzip
            resultStream.close()
        }

        fun logRequest() {
            if (requestLogger != null) {
                requestLogger.handle(ctx, LogUtil.executionTimeMs(ctx))
            } else if (debugLogging == true) {
                LogUtil.logRequestAndResponse(ctx, matcher)
            }
        }

        LogUtil.startTimer(ctx) // start request lifecycle
        ctx.header(Header.SERVER, "Javalin")
        ctx.contentType(defaultContentType)
        tryBeforeAndEndpointHandlers()
        if (ctx.resultFuture() == null) { // finish request synchronously
            tryErrorHandlers()
            tryAfterHandlers()
            writeResult(res)
            logRequest()
            return // sync lifecycle complete
        } else { // finish request asynchronously
            val asyncContext = req.startAsync()
            ctx.resultFuture()!!.exceptionally { throwable ->
                if (throwable is Exception) {
                    exceptionMapper.handle(throwable, ctx)
                }
                null
            }.thenAccept {
                when (it) {
                    is InputStream -> ctx.result(it)
                    is String -> ctx.result(it)
                }
                tryErrorHandlers()
                tryAfterHandlers()
                writeResult(asyncContext.response as HttpServletResponse)
                logRequest()
                asyncContext.complete() // async lifecycle complete
            }
        }
    }

    private fun hasGetHandlerMapped(requestUri: String) = matcher.findEntries(HandlerType.GET, requestUri).isNotEmpty()

    private fun gzipShouldBeDone(ctx: Context) = dynamicGzipEnabled
            && ctx.resultStream()?.available() ?: 0 > 1500 // mtu is apparently ~1500 bytes
            && (ctx.header(Header.ACCEPT_ENCODING) ?: "").contains("gzip", ignoreCase = true)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy