Mon, 30 Oct 2023 14:44:36 +0100
add possibility to show issues w/o version or component - fixes #335
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.entities.HasNode
30 import de.uapcore.lightpit.viewmodel.NavMenu
31 import de.uapcore.lightpit.viewmodel.View
32 import jakarta.servlet.http.HttpServletRequest
33 import jakarta.servlet.http.HttpServletResponse
34 import jakarta.servlet.http.HttpSession
35 import java.util.*
36 import kotlin.math.min
37 import java.sql.Date as SqlDate
39 typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit
40 typealias PathParameters = Map<String, String>
42 sealed class OptionalPathInfo<in T : HasNode>(info: T) {
43 class Specific<T: HasNode>(val elem: T) : OptionalPathInfo<T>(elem)
44 data object All : OptionalPathInfo<HasNode>(object : HasNode { override val node = "-"})
45 data object None : OptionalPathInfo<HasNode>(object : HasNode { override val node = "~"})
46 data object NotFound : OptionalPathInfo<HasNode>(object : HasNode { override val node = ""})
47 val node = info.node
48 }
50 sealed interface ValidationResult<T>
51 class ValidationError<T>(val message: String): ValidationResult<T>
52 class ValidatedValue<T>(val result: T): ValidationResult<T>
54 class HttpRequest(
55 val request: HttpServletRequest,
56 val response: HttpServletResponse,
57 val pathParams: PathParameters = emptyMap()
58 ) {
59 val session: HttpSession = request.session
61 val remoteUser: String? = request.remoteUser
63 /**
64 * The name of the content page.
65 *
66 * @see Constants#REQ_ATTR_CONTENT_PAGE
67 */
68 var contentPage = ""
69 set(value) {
70 field = value
71 request.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(value))
72 }
74 /**
75 * The name of the content page.
76 *
77 * @see Constants#REQ_ATTR_PAGE_TITLE
78 */
79 var pageTitle = ""
80 set(value) {
81 field = value
82 request.setAttribute(Constants.REQ_ATTR_PAGE_TITLE, value)
83 }
85 /**
86 * A list of additional style sheets.
87 *
88 * @see Constants#REQ_ATTR_STYLESHEET
89 */
90 var styleSheets = emptyList<String>()
91 set(value) {
92 field = value
93 request.setAttribute(Constants.REQ_ATTR_STYLESHEET,
94 value.map { it.withExt(".css") }
95 )
96 }
98 /**
99 * A list of additional style sheets.
100 *
101 * @see Constants#REQ_ATTR_JAVASCRIPT
102 */
103 var javascript = ""
104 set(value) {
105 field = value
106 request.setAttribute(Constants.REQ_ATTR_JAVASCRIPT,
107 value.withExt(".js")
108 )
109 }
111 /**
112 * The name of the navigation menu JSP.
113 *
114 * @see Constants#REQ_ATTR_NAVIGATION
115 */
116 var navigationMenu: NavMenu? = null
117 set(value) {
118 field = value
119 request.setAttribute(Constants.REQ_ATTR_NAVIGATION, navigationMenu)
120 }
122 var redirectLocation: String? = null
123 set(value) {
124 field = value
125 if (value == null) {
126 request.removeAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION)
127 } else {
128 request.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, baseHref + value)
129 }
130 }
132 var feedPath: String? = null
133 set(value) {
134 field = value
135 if (value == null) {
136 request.removeAttribute(Constants.REQ_ATTR_FEED_HREF)
137 } else {
138 request.setAttribute(Constants.REQ_ATTR_FEED_HREF, baseHref + value)
139 }
140 }
142 /**
143 * The view object.
144 *
145 * @see Constants#REQ_ATTR_VIEWMODEL
146 */
147 var view: View? = null
148 set(value) {
149 field = value
150 request.setAttribute(Constants.REQ_ATTR_VIEWMODEL, value)
151 }
153 /**
154 * Additional port info, if necessary.
155 */
156 private val portInfo =
157 if ((request.scheme == "http" && request.serverPort == 80)
158 || (request.scheme == "https" && request.serverPort == 443)
159 ) "" else ":${request.serverPort}"
161 /**
162 * The base path of this application.
163 */
164 val baseHref get() = "${request.scheme}://${request.serverName}$portInfo${request.contextPath}/"
166 private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext)
167 private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp")
169 fun param(name: String): String? = request.getParameter(name)
170 fun paramArray(name: String): Array<String> = request.getParameterValues(name) ?: emptyArray()
172 fun <T> param(name: String, validator: (String?) -> (ValidationResult<T>),
173 defaultValue: T, errorMessages: MutableList<String>): T {
174 return when (val result = validator(param(name))) {
175 is ValidationError -> {
176 errorMessages.add(i18n(result.message))
177 defaultValue
178 }
179 is ValidatedValue -> {
180 result.result
181 }
182 }
183 }
186 fun <T : HasNode> lookupPathParam(paramName: String, list: List<T>): OptionalPathInfo<T> {
187 return when (val node = this.pathParams[paramName]) {
188 null -> OptionalPathInfo.All
189 "-" -> OptionalPathInfo.All
190 "~" -> OptionalPathInfo.None
191 else -> list.find { it.node == node }
192 ?.let { OptionalPathInfo.Specific(it) }
193 ?: OptionalPathInfo.NotFound
194 }
195 }
197 val body: String by lazy {
198 request.reader.lineSequence().joinToString("\n")
199 }
201 private fun forward(jsp: String) {
202 request.getRequestDispatcher(jspPath(jsp)).forward(request, response)
203 }
205 fun renderFeed(page: String? = null) {
206 page?.let { contentPage = it }
207 forward("feed")
208 }
210 fun render(page: String? = null) {
211 page?.let { contentPage = it }
212 forward("site")
213 }
215 fun renderCommit(location: String? = null) {
216 location?.let { redirectLocation = it }
217 contentPage = Constants.JSP_COMMIT_SUCCESSFUL
218 render()
219 }
221 fun i18n(key: String): String = ResourceBundle.getBundle("localization/strings", response.locale).getString(key)
222 }
224 /**
225 * A path pattern optionally containing placeholders.
226 *
227 * The special directories . and .. are disallowed in the pattern.
228 * Placeholders start with a % sign.
229 *
230 * @param pattern the pattern
231 */
232 class PathPattern(pattern: String) {
233 private val nodePatterns: List<String>
234 private val collection: Boolean
236 private fun parse(pattern: String): List<String> {
237 val nodes = pattern.split("/").filter { it.isNotBlank() }.toList()
238 require(nodes.none { it == "." || it == ".." }) { "Path must not contain '.' or '..' nodes." }
239 return nodes
240 }
242 /**
243 * Matches a path against this pattern.
244 * The path must be canonical in the sense that no . or .. parts occur.
245 *
246 * @param path the path to match
247 * @return true if the path matches the pattern, false otherwise
248 */
249 fun matches(path: String): Boolean {
250 if (collection xor path.endsWith("/")) return false
251 val nodes = parse(path)
252 if (nodePatterns.size != nodes.size) return false
253 for (i in nodePatterns.indices) {
254 val pattern = nodePatterns[i]
255 val node = nodes[i]
256 if (pattern.startsWith("%")) continue
257 if (pattern != node) return false
258 }
259 return true
260 }
262 /**
263 * Returns the path parameters found in the specified path using this pattern.
264 * The return value of this method is undefined, if the patter does not match.
265 *
266 * @param path the path
267 * @return the path parameters, if any, or an empty map
268 * @see .matches
269 */
270 fun obtainPathParameters(path: String): PathParameters {
271 val params = mutableMapOf<String, String>()
272 val nodes = parse(path)
273 for (i in 0 until min(nodes.size, nodePatterns.size)) {
274 val pattern = nodePatterns[i]
275 val node = nodes[i]
276 if (pattern.startsWith("%")) {
277 params[pattern.substring(1)] = node
278 }
279 }
280 return params
281 }
283 override fun hashCode(): Int {
284 val str = StringBuilder()
285 for (node in nodePatterns) {
286 if (node.startsWith("%")) {
287 str.append("/%")
288 } else {
289 str.append('/')
290 str.append(node)
291 }
292 }
293 if (collection) str.append('/')
294 return str.toString().hashCode()
295 }
297 override fun equals(other: Any?): Boolean {
298 if (other is PathPattern) {
299 if (collection xor other.collection || nodePatterns.size != other.nodePatterns.size) return false
300 for (i in nodePatterns.indices) {
301 val left = nodePatterns[i]
302 val right = other.nodePatterns[i]
303 if (left.startsWith("%") && right.startsWith("%")) continue
304 if (left != right) return false
305 }
306 return true
307 } else {
308 return false
309 }
310 }
312 init {
313 nodePatterns = parse(pattern)
314 collection = pattern.endsWith("/")
315 }
316 }
318 // <editor-fold desc="Validators">
320 fun dateOptValidator(input: String?): ValidationResult<SqlDate?> {
321 return if (input.isNullOrBlank()) {
322 ValidatedValue(null)
323 } else {
324 try {
325 ValidatedValue(SqlDate.valueOf(input))
326 } catch (ignored: IllegalArgumentException) {
327 ValidationError("validation.date.format")
328 }
329 }
330 }
332 fun boolValidator(input: String?): ValidationResult<Boolean> {
333 return if (input.isNullOrBlank()) {
334 ValidatedValue(false)
335 } else {
336 ValidatedValue(!(input.equals("false", true) || input == "0"))
337 }
338 }
340 // </editor-fold>