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

Sat, 22 Jul 2023 22:32:04 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 22 Jul 2023 22:32:04 +0200
changeset 284
671c1c8fbf1c
parent 260
fb2ae2d63a56
child 292
703591e739f4
permissions
-rw-r--r--

add full support for commit references - fixes #276

     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 jakarta.servlet.http.HttpServletRequest
    32 import jakarta.servlet.http.HttpServletResponse
    33 import jakarta.servlet.http.HttpSession
    34 import java.util.*
    35 import kotlin.math.min
    36 import java.sql.Date as SqlDate
    38 typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit
    39 typealias PathParameters = Map<String, String>
    41 sealed interface ValidationResult<T>
    42 class ValidationError<T>(val message: String): ValidationResult<T>
    43 class ValidatedValue<T>(val result: T): ValidationResult<T>
    45 class HttpRequest(
    46     val request: HttpServletRequest,
    47     val response: HttpServletResponse,
    48     val pathParams: PathParameters = emptyMap()
    49 ) {
    50     val session: HttpSession = request.session
    52     val remoteUser: String? = request.remoteUser
    54     /**
    55      * The name of the content page.
    56      *
    57      * @see Constants#REQ_ATTR_CONTENT_PAGE
    58      */
    59     var contentPage = ""
    60         set(value) {
    61             field = value
    62             request.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(value))
    63         }
    65     /**
    66      * The name of the content page.
    67      *
    68      * @see Constants#REQ_ATTR_PAGE_TITLE
    69      */
    70     var pageTitle = ""
    71         set(value) {
    72             field = value
    73             request.setAttribute(Constants.REQ_ATTR_PAGE_TITLE, value)
    74         }
    76     /**
    77      * A list of additional style sheets.
    78      *
    79      * @see Constants#REQ_ATTR_STYLESHEET
    80      */
    81     var styleSheets = emptyList<String>()
    82         set(value) {
    83             field = value
    84             request.setAttribute(Constants.REQ_ATTR_STYLESHEET,
    85                 value.map { it.withExt(".css") }
    86             )
    87         }
    89     /**
    90      * A list of additional style sheets.
    91      *
    92      * @see Constants#REQ_ATTR_JAVASCRIPT
    93      */
    94     var javascript = ""
    95         set(value) {
    96             field = value
    97             request.setAttribute(Constants.REQ_ATTR_JAVASCRIPT,
    98                 value.withExt(".js")
    99             )
   100         }
   102     /**
   103      * The name of the navigation menu JSP.
   104      *
   105      * @see Constants#REQ_ATTR_NAVIGATION
   106      */
   107     var navigationMenu: NavMenu? = null
   108         set(value) {
   109             field = value
   110             request.setAttribute(Constants.REQ_ATTR_NAVIGATION, navigationMenu)
   111         }
   113     var redirectLocation: String? = null
   114         set(value) {
   115             field = value
   116             if (value == null) {
   117                 request.removeAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION)
   118             } else {
   119                 request.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, baseHref + value)
   120             }
   121         }
   123     var feedPath: String? = null
   124         set(value) {
   125             field = value
   126             if (value == null) {
   127                 request.removeAttribute(Constants.REQ_ATTR_FEED_HREF)
   128             } else {
   129                 request.setAttribute(Constants.REQ_ATTR_FEED_HREF, baseHref + value)
   130             }
   131         }
   133     /**
   134      * The view object.
   135      *
   136      * @see Constants#REQ_ATTR_VIEWMODEL
   137      */
   138     var view: View? = null
   139         set(value) {
   140             field = value
   141             request.setAttribute(Constants.REQ_ATTR_VIEWMODEL, value)
   142         }
   144     /**
   145      * Additional port info, if necessary.
   146      */
   147     private val portInfo =
   148         if ((request.scheme == "http" && request.serverPort == 80)
   149             || (request.scheme == "https" && request.serverPort == 443)
   150         ) "" else ":${request.serverPort}"
   152     /**
   153      * The base path of this application.
   154      */
   155     val baseHref get() = "${request.scheme}://${request.serverName}$portInfo${request.contextPath}/"
   157     private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext)
   158     private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp")
   160     fun param(name: String): String? = request.getParameter(name)
   161     fun paramArray(name: String): Array<String> = request.getParameterValues(name) ?: emptyArray()
   163     fun <T> param(name: String, validator: (String?) -> (ValidationResult<T>),
   164                   defaultValue: T, errorMessages: MutableList<String>): T {
   165         return when (val result = validator(param(name))) {
   166             is ValidationError -> {
   167                 errorMessages.add(i18n(result.message))
   168                 defaultValue
   169             }
   170             is ValidatedValue -> {
   171                 result.result
   172             }
   173         }
   174     }
   176     val body: String by lazy {
   177         request.reader.lineSequence().joinToString("\n")
   178     }
   180     private fun forward(jsp: String) {
   181         request.getRequestDispatcher(jspPath(jsp)).forward(request, response)
   182     }
   184     fun renderFeed(page: String? = null) {
   185         page?.let { contentPage = it }
   186         forward("feed")
   187     }
   189     fun render(page: String? = null) {
   190         page?.let { contentPage = it }
   191         forward("site")
   192     }
   194     fun renderCommit(location: String? = null) {
   195         location?.let { redirectLocation = it }
   196         contentPage = Constants.JSP_COMMIT_SUCCESSFUL
   197         render()
   198     }
   200     fun i18n(key: String): String = ResourceBundle.getBundle("localization/strings", response.locale).getString(key)
   201 }
   203 /**
   204  * A path pattern optionally containing placeholders.
   205  *
   206  * The special directories . and .. are disallowed in the pattern.
   207  * Placeholders start with a % sign.
   208  *
   209  * @param pattern the pattern
   210  */
   211 class PathPattern(pattern: String) {
   212     private val nodePatterns: List<String>
   213     private val collection: Boolean
   215     private fun parse(pattern: String): List<String> {
   216         val nodes = pattern.split("/").filter { it.isNotBlank() }.toList()
   217         require(nodes.none { it == "." || it == ".." }) { "Path must not contain '.' or '..' nodes." }
   218         return nodes
   219     }
   221     /**
   222      * Matches a path against this pattern.
   223      * The path must be canonical in the sense that no . or .. parts occur.
   224      *
   225      * @param path the path to match
   226      * @return true if the path matches the pattern, false otherwise
   227      */
   228     fun matches(path: String): Boolean {
   229         if (collection xor path.endsWith("/")) return false
   230         val nodes = parse(path)
   231         if (nodePatterns.size != nodes.size) return false
   232         for (i in nodePatterns.indices) {
   233             val pattern = nodePatterns[i]
   234             val node = nodes[i]
   235             if (pattern.startsWith("%")) continue
   236             if (pattern != node) return false
   237         }
   238         return true
   239     }
   241     /**
   242      * Returns the path parameters found in the specified path using this pattern.
   243      * The return value of this method is undefined, if the patter does not match.
   244      *
   245      * @param path the path
   246      * @return the path parameters, if any, or an empty map
   247      * @see .matches
   248      */
   249     fun obtainPathParameters(path: String): PathParameters {
   250         val params = mutableMapOf<String, String>()
   251         val nodes = parse(path)
   252         for (i in 0 until min(nodes.size, nodePatterns.size)) {
   253             val pattern = nodePatterns[i]
   254             val node = nodes[i]
   255             if (pattern.startsWith("%")) {
   256                 params[pattern.substring(1)] = node
   257             }
   258         }
   259         return params
   260     }
   262     override fun hashCode(): Int {
   263         val str = StringBuilder()
   264         for (node in nodePatterns) {
   265             if (node.startsWith("%")) {
   266                 str.append("/%")
   267             } else {
   268                 str.append('/')
   269                 str.append(node)
   270             }
   271         }
   272         if (collection) str.append('/')
   273         return str.toString().hashCode()
   274     }
   276     override fun equals(other: Any?): Boolean {
   277         if (other is PathPattern) {
   278             if (collection xor other.collection || nodePatterns.size != other.nodePatterns.size) return false
   279             for (i in nodePatterns.indices) {
   280                 val left = nodePatterns[i]
   281                 val right = other.nodePatterns[i]
   282                 if (left.startsWith("%") && right.startsWith("%")) continue
   283                 if (left != right) return false
   284             }
   285             return true
   286         } else {
   287             return false
   288         }
   289     }
   291     init {
   292         nodePatterns = parse(pattern)
   293         collection = pattern.endsWith("/")
   294     }
   295 }
   297 // <editor-fold desc="Validators">
   299 fun dateOptValidator(input: String?): ValidationResult<SqlDate?> {
   300     return if (input.isNullOrBlank()) {
   301         ValidatedValue(null)
   302     } else {
   303         try {
   304             ValidatedValue(SqlDate.valueOf(input))
   305         } catch (ignored: IllegalArgumentException) {
   306             ValidationError("validation.date.format")
   307         }
   308     }
   309 }
   311 fun boolValidator(input: String?): ValidationResult<Boolean> {
   312     return if (input.isNullOrBlank()) {
   313         ValidatedValue(false)
   314     } else {
   315         ValidatedValue(!(input.equals("false", true) || input == "0"))
   316     }
   317 }
   319 // </editor-fold>

mercurial