Wed, 18 Aug 2021 12:47:32 +0200
#158 adds total number of comments
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 sealed interface ValidationResult<T>
41 class ValidationError<T>(val message: String): ValidationResult<T>
42 class ValidatedValue<T>(val result: T): ValidationResult<T>
44 class HttpRequest(
45 val request: HttpServletRequest,
46 val response: HttpServletResponse,
47 val pathParams: PathParameters = emptyMap()
48 ) {
49 val session: HttpSession = request.session
51 val remoteUser: String? = request.remoteUser
53 /**
54 * The name of the content page.
55 *
56 * @see Constants#REQ_ATTR_CONTENT_PAGE
57 */
58 var contentPage = ""
59 set(value) {
60 field = value
61 request.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(value))
62 }
64 /**
65 * The name of the content page.
66 *
67 * @see Constants#REQ_ATTR_PAGE_TITLE
68 */
69 var pageTitle = ""
70 set(value) {
71 field = value
72 request.setAttribute(Constants.REQ_ATTR_PAGE_TITLE, value)
73 }
75 /**
76 * A list of additional style sheets.
77 *
78 * @see Constants#REQ_ATTR_STYLESHEET
79 */
80 var styleSheets = emptyList<String>()
81 set(value) {
82 field = value
83 request.setAttribute(Constants.REQ_ATTR_STYLESHEET,
84 value.map { it.withExt(".css") }
85 )
86 }
88 /**
89 * A list of additional style sheets.
90 *
91 * @see Constants#REQ_ATTR_JAVASCRIPT
92 */
93 var javascript = ""
94 set(value) {
95 field = value
96 request.setAttribute(Constants.REQ_ATTR_JAVASCRIPT,
97 value.withExt(".js")
98 )
99 }
101 /**
102 * The name of the navigation menu JSP.
103 *
104 * @see Constants#REQ_ATTR_NAVIGATION
105 */
106 var navigationMenu: NavMenu? = null
107 set(value) {
108 field = value
109 request.setAttribute(Constants.REQ_ATTR_NAVIGATION, navigationMenu)
110 }
112 var redirectLocation: String? = null
113 set(value) {
114 field = value
115 if (value == null) {
116 request.removeAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION)
117 } else {
118 request.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, baseHref + value)
119 }
120 }
122 var feedPath: String? = null
123 set(value) {
124 field = value
125 if (value == null) {
126 request.removeAttribute(Constants.REQ_ATTR_FEED_HREF)
127 } else {
128 request.setAttribute(Constants.REQ_ATTR_FEED_HREF, baseHref + value)
129 }
130 }
132 /**
133 * The view object.
134 *
135 * @see Constants#REQ_ATTR_VIEWMODEL
136 */
137 var view: View? = null
138 set(value) {
139 field = value
140 request.setAttribute(Constants.REQ_ATTR_VIEWMODEL, value)
141 }
143 /**
144 * Additional port info, if necessary.
145 */
146 private val portInfo =
147 if ((request.scheme == "http" && request.serverPort == 80)
148 || (request.scheme == "https" && request.serverPort == 443)
149 ) "" else ":${request.serverPort}"
151 /**
152 * The base path of this application.
153 */
154 val baseHref get() = "${request.scheme}://${request.serverName}$portInfo${request.contextPath}/"
156 private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext)
157 private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp")
159 fun param(name: String): String? = request.getParameter(name)
160 fun paramArray(name: String): Array<String> = request.getParameterValues(name) ?: emptyArray()
162 fun <T> param(name: String, validator: (String?) -> (ValidationResult<T>),
163 defaultValue: T, errorMessages: MutableList<String>): T {
164 return when (val result = validator(param(name))) {
165 is ValidationError -> {
166 errorMessages.add(i18n(result.message))
167 defaultValue
168 }
169 is ValidatedValue -> {
170 result.result
171 }
172 }
173 }
175 private fun forward(jsp: String) {
176 request.getRequestDispatcher(jspPath(jsp)).forward(request, response)
177 }
179 fun renderFeed(page: String? = null) {
180 page?.let { contentPage = it }
181 forward("feed")
182 }
184 fun render(page: String? = null) {
185 page?.let { contentPage = it }
186 forward("site")
187 }
189 fun renderCommit(location: String? = null) {
190 location?.let { redirectLocation = it }
191 contentPage = Constants.JSP_COMMIT_SUCCESSFUL
192 render()
193 }
195 fun i18n(key: String) = ResourceBundle.getBundle("localization/strings", response.locale).getString(key)
196 }
198 /**
199 * A path pattern optionally containing placeholders.
200 *
201 * The special directories . and .. are disallowed in the pattern.
202 * Placeholders start with a % sign.
203 *
204 * @param pattern the pattern
205 */
206 class PathPattern(pattern: String) {
207 private val nodePatterns: List<String>
208 private val collection: Boolean
210 private fun parse(pattern: String): List<String> {
211 val nodes = pattern.split("/").filter { it.isNotBlank() }.toList()
212 require(nodes.none { it == "." || it == ".." }) { "Path must not contain '.' or '..' nodes." }
213 return nodes
214 }
216 /**
217 * Matches a path against this pattern.
218 * The path must be canonical in the sense that no . or .. parts occur.
219 *
220 * @param path the path to match
221 * @return true if the path matches the pattern, false otherwise
222 */
223 fun matches(path: String): Boolean {
224 if (collection xor path.endsWith("/")) return false
225 val nodes = parse(path)
226 if (nodePatterns.size != nodes.size) return false
227 for (i in nodePatterns.indices) {
228 val pattern = nodePatterns[i]
229 val node = nodes[i]
230 if (pattern.startsWith("%")) continue
231 if (pattern != node) return false
232 }
233 return true
234 }
236 /**
237 * Returns the path parameters found in the specified path using this pattern.
238 * The return value of this method is undefined, if the patter does not match.
239 *
240 * @param path the path
241 * @return the path parameters, if any, or an empty map
242 * @see .matches
243 */
244 fun obtainPathParameters(path: String): PathParameters {
245 val params = mutableMapOf<String, String>()
246 val nodes = parse(path)
247 for (i in 0 until min(nodes.size, nodePatterns.size)) {
248 val pattern = nodePatterns[i]
249 val node = nodes[i]
250 if (pattern.startsWith("%")) {
251 params[pattern.substring(1)] = node
252 }
253 }
254 return params
255 }
257 override fun hashCode(): Int {
258 val str = StringBuilder()
259 for (node in nodePatterns) {
260 if (node.startsWith("%")) {
261 str.append("/%")
262 } else {
263 str.append('/')
264 str.append(node)
265 }
266 }
267 if (collection) str.append('/')
268 return str.toString().hashCode()
269 }
271 override fun equals(other: Any?): Boolean {
272 if (other is PathPattern) {
273 if (collection xor other.collection || nodePatterns.size != other.nodePatterns.size) return false
274 for (i in nodePatterns.indices) {
275 val left = nodePatterns[i]
276 val right = other.nodePatterns[i]
277 if (left.startsWith("%") && right.startsWith("%")) continue
278 if (left != right) return false
279 }
280 return true
281 } else {
282 return false
283 }
284 }
286 init {
287 nodePatterns = parse(pattern)
288 collection = pattern.endsWith("/")
289 }
290 }