Sat, 15 May 2021 16:19:29 +0200
#109 adds RSS feed button to project header and changes feed output slightly
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 javax.servlet.http.HttpServletRequest
32 import javax.servlet.http.HttpServletResponse
33 import javax.servlet.http.HttpSession
34 import kotlin.math.min
36 typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit
37 typealias PathParameters = Map<String, String>
39 class HttpRequest(
40 val request: HttpServletRequest,
41 val response: HttpServletResponse,
42 val pathParams: PathParameters = emptyMap()
43 ) {
44 val session: HttpSession = request.session
46 val remoteUser: String? = request.remoteUser
48 /**
49 * The name of the content page.
50 *
51 * @see Constants#REQ_ATTR_CONTENT_PAGE
52 */
53 var contentPage = ""
54 set(value) {
55 field = value
56 request.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(value))
57 }
59 /**
60 * A list of additional style sheets.
61 *
62 * @see Constants#REQ_ATTR_STYLESHEET
63 */
64 var styleSheets = emptyList<String>()
65 set(value) {
66 field = value
67 request.setAttribute(Constants.REQ_ATTR_STYLESHEET,
68 value.map { it.withExt(".css") }
69 )
70 }
72 /**
73 * The name of the navigation menu JSP.
74 *
75 * @see Constants#REQ_ATTR_NAVIGATION
76 */
77 var navigationMenu: NavMenu? = null
78 set(value) {
79 field = value
80 request.setAttribute(Constants.REQ_ATTR_NAVIGATION, navigationMenu)
81 }
83 var redirectLocation: String? = null
84 set(value) {
85 field = value
86 if (value == null) {
87 request.removeAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION)
88 } else {
89 request.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, baseHref + value)
90 }
91 }
93 var feedPath: String? = null
94 set(value) {
95 field = value
96 if (value == null) {
97 request.removeAttribute(Constants.REQ_ATTR_FEED_HREF)
98 } else {
99 request.setAttribute(Constants.REQ_ATTR_FEED_HREF, baseHref + value)
100 }
101 }
103 /**
104 * The view object.
105 *
106 * @see Constants#REQ_ATTR_VIEWMODEL
107 */
108 var view: View? = null
109 set(value) {
110 field = value
111 request.setAttribute(Constants.REQ_ATTR_VIEWMODEL, value)
112 }
114 /**
115 * Additional port info, if necessary.
116 */
117 private val portInfo =
118 if ((request.scheme == "http" && request.serverPort == 80)
119 || (request.scheme == "https" && request.serverPort == 443)
120 ) "" else ":${request.serverPort}"
122 /**
123 * The base path of this application.
124 */
125 val baseHref get() = "${request.scheme}://${request.serverName}$portInfo${request.contextPath}/"
127 private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext)
128 private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp")
130 fun param(name: String): String? = request.getParameter(name)
131 fun paramArray(name: String): Array<String> = request.getParameterValues(name) ?: emptyArray()
133 private fun forward(jsp: String) {
134 request.getRequestDispatcher(jspPath(jsp)).forward(request, response)
135 }
137 fun renderFeed(page: String? = null) {
138 page?.let { contentPage = it }
139 forward("feed")
140 }
142 fun render(page: String? = null) {
143 page?.let { contentPage = it }
144 forward("site")
145 }
147 fun renderCommit(location: String? = null) {
148 location?.let { redirectLocation = it }
149 contentPage = Constants.JSP_COMMIT_SUCCESSFUL
150 render()
151 }
152 }
154 /**
155 * A path pattern optionally containing placeholders.
156 *
157 * The special directories . and .. are disallowed in the pattern.
158 * Placeholders start with a % sign.
159 *
160 * @param pattern the pattern
161 */
162 class PathPattern(pattern: String) {
163 private val nodePatterns: List<String>
164 private val collection: Boolean
166 private fun parse(pattern: String): List<String> {
167 val nodes = pattern.split("/").filter { it.isNotBlank() }.toList()
168 require(nodes.none { it == "." || it == ".." }) { "Path must not contain '.' or '..' nodes." }
169 return nodes
170 }
172 /**
173 * Matches a path against this pattern.
174 * The path must be canonical in the sense that no . or .. parts occur.
175 *
176 * @param path the path to match
177 * @return true if the path matches the pattern, false otherwise
178 */
179 fun matches(path: String): Boolean {
180 if (collection xor path.endsWith("/")) return false
181 val nodes = parse(path)
182 if (nodePatterns.size != nodes.size) return false
183 for (i in nodePatterns.indices) {
184 val pattern = nodePatterns[i]
185 val node = nodes[i]
186 if (pattern.startsWith("%")) continue
187 if (pattern != node) return false
188 }
189 return true
190 }
192 /**
193 * Returns the path parameters found in the specified path using this pattern.
194 * The return value of this method is undefined, if the patter does not match.
195 *
196 * @param path the path
197 * @return the path parameters, if any, or an empty map
198 * @see .matches
199 */
200 fun obtainPathParameters(path: String): PathParameters {
201 val params = mutableMapOf<String, String>()
202 val nodes = parse(path)
203 for (i in 0 until min(nodes.size, nodePatterns.size)) {
204 val pattern = nodePatterns[i]
205 val node = nodes[i]
206 if (pattern.startsWith("%")) {
207 params[pattern.substring(1)] = node
208 }
209 }
210 return params
211 }
213 override fun hashCode(): Int {
214 val str = StringBuilder()
215 for (node in nodePatterns) {
216 if (node.startsWith("%")) {
217 str.append("/%")
218 } else {
219 str.append('/')
220 str.append(node)
221 }
222 }
223 if (collection) str.append('/')
224 return str.toString().hashCode()
225 }
227 override fun equals(other: Any?): Boolean {
228 if (other is PathPattern) {
229 if (collection xor other.collection || nodePatterns.size != other.nodePatterns.size) return false
230 for (i in nodePatterns.indices) {
231 val left = nodePatterns[i]
232 val right = other.nodePatterns[i]
233 if (left.startsWith("%") && right.startsWith("%")) continue
234 if (left != right) return false
235 }
236 return true
237 } else {
238 return false
239 }
240 }
242 init {
243 nodePatterns = parse(pattern)
244 collection = pattern.endsWith("/")
245 }
246 }