Tue, 03 Aug 2021 12:22:10 +0200
fixes response locale not set for new sessions
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 */
26 package de.uapcore.lightpit
28 import de.uapcore.lightpit.dao.DataAccessObject
29 import de.uapcore.lightpit.viewmodel.NavMenu
30 import de.uapcore.lightpit.viewmodel.View
31 import java.util.*
32 import javax.servlet.http.HttpServletRequest
33 import javax.servlet.http.HttpServletResponse
34 import javax.servlet.http.HttpSession
35 import kotlin.math.min
37 typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit
38 typealias PathParameters = Map<String, String>
40 class HttpRequest(
41 val request: HttpServletRequest,
42 val response: HttpServletResponse,
43 val pathParams: PathParameters = emptyMap()
44 ) {
45 val session: HttpSession = request.session
47 val remoteUser: String? = request.remoteUser
49 /**
50 * The name of the content page.
51 *
52 * @see Constants#REQ_ATTR_CONTENT_PAGE
53 */
54 var contentPage = ""
55 set(value) {
56 field = value
57 request.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(value))
58 }
60 /**
61 * The name of the content page.
62 *
63 * @see Constants#REQ_ATTR_PAGE_TITLE
64 */
65 var pageTitle = ""
66 set(value) {
67 field = value
68 request.setAttribute(Constants.REQ_ATTR_PAGE_TITLE, value)
69 }
71 /**
72 * A list of additional style sheets.
73 *
74 * @see Constants#REQ_ATTR_STYLESHEET
75 */
76 var styleSheets = emptyList<String>()
77 set(value) {
78 field = value
79 request.setAttribute(Constants.REQ_ATTR_STYLESHEET,
80 value.map { it.withExt(".css") }
81 )
82 }
84 /**
85 * A list of additional style sheets.
86 *
87 * @see Constants#REQ_ATTR_JAVASCRIPT
88 */
89 var javascript = ""
90 set(value) {
91 field = value
92 request.setAttribute(Constants.REQ_ATTR_JAVASCRIPT,
93 value.withExt(".js")
94 )
95 }
97 /**
98 * The name of the navigation menu JSP.
99 *
100 * @see Constants#REQ_ATTR_NAVIGATION
101 */
102 var navigationMenu: NavMenu? = null
103 set(value) {
104 field = value
105 request.setAttribute(Constants.REQ_ATTR_NAVIGATION, navigationMenu)
106 }
108 var redirectLocation: String? = null
109 set(value) {
110 field = value
111 if (value == null) {
112 request.removeAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION)
113 } else {
114 request.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, baseHref + value)
115 }
116 }
118 var feedPath: String? = null
119 set(value) {
120 field = value
121 if (value == null) {
122 request.removeAttribute(Constants.REQ_ATTR_FEED_HREF)
123 } else {
124 request.setAttribute(Constants.REQ_ATTR_FEED_HREF, baseHref + value)
125 }
126 }
128 /**
129 * The view object.
130 *
131 * @see Constants#REQ_ATTR_VIEWMODEL
132 */
133 var view: View? = null
134 set(value) {
135 field = value
136 request.setAttribute(Constants.REQ_ATTR_VIEWMODEL, value)
137 }
139 /**
140 * Additional port info, if necessary.
141 */
142 private val portInfo =
143 if ((request.scheme == "http" && request.serverPort == 80)
144 || (request.scheme == "https" && request.serverPort == 443)
145 ) "" else ":${request.serverPort}"
147 /**
148 * The base path of this application.
149 */
150 val baseHref get() = "${request.scheme}://${request.serverName}$portInfo${request.contextPath}/"
152 private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext)
153 private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp")
155 fun param(name: String): String? = request.getParameter(name)
156 fun paramArray(name: String): Array<String> = request.getParameterValues(name) ?: emptyArray()
158 private fun forward(jsp: String) {
159 request.getRequestDispatcher(jspPath(jsp)).forward(request, response)
160 }
162 fun renderFeed(page: String? = null) {
163 page?.let { contentPage = it }
164 forward("feed")
165 }
167 fun render(page: String? = null) {
168 page?.let { contentPage = it }
169 forward("site")
170 }
172 fun renderCommit(location: String? = null) {
173 location?.let { redirectLocation = it }
174 contentPage = Constants.JSP_COMMIT_SUCCESSFUL
175 render()
176 }
178 fun i18n(key: String) = ResourceBundle.getBundle("localization/strings", response.locale).getString(key)
179 }
181 /**
182 * A path pattern optionally containing placeholders.
183 *
184 * The special directories . and .. are disallowed in the pattern.
185 * Placeholders start with a % sign.
186 *
187 * @param pattern the pattern
188 */
189 class PathPattern(pattern: String) {
190 private val nodePatterns: List<String>
191 private val collection: Boolean
193 private fun parse(pattern: String): List<String> {
194 val nodes = pattern.split("/").filter { it.isNotBlank() }.toList()
195 require(nodes.none { it == "." || it == ".." }) { "Path must not contain '.' or '..' nodes." }
196 return nodes
197 }
199 /**
200 * Matches a path against this pattern.
201 * The path must be canonical in the sense that no . or .. parts occur.
202 *
203 * @param path the path to match
204 * @return true if the path matches the pattern, false otherwise
205 */
206 fun matches(path: String): Boolean {
207 if (collection xor path.endsWith("/")) return false
208 val nodes = parse(path)
209 if (nodePatterns.size != nodes.size) return false
210 for (i in nodePatterns.indices) {
211 val pattern = nodePatterns[i]
212 val node = nodes[i]
213 if (pattern.startsWith("%")) continue
214 if (pattern != node) return false
215 }
216 return true
217 }
219 /**
220 * Returns the path parameters found in the specified path using this pattern.
221 * The return value of this method is undefined, if the patter does not match.
222 *
223 * @param path the path
224 * @return the path parameters, if any, or an empty map
225 * @see .matches
226 */
227 fun obtainPathParameters(path: String): PathParameters {
228 val params = mutableMapOf<String, String>()
229 val nodes = parse(path)
230 for (i in 0 until min(nodes.size, nodePatterns.size)) {
231 val pattern = nodePatterns[i]
232 val node = nodes[i]
233 if (pattern.startsWith("%")) {
234 params[pattern.substring(1)] = node
235 }
236 }
237 return params
238 }
240 override fun hashCode(): Int {
241 val str = StringBuilder()
242 for (node in nodePatterns) {
243 if (node.startsWith("%")) {
244 str.append("/%")
245 } else {
246 str.append('/')
247 str.append(node)
248 }
249 }
250 if (collection) str.append('/')
251 return str.toString().hashCode()
252 }
254 override fun equals(other: Any?): Boolean {
255 if (other is PathPattern) {
256 if (collection xor other.collection || nodePatterns.size != other.nodePatterns.size) return false
257 for (i in nodePatterns.indices) {
258 val left = nodePatterns[i]
259 val right = other.nodePatterns[i]
260 if (left.startsWith("%") && right.startsWith("%")) continue
261 if (left != right) return false
262 }
263 return true
264 } else {
265 return false
266 }
267 }
269 init {
270 nodePatterns = parse(pattern)
271 collection = pattern.endsWith("/")
272 }
273 }