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 +}