universe@179: /* universe@179: * Copyright 2021 Mike Becker. All rights reserved. universe@179: * universe@179: * Redistribution and use in source and binary forms, with or without universe@179: * modification, are permitted provided that the following conditions are met: universe@179: * universe@179: * 1. Redistributions of source code must retain the above copyright universe@179: * notice, this list of conditions and the following disclaimer. universe@179: * universe@179: * 2. Redistributions in binary form must reproduce the above copyright universe@179: * notice, this list of conditions and the following disclaimer in the universe@179: * documentation and/or other materials provided with the distribution. universe@179: * universe@179: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" universe@179: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE universe@179: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE universe@179: * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE universe@179: * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL universe@179: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR universe@179: * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER universe@179: * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, universe@179: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE universe@179: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. universe@179: */ universe@179: universe@179: package de.uapcore.lightpit universe@179: universe@184: import de.uapcore.lightpit.dao.DataAccessObject universe@184: import de.uapcore.lightpit.viewmodel.NavMenu universe@184: import de.uapcore.lightpit.viewmodel.View universe@184: import javax.servlet.http.HttpServletRequest universe@184: import javax.servlet.http.HttpServletResponse universe@184: import javax.servlet.http.HttpSession universe@179: import kotlin.math.min universe@179: universe@184: typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit universe@184: typealias PathParameters = Map universe@184: universe@184: class HttpRequest( universe@184: val request: HttpServletRequest, universe@184: val response: HttpServletResponse, universe@184: val pathParams: PathParameters = emptyMap() universe@184: ) { universe@184: val session: HttpSession = request.session universe@184: universe@184: val remoteUser: String? = request.remoteUser universe@179: universe@179: /** universe@184: * The name of the content page. universe@179: * universe@184: * @see Constants#REQ_ATTR_CONTENT_PAGE universe@179: */ universe@184: var contentPage = "" universe@184: set(value) { universe@184: field = value universe@184: request.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(value)) universe@184: } universe@179: universe@179: /** universe@184: * A list of additional style sheets. universe@179: * universe@184: * @see Constants#REQ_ATTR_STYLESHEET universe@179: */ universe@184: var styleSheets = emptyList() universe@184: set(value) { universe@184: field = value universe@184: request.setAttribute(Constants.REQ_ATTR_STYLESHEET, universe@184: value.map { it.withExt(".css") } universe@184: ) universe@184: } universe@179: universe@184: /** universe@184: * The name of the navigation menu JSP. universe@184: * universe@184: * @see Constants#REQ_ATTR_NAVIGATION universe@184: */ universe@184: var navigationMenu: NavMenu? = null universe@184: set(value) { universe@184: field = value universe@184: request.setAttribute(Constants.REQ_ATTR_NAVIGATION, navigationMenu) universe@184: } universe@184: universe@199: var redirectLocation: String? = null universe@184: set(value) { universe@184: field = value universe@199: if (value == null) { universe@199: request.removeAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION) universe@199: } else { universe@199: request.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, baseHref + value) universe@199: } universe@184: } universe@184: universe@199: var feedPath: String? = null universe@198: set(value) { universe@198: field = value universe@199: if (value == null) { universe@199: request.removeAttribute(Constants.REQ_ATTR_FEED_HREF) universe@199: } else { universe@199: request.setAttribute(Constants.REQ_ATTR_FEED_HREF, baseHref + value) universe@199: } universe@198: } universe@198: universe@184: /** universe@184: * The view object. universe@184: * universe@184: * @see Constants#REQ_ATTR_VIEWMODEL universe@184: */ universe@184: var view: View? = null universe@184: set(value) { universe@184: field = value universe@184: request.setAttribute(Constants.REQ_ATTR_VIEWMODEL, value) universe@184: } universe@184: universe@184: /** universe@198: * Additional port info, if necessary. universe@198: */ universe@198: private val portInfo = universe@198: if ((request.scheme == "http" && request.serverPort == 80) universe@198: || (request.scheme == "https" && request.serverPort == 443) universe@198: ) "" else ":${request.serverPort}" universe@198: universe@198: /** universe@184: * The base path of this application. universe@184: */ universe@198: val baseHref get() = "${request.scheme}://${request.serverName}$portInfo${request.contextPath}/" universe@198: universe@184: private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext) universe@184: private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp") universe@184: universe@184: fun param(name: String): String? = request.getParameter(name) universe@184: fun paramArray(name: String): Array = request.getParameterValues(name) ?: emptyArray() universe@184: universe@198: private fun forward(jsp: String) { universe@195: request.getRequestDispatcher(jspPath(jsp)).forward(request, response) universe@195: } universe@195: universe@198: fun renderFeed(page: String? = null) { universe@198: page?.let { contentPage = it } universe@198: forward("feed") universe@198: } universe@198: universe@184: fun render(page: String? = null) { universe@184: page?.let { contentPage = it } universe@195: forward("site") universe@184: } universe@184: universe@184: fun renderCommit(location: String? = null) { universe@184: location?.let { redirectLocation = it } universe@184: contentPage = Constants.JSP_COMMIT_SUCCESSFUL universe@184: render() universe@184: } universe@184: } universe@179: universe@179: /** universe@179: * A path pattern optionally containing placeholders. universe@179: * universe@179: * The special directories . and .. are disallowed in the pattern. universe@184: * Placeholders start with a % sign. universe@179: * universe@179: * @param pattern the pattern universe@179: */ universe@179: class PathPattern(pattern: String) { universe@179: private val nodePatterns: List universe@179: private val collection: Boolean universe@179: universe@179: private fun parse(pattern: String): List { universe@179: val nodes = pattern.split("/").filter { it.isNotBlank() }.toList() universe@179: require(nodes.none { it == "." || it == ".." }) { "Path must not contain '.' or '..' nodes." } universe@179: return nodes universe@179: } universe@179: universe@179: /** universe@179: * Matches a path against this pattern. universe@179: * The path must be canonical in the sense that no . or .. parts occur. universe@179: * universe@179: * @param path the path to match universe@179: * @return true if the path matches the pattern, false otherwise universe@179: */ universe@179: fun matches(path: String): Boolean { universe@179: if (collection xor path.endsWith("/")) return false universe@179: val nodes = parse(path) universe@179: if (nodePatterns.size != nodes.size) return false universe@179: for (i in nodePatterns.indices) { universe@179: val pattern = nodePatterns[i] universe@179: val node = nodes[i] universe@184: if (pattern.startsWith("%")) continue universe@179: if (pattern != node) return false universe@179: } universe@179: return true universe@179: } universe@179: universe@179: /** universe@179: * Returns the path parameters found in the specified path using this pattern. universe@179: * The return value of this method is undefined, if the patter does not match. universe@179: * universe@179: * @param path the path universe@179: * @return the path parameters, if any, or an empty map universe@179: * @see .matches universe@179: */ universe@179: fun obtainPathParameters(path: String): PathParameters { universe@184: val params = mutableMapOf() universe@179: val nodes = parse(path) universe@179: for (i in 0 until min(nodes.size, nodePatterns.size)) { universe@179: val pattern = nodePatterns[i] universe@179: val node = nodes[i] universe@184: if (pattern.startsWith("%")) { universe@179: params[pattern.substring(1)] = node universe@179: } universe@179: } universe@179: return params universe@179: } universe@179: universe@179: override fun hashCode(): Int { universe@179: val str = StringBuilder() universe@179: for (node in nodePatterns) { universe@184: if (node.startsWith("%")) { universe@184: str.append("/%") universe@179: } else { universe@179: str.append('/') universe@179: str.append(node) universe@179: } universe@179: } universe@179: if (collection) str.append('/') universe@179: return str.toString().hashCode() universe@179: } universe@179: universe@179: override fun equals(other: Any?): Boolean { universe@179: if (other is PathPattern) { universe@179: if (collection xor other.collection || nodePatterns.size != other.nodePatterns.size) return false universe@179: for (i in nodePatterns.indices) { universe@179: val left = nodePatterns[i] universe@179: val right = other.nodePatterns[i] universe@184: if (left.startsWith("%") && right.startsWith("%")) continue universe@179: if (left != right) return false universe@179: } universe@179: return true universe@179: } else { universe@179: return false universe@179: } universe@179: } universe@179: universe@179: init { universe@179: nodePatterns = parse(pattern) universe@179: collection = pattern.endsWith("/") universe@179: } universe@179: } universe@179: