src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt

changeset 184
e8eecee6aadf
child 208
785820da6485
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt	Fri Apr 02 11:59:14 2021 +0200
     1.3 @@ -0,0 +1,182 @@
     1.4 +/*
     1.5 + * Copyright 2021 Mike Becker. All rights reserved.
     1.6 + *
     1.7 + * Redistribution and use in source and binary forms, with or without
     1.8 + * modification, are permitted provided that the following conditions are met:
     1.9 + *
    1.10 + * 1. Redistributions of source code must retain the above copyright
    1.11 + * notice, this list of conditions and the following disclaimer.
    1.12 + *
    1.13 + * 2. Redistributions in binary form must reproduce the above copyright
    1.14 + * notice, this list of conditions and the following disclaimer in the
    1.15 + * documentation and/or other materials provided with the distribution.
    1.16 + *
    1.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    1.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    1.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    1.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    1.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    1.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    1.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    1.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    1.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    1.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    1.27 + */
    1.28 +
    1.29 +package de.uapcore.lightpit
    1.30 +
    1.31 +import de.uapcore.lightpit.DataSourceProvider.Companion.SC_ATTR_NAME
    1.32 +import de.uapcore.lightpit.dao.DataAccessObject
    1.33 +import de.uapcore.lightpit.dao.createDataAccessObject
    1.34 +import java.sql.SQLException
    1.35 +import java.util.*
    1.36 +import javax.servlet.http.HttpServlet
    1.37 +import javax.servlet.http.HttpServletRequest
    1.38 +import javax.servlet.http.HttpServletResponse
    1.39 +
    1.40 +abstract class AbstractServlet : LoggingTrait, HttpServlet() {
    1.41 +
    1.42 +    /**
    1.43 +     * Contains the GET request mappings.
    1.44 +     */
    1.45 +    private val getMappings = mutableMapOf<PathPattern, MappingMethod>()
    1.46 +
    1.47 +    /**
    1.48 +     * Contains the POST request mappings.
    1.49 +     */
    1.50 +    private val postMappings = mutableMapOf<PathPattern, MappingMethod>()
    1.51 +
    1.52 +    protected fun get(pattern: String, method: MappingMethod) {
    1.53 +        getMappings[PathPattern(pattern)] = method
    1.54 +    }
    1.55 +
    1.56 +    protected fun post(pattern: String, method: MappingMethod) {
    1.57 +        postMappings[PathPattern(pattern)] = method
    1.58 +    }
    1.59 +
    1.60 +    private fun notFound(http: HttpRequest, dao: DataAccessObject) {
    1.61 +        http.response.sendError(HttpServletResponse.SC_NOT_FOUND)
    1.62 +    }
    1.63 +
    1.64 +    private fun findMapping(
    1.65 +        mappings: Map<PathPattern, MappingMethod>,
    1.66 +        req: HttpServletRequest
    1.67 +    ): Pair<PathPattern, MappingMethod> {
    1.68 +        val requestPath = sanitizedRequestPath(req)
    1.69 +        val candidates = mappings.filter { it.key.matches(requestPath) }
    1.70 +        return if (candidates.isEmpty()) {
    1.71 +            Pair(PathPattern(requestPath), ::notFound)
    1.72 +        } else {
    1.73 +            if (candidates.size > 1) {
    1.74 +                logger().warn("Ambiguous mapping for request path '{}'", requestPath)
    1.75 +            }
    1.76 +            candidates.entries.first().toPair()
    1.77 +        }
    1.78 +    }
    1.79 +
    1.80 +    private fun invokeMapping(
    1.81 +        mapping: Pair<PathPattern, MappingMethod>,
    1.82 +        req: HttpServletRequest,
    1.83 +        resp: HttpServletResponse,
    1.84 +        dao: DataAccessObject
    1.85 +    ) {
    1.86 +        val params = mapping.first.obtainPathParameters(sanitizedRequestPath(req))
    1.87 +        val method = mapping.second
    1.88 +        logger().trace("invoke {}", method)
    1.89 +        method(HttpRequest(req, resp, params), dao)
    1.90 +    }
    1.91 +
    1.92 +    private fun sanitizedRequestPath(req: HttpServletRequest) = req.pathInfo ?: "/"
    1.93 +
    1.94 +    private fun doProcess(
    1.95 +        req: HttpServletRequest,
    1.96 +        resp: HttpServletResponse,
    1.97 +        mappings: Map<PathPattern, MappingMethod>
    1.98 +    ) {
    1.99 +        val session = req.session
   1.100 +
   1.101 +        // the very first thing to do is to force UTF-8
   1.102 +        req.characterEncoding = "UTF-8"
   1.103 +
   1.104 +        // choose the requested language as session language (if available) or fall back to english, otherwise
   1.105 +        if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) {
   1.106 +            val availableLanguages = availableLanguages()
   1.107 +            val reqLocale = req.locale
   1.108 +            val sessionLocale = if (availableLanguages.contains(reqLocale)) reqLocale else availableLanguages.first()
   1.109 +            session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale)
   1.110 +            logger().debug(
   1.111 +                "Setting language for new session {}: {}", session.id, sessionLocale.displayLanguage
   1.112 +            )
   1.113 +        } else {
   1.114 +            val sessionLocale = session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale
   1.115 +            resp.locale = sessionLocale
   1.116 +            logger().trace("Continuing session {} with language {}", session.id, sessionLocale)
   1.117 +        }
   1.118 +
   1.119 +        // set some internal request attributes
   1.120 +        val http = HttpRequest(req, resp)
   1.121 +        val fullPath = req.servletPath + Optional.ofNullable(req.pathInfo).orElse("")
   1.122 +        req.setAttribute(Constants.REQ_ATTR_BASE_HREF, http.baseHref)
   1.123 +        req.setAttribute(Constants.REQ_ATTR_PATH, fullPath)
   1.124 +        req.getHeader("Referer")?.let {
   1.125 +            // TODO: add a sanity check to avoid link injection
   1.126 +            req.setAttribute(Constants.REQ_ATTR_REFERER, it)
   1.127 +        }
   1.128 +
   1.129 +        // if this is an error path, bypass the normal flow
   1.130 +        if (fullPath.startsWith("/error/")) {
   1.131 +            http.styleSheets = listOf("error")
   1.132 +            http.render("error")
   1.133 +            return
   1.134 +        }
   1.135 +
   1.136 +        // obtain a connection and create the data access objects
   1.137 +        val dsp = req.servletContext.getAttribute(SC_ATTR_NAME) as DataSourceProvider
   1.138 +        val dialect = dsp.dialect
   1.139 +        val ds = dsp.dataSource
   1.140 +        if (ds == null) {
   1.141 +            resp.sendError(
   1.142 +                HttpServletResponse.SC_SERVICE_UNAVAILABLE,
   1.143 +                "JNDI DataSource lookup failed. See log for details."
   1.144 +            )
   1.145 +            return
   1.146 +        }
   1.147 +        try {
   1.148 +            ds.connection.use { connection ->
   1.149 +                val dao = createDataAccessObject(dialect, connection)
   1.150 +                try {
   1.151 +                    connection.autoCommit = false
   1.152 +                    invokeMapping(findMapping(mappings, req), req, resp, dao)
   1.153 +                    connection.commit()
   1.154 +                } catch (ex: SQLException) {
   1.155 +                    logger().warn("Database transaction failed (Code {}): {}", ex.errorCode, ex.message)
   1.156 +                    logger().debug("Details: ", ex)
   1.157 +                    resp.sendError(
   1.158 +                        HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
   1.159 +                        "Unhandled Transaction Error - Code: " + ex.errorCode
   1.160 +                    )
   1.161 +                    connection.rollback()
   1.162 +                }
   1.163 +            }
   1.164 +        } catch (ex: SQLException) {
   1.165 +            logger().error("Severe Database Exception (Code {}): {}", ex.errorCode, ex.message)
   1.166 +            logger().debug("Details: ", ex)
   1.167 +            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.errorCode)
   1.168 +        }
   1.169 +    }
   1.170 +
   1.171 +    override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
   1.172 +        doProcess(req, resp, getMappings)
   1.173 +    }
   1.174 +
   1.175 +    override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) {
   1.176 +        doProcess(req, resp, postMappings)
   1.177 +    }
   1.178 +
   1.179 +    protected fun availableLanguages(): List<Locale> {
   1.180 +        val langTags = servletContext.getInitParameter(Constants.CTX_ATTR_LANGUAGES)?.split(",")?.map(String::trim) ?: emptyList()
   1.181 +        val locales = langTags.map(Locale::forLanguageTag).filter { it.language.isNotEmpty() }
   1.182 +        return if (locales.isEmpty()) listOf(Locale.ENGLISH) else locales
   1.183 +    }
   1.184 +
   1.185 +}

mercurial