Sat, 06 Jan 2024 20:31:14 +0100
add language selection cookie - fixes #352
184 | 1 | /* |
2 | * Copyright 2021 Mike Becker. All rights reserved. | |
3 | * | |
4 | * Redistribution and use in source and binary forms, with or without | |
5 | * modification, are permitted provided that the following conditions are met: | |
6 | * | |
7 | * 1. Redistributions of source code must retain the above copyright | |
8 | * notice, this list of conditions and the following disclaimer. | |
9 | * | |
10 | * 2. Redistributions in binary form must reproduce the above copyright | |
11 | * notice, this list of conditions and the following disclaimer in the | |
12 | * documentation and/or other materials provided with the distribution. | |
13 | * | |
14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
17 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
18 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
19 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
20 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
21 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
22 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
24 | */ | |
25 | ||
26 | package de.uapcore.lightpit | |
27 | ||
28 | import de.uapcore.lightpit.DataSourceProvider.Companion.SC_ATTR_NAME | |
29 | import de.uapcore.lightpit.dao.DataAccessObject | |
30 | import de.uapcore.lightpit.dao.createDataAccessObject | |
298
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
31 | import jakarta.servlet.http.Cookie |
254
55ca6cafc3dd
#233 migrate to Jakarta EE and update dependencies
Mike Becker <universe@uap-core.de>
parents:
247
diff
changeset
|
32 | import jakarta.servlet.http.HttpServlet |
55ca6cafc3dd
#233 migrate to Jakarta EE and update dependencies
Mike Becker <universe@uap-core.de>
parents:
247
diff
changeset
|
33 | import jakarta.servlet.http.HttpServletRequest |
55ca6cafc3dd
#233 migrate to Jakarta EE and update dependencies
Mike Becker <universe@uap-core.de>
parents:
247
diff
changeset
|
34 | import jakarta.servlet.http.HttpServletResponse |
184 | 35 | import java.sql.SQLException |
36 | import java.util.* | |
37 | ||
247 | 38 | abstract class AbstractServlet : HttpServlet() { |
298
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
39 | |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
40 | companion object { |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
41 | const val LANGUAGE_COOKIE_NAME = "lpit_language" |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
42 | } |
247 | 43 | |
44 | protected val logger = MyLogger() | |
184 | 45 | |
46 | /** | |
47 | * Contains the GET request mappings. | |
48 | */ | |
49 | private val getMappings = mutableMapOf<PathPattern, MappingMethod>() | |
50 | ||
51 | /** | |
52 | * Contains the POST request mappings. | |
53 | */ | |
54 | private val postMappings = mutableMapOf<PathPattern, MappingMethod>() | |
55 | ||
56 | protected fun get(pattern: String, method: MappingMethod) { | |
57 | getMappings[PathPattern(pattern)] = method | |
58 | } | |
59 | ||
60 | protected fun post(pattern: String, method: MappingMethod) { | |
61 | postMappings[PathPattern(pattern)] = method | |
62 | } | |
63 | ||
64 | private fun notFound(http: HttpRequest, dao: DataAccessObject) { | |
65 | http.response.sendError(HttpServletResponse.SC_NOT_FOUND) | |
66 | } | |
67 | ||
68 | private fun findMapping( | |
69 | mappings: Map<PathPattern, MappingMethod>, | |
70 | req: HttpServletRequest | |
71 | ): Pair<PathPattern, MappingMethod> { | |
72 | val requestPath = sanitizedRequestPath(req) | |
73 | val candidates = mappings.filter { it.key.matches(requestPath) } | |
74 | return if (candidates.isEmpty()) { | |
75 | Pair(PathPattern(requestPath), ::notFound) | |
76 | } else { | |
77 | if (candidates.size > 1) { | |
247 | 78 | logger.warn("Ambiguous mapping for request path '{0}'", requestPath) |
184 | 79 | } |
80 | candidates.entries.first().toPair() | |
81 | } | |
82 | } | |
83 | ||
84 | private fun invokeMapping( | |
85 | mapping: Pair<PathPattern, MappingMethod>, | |
86 | req: HttpServletRequest, | |
87 | resp: HttpServletResponse, | |
88 | dao: DataAccessObject | |
89 | ) { | |
90 | val params = mapping.first.obtainPathParameters(sanitizedRequestPath(req)) | |
91 | val method = mapping.second | |
247 | 92 | logger.trace("invoke {0}", method) |
184 | 93 | method(HttpRequest(req, resp, params), dao) |
94 | } | |
95 | ||
96 | private fun sanitizedRequestPath(req: HttpServletRequest) = req.pathInfo ?: "/" | |
97 | ||
98 | private fun doProcess( | |
99 | req: HttpServletRequest, | |
100 | resp: HttpServletResponse, | |
101 | mappings: Map<PathPattern, MappingMethod> | |
102 | ) { | |
103 | val session = req.session | |
104 | ||
105 | // the very first thing to do is to force UTF-8 | |
106 | req.characterEncoding = "UTF-8" | |
107 | ||
108 | // set some internal request attributes | |
109 | val http = HttpRequest(req, resp) | |
110 | val fullPath = req.servletPath + Optional.ofNullable(req.pathInfo).orElse("") | |
111 | req.setAttribute(Constants.REQ_ATTR_BASE_HREF, http.baseHref) | |
112 | req.setAttribute(Constants.REQ_ATTR_PATH, fullPath) | |
113 | req.getHeader("Referer")?.let { | |
114 | // TODO: add a sanity check to avoid link injection | |
115 | req.setAttribute(Constants.REQ_ATTR_REFERER, it) | |
116 | } | |
117 | ||
298
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
118 | // choose the requested language as session language (if available) |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
119 | if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) { |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
120 | // language selection stored in cookie |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
121 | val cookieLocale = cookieLanguage(http) |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
122 | |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
123 | // if no cookie, fall back to request locale a.k.a "Browser Language" |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
124 | val reqLocale = cookieLocale ?: req.locale |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
125 | |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
126 | val availableLanguages = availableLanguages() |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
127 | val sessionLocale = if (availableLanguages.contains(reqLocale)) reqLocale else availableLanguages.first() |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
128 | |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
129 | // select the language (this will also refresh the cookie max-age) |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
130 | selectLanguage(http, sessionLocale) |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
131 | |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
132 | logger.debug( |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
133 | "Setting language for new session {0}: {1}", session.id, sessionLocale.displayLanguage |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
134 | ) |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
135 | } else { |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
136 | val sessionLocale = session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
137 | resp.locale = sessionLocale |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
138 | logger.trace("Continuing session {0} with language {1}", session.id, sessionLocale) |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
139 | } |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
140 | |
184 | 141 | // if this is an error path, bypass the normal flow |
142 | if (fullPath.startsWith("/error/")) { | |
143 | http.styleSheets = listOf("error") | |
144 | http.render("error") | |
145 | return | |
146 | } | |
147 | ||
148 | // obtain a connection and create the data access objects | |
149 | val dsp = req.servletContext.getAttribute(SC_ATTR_NAME) as DataSourceProvider | |
150 | val dialect = dsp.dialect | |
151 | val ds = dsp.dataSource | |
152 | if (ds == null) { | |
153 | resp.sendError( | |
154 | HttpServletResponse.SC_SERVICE_UNAVAILABLE, | |
155 | "JNDI DataSource lookup failed. See log for details." | |
156 | ) | |
157 | return | |
158 | } | |
159 | try { | |
160 | ds.connection.use { connection -> | |
161 | val dao = createDataAccessObject(dialect, connection) | |
162 | try { | |
163 | connection.autoCommit = false | |
164 | invokeMapping(findMapping(mappings, req), req, resp, dao) | |
165 | connection.commit() | |
166 | } catch (ex: SQLException) { | |
247 | 167 | logger.warn("Database transaction failed (Code {0}): {1}", ex.errorCode, ex.message) |
168 | logger.debug("Details: ", ex) | |
184 | 169 | resp.sendError( |
170 | HttpServletResponse.SC_INTERNAL_SERVER_ERROR, | |
171 | "Unhandled Transaction Error - Code: " + ex.errorCode | |
172 | ) | |
173 | connection.rollback() | |
174 | } | |
175 | } | |
176 | } catch (ex: SQLException) { | |
247 | 177 | logger.error("Severe Database Exception (Code {0}): {1}", ex.errorCode, ex.message) |
178 | logger.debug("Details: ", ex) | |
184 | 179 | resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.errorCode) |
180 | } | |
181 | } | |
182 | ||
183 | override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { | |
184 | doProcess(req, resp, getMappings) | |
185 | } | |
186 | ||
187 | override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { | |
188 | doProcess(req, resp, postMappings) | |
189 | } | |
190 | ||
191 | protected fun availableLanguages(): List<Locale> { | |
192 | val langTags = servletContext.getInitParameter(Constants.CTX_ATTR_LANGUAGES)?.split(",")?.map(String::trim) ?: emptyList() | |
193 | val locales = langTags.map(Locale::forLanguageTag).filter { it.language.isNotEmpty() } | |
208
785820da6485
fixes response locale not set for new sessions
Mike Becker <universe@uap-core.de>
parents:
184
diff
changeset
|
194 | return locales.ifEmpty { listOf(Locale.ENGLISH) } |
184 | 195 | } |
196 | ||
298
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
197 | private fun cookieLanguage(http: HttpRequest): Locale? = |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
198 | http.request.cookies?.firstOrNull { c -> c.name == LANGUAGE_COOKIE_NAME } |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
199 | ?.runCatching {Locale.forLanguageTag(this.value)}?.getOrNull() |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
200 | |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
201 | protected fun selectLanguage(http: HttpRequest, locale: Locale) { |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
202 | http.response.locale = locale |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
203 | http.session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, locale) |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
204 | // delete cookie if language selection matches request locale, otherwise set cookie |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
205 | val cookie = Cookie(LANGUAGE_COOKIE_NAME, "") |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
206 | cookie.isHttpOnly = true |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
207 | cookie.path = http.request.contextPath |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
208 | if (http.request.locale.language == locale.language) { |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
209 | cookie.maxAge = 0 |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
210 | } else { |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
211 | cookie.value = locale.language |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
212 | cookie.maxAge = 2592000 // 30 days |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
213 | } |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
214 | http.response.addCookie(cookie) |
1275eb652008
add language selection cookie - fixes #352
Mike Becker <universe@uap-core.de>
parents:
254
diff
changeset
|
215 | } |
184 | 216 | } |