universe@184: /* universe@184: * Copyright 2021 Mike Becker. All rights reserved. universe@184: * universe@184: * Redistribution and use in source and binary forms, with or without universe@184: * modification, are permitted provided that the following conditions are met: universe@184: * universe@184: * 1. Redistributions of source code must retain the above copyright universe@184: * notice, this list of conditions and the following disclaimer. universe@184: * universe@184: * 2. Redistributions in binary form must reproduce the above copyright universe@184: * notice, this list of conditions and the following disclaimer in the universe@184: * documentation and/or other materials provided with the distribution. universe@184: * universe@184: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" universe@184: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE universe@184: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE universe@184: * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE universe@184: * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL universe@184: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR universe@184: * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER universe@184: * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, universe@184: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE universe@184: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. universe@184: */ universe@184: universe@184: package de.uapcore.lightpit universe@184: universe@184: import de.uapcore.lightpit.DataSourceProvider.Companion.SC_ATTR_NAME universe@184: import de.uapcore.lightpit.dao.DataAccessObject universe@184: import de.uapcore.lightpit.dao.createDataAccessObject universe@184: import java.sql.SQLException universe@184: import java.util.* universe@184: import javax.servlet.http.HttpServlet universe@184: import javax.servlet.http.HttpServletRequest universe@184: import javax.servlet.http.HttpServletResponse universe@184: universe@184: abstract class AbstractServlet : LoggingTrait, HttpServlet() { universe@184: universe@184: /** universe@184: * Contains the GET request mappings. universe@184: */ universe@184: private val getMappings = mutableMapOf() universe@184: universe@184: /** universe@184: * Contains the POST request mappings. universe@184: */ universe@184: private val postMappings = mutableMapOf() universe@184: universe@184: protected fun get(pattern: String, method: MappingMethod) { universe@184: getMappings[PathPattern(pattern)] = method universe@184: } universe@184: universe@184: protected fun post(pattern: String, method: MappingMethod) { universe@184: postMappings[PathPattern(pattern)] = method universe@184: } universe@184: universe@184: private fun notFound(http: HttpRequest, dao: DataAccessObject) { universe@184: http.response.sendError(HttpServletResponse.SC_NOT_FOUND) universe@184: } universe@184: universe@184: private fun findMapping( universe@184: mappings: Map, universe@184: req: HttpServletRequest universe@184: ): Pair { universe@184: val requestPath = sanitizedRequestPath(req) universe@184: val candidates = mappings.filter { it.key.matches(requestPath) } universe@184: return if (candidates.isEmpty()) { universe@184: Pair(PathPattern(requestPath), ::notFound) universe@184: } else { universe@184: if (candidates.size > 1) { universe@184: logger().warn("Ambiguous mapping for request path '{}'", requestPath) universe@184: } universe@184: candidates.entries.first().toPair() universe@184: } universe@184: } universe@184: universe@184: private fun invokeMapping( universe@184: mapping: Pair, universe@184: req: HttpServletRequest, universe@184: resp: HttpServletResponse, universe@184: dao: DataAccessObject universe@184: ) { universe@184: val params = mapping.first.obtainPathParameters(sanitizedRequestPath(req)) universe@184: val method = mapping.second universe@184: logger().trace("invoke {}", method) universe@184: method(HttpRequest(req, resp, params), dao) universe@184: } universe@184: universe@184: private fun sanitizedRequestPath(req: HttpServletRequest) = req.pathInfo ?: "/" universe@184: universe@184: private fun doProcess( universe@184: req: HttpServletRequest, universe@184: resp: HttpServletResponse, universe@184: mappings: Map universe@184: ) { universe@184: val session = req.session universe@184: universe@184: // the very first thing to do is to force UTF-8 universe@184: req.characterEncoding = "UTF-8" universe@184: universe@184: // choose the requested language as session language (if available) or fall back to english, otherwise universe@184: if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) { universe@184: val availableLanguages = availableLanguages() universe@184: val reqLocale = req.locale universe@184: val sessionLocale = if (availableLanguages.contains(reqLocale)) reqLocale else availableLanguages.first() universe@184: session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale) universe@184: logger().debug( universe@184: "Setting language for new session {}: {}", session.id, sessionLocale.displayLanguage universe@184: ) universe@184: } else { universe@184: val sessionLocale = session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale universe@184: resp.locale = sessionLocale universe@184: logger().trace("Continuing session {} with language {}", session.id, sessionLocale) universe@184: } universe@184: universe@184: // set some internal request attributes universe@184: val http = HttpRequest(req, resp) universe@184: val fullPath = req.servletPath + Optional.ofNullable(req.pathInfo).orElse("") universe@184: req.setAttribute(Constants.REQ_ATTR_BASE_HREF, http.baseHref) universe@184: req.setAttribute(Constants.REQ_ATTR_PATH, fullPath) universe@184: req.getHeader("Referer")?.let { universe@184: // TODO: add a sanity check to avoid link injection universe@184: req.setAttribute(Constants.REQ_ATTR_REFERER, it) universe@184: } universe@184: universe@184: // if this is an error path, bypass the normal flow universe@184: if (fullPath.startsWith("/error/")) { universe@184: http.styleSheets = listOf("error") universe@184: http.render("error") universe@184: return universe@184: } universe@184: universe@184: // obtain a connection and create the data access objects universe@184: val dsp = req.servletContext.getAttribute(SC_ATTR_NAME) as DataSourceProvider universe@184: val dialect = dsp.dialect universe@184: val ds = dsp.dataSource universe@184: if (ds == null) { universe@184: resp.sendError( universe@184: HttpServletResponse.SC_SERVICE_UNAVAILABLE, universe@184: "JNDI DataSource lookup failed. See log for details." universe@184: ) universe@184: return universe@184: } universe@184: try { universe@184: ds.connection.use { connection -> universe@184: val dao = createDataAccessObject(dialect, connection) universe@184: try { universe@184: connection.autoCommit = false universe@184: invokeMapping(findMapping(mappings, req), req, resp, dao) universe@184: connection.commit() universe@184: } catch (ex: SQLException) { universe@184: logger().warn("Database transaction failed (Code {}): {}", ex.errorCode, ex.message) universe@184: logger().debug("Details: ", ex) universe@184: resp.sendError( universe@184: HttpServletResponse.SC_INTERNAL_SERVER_ERROR, universe@184: "Unhandled Transaction Error - Code: " + ex.errorCode universe@184: ) universe@184: connection.rollback() universe@184: } universe@184: } universe@184: } catch (ex: SQLException) { universe@184: logger().error("Severe Database Exception (Code {}): {}", ex.errorCode, ex.message) universe@184: logger().debug("Details: ", ex) universe@184: resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.errorCode) universe@184: } universe@184: } universe@184: universe@184: override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { universe@184: doProcess(req, resp, getMappings) universe@184: } universe@184: universe@184: override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { universe@184: doProcess(req, resp, postMappings) universe@184: } universe@184: universe@184: protected fun availableLanguages(): List { universe@184: val langTags = servletContext.getInitParameter(Constants.CTX_ATTR_LANGUAGES)?.split(",")?.map(String::trim) ?: emptyList() universe@184: val locales = langTags.map(Locale::forLanguageTag).filter { it.language.isNotEmpty() } universe@184: return if (locales.isEmpty()) listOf(Locale.ENGLISH) else locales universe@184: } universe@184: universe@184: }