src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt

Mon, 30 Oct 2023 14:44:36 +0100

author
Mike Becker <universe@uap-core.de>
date
Mon, 30 Oct 2023 14:44:36 +0100
changeset 292
703591e739f4
parent 284
671c1c8fbf1c
permissions
-rw-r--r--

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>

mercurial