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@298: import jakarta.servlet.http.Cookie universe@254: import jakarta.servlet.http.HttpServlet universe@254: import jakarta.servlet.http.HttpServletRequest universe@254: import jakarta.servlet.http.HttpServletResponse universe@184: import java.sql.SQLException universe@184: import java.util.* universe@184: universe@247: abstract class AbstractServlet : HttpServlet() { universe@298: universe@298: companion object { universe@298: const val LANGUAGE_COOKIE_NAME = "lpit_language" universe@298: } universe@247: universe@247: protected val logger = MyLogger() 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@247: logger.warn("Ambiguous mapping for request path '{0}'", 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@247: logger.trace("invoke {0}", 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: // 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@298: // choose the requested language as session language (if available) universe@298: if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) { universe@298: // language selection stored in cookie universe@298: val cookieLocale = cookieLanguage(http) universe@298: universe@298: // if no cookie, fall back to request locale a.k.a "Browser Language" universe@298: val reqLocale = cookieLocale ?: req.locale universe@298: universe@298: val availableLanguages = availableLanguages() universe@298: val sessionLocale = if (availableLanguages.contains(reqLocale)) reqLocale else availableLanguages.first() universe@298: universe@298: // select the language (this will also refresh the cookie max-age) universe@298: selectLanguage(http, sessionLocale) universe@298: universe@298: logger.debug( universe@298: "Setting language for new session {0}: {1}", session.id, sessionLocale.displayLanguage universe@298: ) universe@298: } else { universe@298: val sessionLocale = session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale universe@298: resp.locale = sessionLocale universe@298: logger.trace("Continuing session {0} with language {1}", session.id, sessionLocale) universe@298: } universe@298: 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@247: logger.warn("Database transaction failed (Code {0}): {1}", ex.errorCode, ex.message) universe@247: 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@247: logger.error("Severe Database Exception (Code {0}): {1}", ex.errorCode, ex.message) universe@247: 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@208: return locales.ifEmpty { listOf(Locale.ENGLISH) } universe@184: } universe@184: universe@298: private fun cookieLanguage(http: HttpRequest): Locale? = universe@298: http.request.cookies?.firstOrNull { c -> c.name == LANGUAGE_COOKIE_NAME } universe@298: ?.runCatching {Locale.forLanguageTag(this.value)}?.getOrNull() universe@298: universe@298: protected fun selectLanguage(http: HttpRequest, locale: Locale) { universe@298: http.response.locale = locale universe@298: http.session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, locale) universe@298: // delete cookie if language selection matches request locale, otherwise set cookie universe@298: val cookie = Cookie(LANGUAGE_COOKIE_NAME, "") universe@298: cookie.isHttpOnly = true universe@298: cookie.path = http.request.contextPath universe@298: if (http.request.locale.language == locale.language) { universe@298: cookie.maxAge = 0 universe@298: } else { universe@298: cookie.value = locale.language universe@298: cookie.maxAge = 2592000 // 30 days universe@298: } universe@298: http.response.addCookie(cookie) universe@298: } universe@184: }