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