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@292: import de.uapcore.lightpit.entities.HasNode universe@184: import de.uapcore.lightpit.viewmodel.NavMenu universe@184: import de.uapcore.lightpit.viewmodel.View universe@254: import jakarta.servlet.http.HttpServletRequest universe@254: import jakarta.servlet.http.HttpServletResponse universe@254: import jakarta.servlet.http.HttpSession universe@205: import java.util.* universe@179: import kotlin.math.min universe@225: import java.sql.Date as SqlDate universe@179: universe@184: typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit universe@184: typealias PathParameters = Map universe@184: universe@292: sealed class OptionalPathInfo(info: T) { universe@292: class Specific(val elem: T) : OptionalPathInfo(elem) universe@292: data object All : OptionalPathInfo(object : HasNode { override val node = "-"}) universe@292: data object None : OptionalPathInfo(object : HasNode { override val node = "~"}) universe@292: data object NotFound : OptionalPathInfo(object : HasNode { override val node = ""}) universe@292: val node = info.node universe@292: } universe@292: universe@209: sealed interface ValidationResult universe@209: class ValidationError(val message: String): ValidationResult universe@209: class ValidatedValue(val result: T): ValidationResult universe@209: 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@184: universe@184: /** universe@184: * The name of the content page. universe@184: * universe@184: * @see Constants#REQ_ATTR_CONTENT_PAGE universe@184: */ 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@184: universe@184: /** universe@205: * The name of the content page. universe@205: * universe@207: * @see Constants#REQ_ATTR_PAGE_TITLE universe@205: */ universe@205: var pageTitle = "" universe@205: set(value) { universe@205: field = value universe@205: request.setAttribute(Constants.REQ_ATTR_PAGE_TITLE, value) universe@205: } universe@205: universe@205: /** universe@184: * A list of additional style sheets. universe@184: * universe@184: * @see Constants#REQ_ATTR_STYLESHEET universe@184: */ 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@179: /** universe@207: * A list of additional style sheets. universe@207: * universe@207: * @see Constants#REQ_ATTR_JAVASCRIPT universe@207: */ universe@207: var javascript = "" universe@207: set(value) { universe@207: field = value universe@207: request.setAttribute(Constants.REQ_ATTR_JAVASCRIPT, universe@207: value.withExt(".js") universe@207: ) universe@207: } universe@207: universe@207: /** universe@184: * The name of the navigation menu JSP. universe@179: * universe@184: * @see Constants#REQ_ATTR_NAVIGATION universe@179: */ 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@179: 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@179: /** universe@184: * The view object. universe@179: * 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@179: */ 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@179: universe@210: fun param(name: String, validator: (String?) -> (ValidationResult), universe@210: defaultValue: T, errorMessages: MutableList): T { universe@209: return when (val result = validator(param(name))) { universe@209: is ValidationError -> { universe@209: errorMessages.add(i18n(result.message)) universe@210: defaultValue universe@209: } universe@209: is ValidatedValue -> { universe@209: result.result universe@209: } universe@209: } universe@209: } universe@209: universe@292: universe@292: fun lookupPathParam(paramName: String, list: List): OptionalPathInfo { universe@292: return when (val node = this.pathParams[paramName]) { universe@292: null -> OptionalPathInfo.All universe@292: "-" -> OptionalPathInfo.All universe@292: "~" -> OptionalPathInfo.None universe@292: else -> list.find { it.node == node } universe@292: ?.let { OptionalPathInfo.Specific(it) } universe@292: ?: OptionalPathInfo.NotFound universe@292: } universe@292: } universe@292: universe@284: val body: String by lazy { universe@284: request.reader.lineSequence().joinToString("\n") universe@284: } universe@284: 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@205: universe@260: fun i18n(key: String): String = ResourceBundle.getBundle("localization/strings", response.locale).getString(key) 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: universe@225: // universe@225: universe@225: fun dateOptValidator(input: String?): ValidationResult { universe@225: return if (input.isNullOrBlank()) { universe@225: ValidatedValue(null) universe@225: } else { universe@225: try { universe@225: ValidatedValue(SqlDate.valueOf(input)) universe@225: } catch (ignored: IllegalArgumentException) { universe@225: ValidationError("validation.date.format") universe@225: } universe@225: } universe@225: } universe@225: universe@227: fun boolValidator(input: String?): ValidationResult { universe@227: return if (input.isNullOrBlank()) { universe@227: ValidatedValue(false) universe@227: } else { universe@227: ValidatedValue(!(input.equals("false", true) || input == "0")) universe@227: } universe@227: } universe@227: universe@225: //