Wed, 28 Dec 2022 13:21:30 +0100
#233 migrate to Jakarta EE and update dependencies
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 jakarta.servlet.http.HttpServletRequest
32 import jakarta.servlet.http.HttpServletResponse
33 import jakarta.servlet.http.HttpSession
34 import java.util.*
35 import kotlin.math.min
36 import java.sql.Date as SqlDate
38 typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit
39 typealias PathParameters = Map<String, String>
41 sealed interface ValidationResult<T>
42 class ValidationError<T>(val message: String): ValidationResult<T>
43 class ValidatedValue<T>(val result: T): ValidationResult<T>
45 class HttpRequest(
46 val request: HttpServletRequest,
47 val response: HttpServletResponse,
48 val pathParams: PathParameters = emptyMap()
49 ) {
50 val session: HttpSession = request.session
52 val remoteUser: String? = request.remoteUser
54 /**
55 * The name of the content page.
56 *
57 * @see Constants#REQ_ATTR_CONTENT_PAGE
58 */
59 var contentPage = ""
60 set(value) {
61 field = value
62 request.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(value))
63 }
65 /**
66 * The name of the content page.
67 *
68 * @see Constants#REQ_ATTR_PAGE_TITLE
69 */
70 var pageTitle = ""
71 set(value) {
72 field = value
73 request.setAttribute(Constants.REQ_ATTR_PAGE_TITLE, value)
74 }
76 /**
77 * A list of additional style sheets.
78 *
79 * @see Constants#REQ_ATTR_STYLESHEET
80 */
81 var styleSheets = emptyList<String>()
82 set(value) {
83 field = value
84 request.setAttribute(Constants.REQ_ATTR_STYLESHEET,
85 value.map { it.withExt(".css") }
86 )
87 }
89 /**
90 * A list of additional style sheets.
91 *
92 * @see Constants#REQ_ATTR_JAVASCRIPT
93 */
94 var javascript = ""
95 set(value) {
96 field = value
97 request.setAttribute(Constants.REQ_ATTR_JAVASCRIPT,
98 value.withExt(".js")
99 )
100 }
102 /**
103 * The name of the navigation menu JSP.
104 *
105 * @see Constants#REQ_ATTR_NAVIGATION
106 */
107 var navigationMenu: NavMenu? = null
108 set(value) {
109 field = value
110 request.setAttribute(Constants.REQ_ATTR_NAVIGATION, navigationMenu)
111 }
113 var redirectLocation: String? = null
114 set(value) {
115 field = value
116 if (value == null) {
117 request.removeAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION)
118 } else {
119 request.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, baseHref + value)
120 }
121 }
123 var feedPath: String? = null
124 set(value) {
125 field = value
126 if (value == null) {
127 request.removeAttribute(Constants.REQ_ATTR_FEED_HREF)
128 } else {
129 request.setAttribute(Constants.REQ_ATTR_FEED_HREF, baseHref + value)
130 }
131 }
133 /**
134 * The view object.
135 *
136 * @see Constants#REQ_ATTR_VIEWMODEL
137 */
138 var view: View? = null
139 set(value) {
140 field = value
141 request.setAttribute(Constants.REQ_ATTR_VIEWMODEL, value)
142 }
144 /**
145 * Additional port info, if necessary.
146 */
147 private val portInfo =
148 if ((request.scheme == "http" && request.serverPort == 80)
149 || (request.scheme == "https" && request.serverPort == 443)
150 ) "" else ":${request.serverPort}"
152 /**
153 * The base path of this application.
154 */
155 val baseHref get() = "${request.scheme}://${request.serverName}$portInfo${request.contextPath}/"
157 private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext)
158 private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp")
160 fun param(name: String): String? = request.getParameter(name)
161 fun paramArray(name: String): Array<String> = request.getParameterValues(name) ?: emptyArray()
163 fun <T> param(name: String, validator: (String?) -> (ValidationResult<T>),
164 defaultValue: T, errorMessages: MutableList<String>): T {
165 return when (val result = validator(param(name))) {
166 is ValidationError -> {
167 errorMessages.add(i18n(result.message))
168 defaultValue
169 }
170 is ValidatedValue -> {
171 result.result
172 }
173 }
174 }
176 private fun forward(jsp: String) {
177 request.getRequestDispatcher(jspPath(jsp)).forward(request, response)
178 }
180 fun renderFeed(page: String? = null) {
181 page?.let { contentPage = it }
182 forward("feed")
183 }
185 fun render(page: String? = null) {
186 page?.let { contentPage = it }
187 forward("site")
188 }
190 fun renderCommit(location: String? = null) {
191 location?.let { redirectLocation = it }
192 contentPage = Constants.JSP_COMMIT_SUCCESSFUL
193 render()
194 }
196 fun i18n(key: String) = ResourceBundle.getBundle("localization/strings", response.locale).getString(key)
197 }
199 /**
200 * A path pattern optionally containing placeholders.
201 *
202 * The special directories . and .. are disallowed in the pattern.
203 * Placeholders start with a % sign.
204 *
205 * @param pattern the pattern
206 */
207 class PathPattern(pattern: String) {
208 private val nodePatterns: List<String>
209 private val collection: Boolean
211 private fun parse(pattern: String): List<String> {
212 val nodes = pattern.split("/").filter { it.isNotBlank() }.toList()
213 require(nodes.none { it == "." || it == ".." }) { "Path must not contain '.' or '..' nodes." }
214 return nodes
215 }
217 /**
218 * Matches a path against this pattern.
219 * The path must be canonical in the sense that no . or .. parts occur.
220 *
221 * @param path the path to match
222 * @return true if the path matches the pattern, false otherwise
223 */
224 fun matches(path: String): Boolean {
225 if (collection xor path.endsWith("/")) return false
226 val nodes = parse(path)
227 if (nodePatterns.size != nodes.size) return false
228 for (i in nodePatterns.indices) {
229 val pattern = nodePatterns[i]
230 val node = nodes[i]
231 if (pattern.startsWith("%")) continue
232 if (pattern != node) return false
233 }
234 return true
235 }
237 /**
238 * Returns the path parameters found in the specified path using this pattern.
239 * The return value of this method is undefined, if the patter does not match.
240 *
241 * @param path the path
242 * @return the path parameters, if any, or an empty map
243 * @see .matches
244 */
245 fun obtainPathParameters(path: String): PathParameters {
246 val params = mutableMapOf<String, String>()
247 val nodes = parse(path)
248 for (i in 0 until min(nodes.size, nodePatterns.size)) {
249 val pattern = nodePatterns[i]
250 val node = nodes[i]
251 if (pattern.startsWith("%")) {
252 params[pattern.substring(1)] = node
253 }
254 }
255 return params
256 }
258 override fun hashCode(): Int {
259 val str = StringBuilder()
260 for (node in nodePatterns) {
261 if (node.startsWith("%")) {
262 str.append("/%")
263 } else {
264 str.append('/')
265 str.append(node)
266 }
267 }
268 if (collection) str.append('/')
269 return str.toString().hashCode()
270 }
272 override fun equals(other: Any?): Boolean {
273 if (other is PathPattern) {
274 if (collection xor other.collection || nodePatterns.size != other.nodePatterns.size) return false
275 for (i in nodePatterns.indices) {
276 val left = nodePatterns[i]
277 val right = other.nodePatterns[i]
278 if (left.startsWith("%") && right.startsWith("%")) continue
279 if (left != right) return false
280 }
281 return true
282 } else {
283 return false
284 }
285 }
287 init {
288 nodePatterns = parse(pattern)
289 collection = pattern.endsWith("/")
290 }
291 }
293 // <editor-fold desc="Validators">
295 fun dateOptValidator(input: String?): ValidationResult<SqlDate?> {
296 return if (input.isNullOrBlank()) {
297 ValidatedValue(null)
298 } else {
299 try {
300 ValidatedValue(SqlDate.valueOf(input))
301 } catch (ignored: IllegalArgumentException) {
302 ValidationError("validation.date.format")
303 }
304 }
305 }
307 fun boolValidator(input: String?): ValidationResult<Boolean> {
308 return if (input.isNullOrBlank()) {
309 ValidatedValue(false)
310 } else {
311 ValidatedValue(!(input.equals("false", true) || input == "0"))
312 }
313 }
315 // </editor-fold>