1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Tue Jan 05 19:19:31 2021 +0100 1.3 @@ -0,0 +1,155 @@ 1.4 +/* 1.5 + * Copyright 2021 Mike Becker. All rights reserved. 1.6 + * 1.7 + * Redistribution and use in source and binary forms, with or without 1.8 + * modification, are permitted provided that the following conditions are met: 1.9 + * 1.10 + * 1. Redistributions of source code must retain the above copyright 1.11 + * notice, this list of conditions and the following disclaimer. 1.12 + * 1.13 + * 2. Redistributions in binary form must reproduce the above copyright 1.14 + * notice, this list of conditions and the following disclaimer in the 1.15 + * documentation and/or other materials provided with the distribution. 1.16 + * 1.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 1.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 1.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 1.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 1.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 1.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 1.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 1.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 1.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 1.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 1.27 + */ 1.28 + 1.29 +package de.uapcore.lightpit 1.30 + 1.31 +import kotlin.math.min 1.32 + 1.33 +/** 1.34 + * Maps requests to methods. 1.35 + * 1.36 + * This annotation is used to annotate methods within classes which 1.37 + * override [AbstractServlet]. 1.38 + */ 1.39 +@MustBeDocumented 1.40 +@Retention(AnnotationRetention.RUNTIME) 1.41 +@Target(AnnotationTarget.FUNCTION) 1.42 +annotation class RequestMapping( 1.43 + 1.44 + /** 1.45 + * Specifies the HTTP method. 1.46 + * 1.47 + * @return the HTTP method handled by the annotated Java method 1.48 + */ 1.49 + val method: HttpMethod, 1.50 + 1.51 + /** 1.52 + * Specifies the request path relative to the module path. 1.53 + * The trailing slash is important. 1.54 + * A node may start with a dollar ($) sign. 1.55 + * This part of the path is then treated as an path parameter. 1.56 + * Path parameters can be obtained by including the [PathParameters] type in the signature. 1.57 + * 1.58 + * @return the request path the annotated method should handle 1.59 + */ 1.60 + val requestPath: String = "/" 1.61 +) 1.62 + 1.63 +class PathParameters : HashMap<String, String>() 1.64 + 1.65 +/** 1.66 + * A path pattern optionally containing placeholders. 1.67 + * 1.68 + * The special directories . and .. are disallowed in the pattern. 1.69 + * Placeholders start with a $ sign. 1.70 + * 1.71 + * @param pattern the pattern 1.72 + */ 1.73 +class PathPattern(pattern: String) { 1.74 + private val nodePatterns: List<String> 1.75 + private val collection: Boolean 1.76 + 1.77 + private fun parse(pattern: String): List<String> { 1.78 + val nodes = pattern.split("/").filter { it.isNotBlank() }.toList() 1.79 + require(nodes.none { it == "." || it == ".." }) { "Path must not contain '.' or '..' nodes." } 1.80 + return nodes 1.81 + } 1.82 + 1.83 + /** 1.84 + * Matches a path against this pattern. 1.85 + * The path must be canonical in the sense that no . or .. parts occur. 1.86 + * 1.87 + * @param path the path to match 1.88 + * @return true if the path matches the pattern, false otherwise 1.89 + */ 1.90 + fun matches(path: String): Boolean { 1.91 + if (collection xor path.endsWith("/")) return false 1.92 + val nodes = parse(path) 1.93 + if (nodePatterns.size != nodes.size) return false 1.94 + for (i in nodePatterns.indices) { 1.95 + val pattern = nodePatterns[i] 1.96 + val node = nodes[i] 1.97 + if (pattern.startsWith("$")) continue 1.98 + if (pattern != node) return false 1.99 + } 1.100 + return true 1.101 + } 1.102 + 1.103 + /** 1.104 + * Returns the path parameters found in the specified path using this pattern. 1.105 + * The return value of this method is undefined, if the patter does not match. 1.106 + * 1.107 + * @param path the path 1.108 + * @return the path parameters, if any, or an empty map 1.109 + * @see .matches 1.110 + */ 1.111 + fun obtainPathParameters(path: String): PathParameters { 1.112 + val params = PathParameters() 1.113 + val nodes = parse(path) 1.114 + for (i in 0 until min(nodes.size, nodePatterns.size)) { 1.115 + val pattern = nodePatterns[i] 1.116 + val node = nodes[i] 1.117 + if (pattern.startsWith("$")) { 1.118 + params[pattern.substring(1)] = node 1.119 + } 1.120 + } 1.121 + return params 1.122 + } 1.123 + 1.124 + override fun hashCode(): Int { 1.125 + val str = StringBuilder() 1.126 + for (node in nodePatterns) { 1.127 + if (node.startsWith("$")) { 1.128 + str.append("/$") 1.129 + } else { 1.130 + str.append('/') 1.131 + str.append(node) 1.132 + } 1.133 + } 1.134 + if (collection) str.append('/') 1.135 + return str.toString().hashCode() 1.136 + } 1.137 + 1.138 + override fun equals(other: Any?): Boolean { 1.139 + if (other is PathPattern) { 1.140 + if (collection xor other.collection || nodePatterns.size != other.nodePatterns.size) return false 1.141 + for (i in nodePatterns.indices) { 1.142 + val left = nodePatterns[i] 1.143 + val right = other.nodePatterns[i] 1.144 + if (left.startsWith("$") && right.startsWith("$")) continue 1.145 + if (left != right) return false 1.146 + } 1.147 + return true 1.148 + } else { 1.149 + return false 1.150 + } 1.151 + } 1.152 + 1.153 + init { 1.154 + nodePatterns = parse(pattern) 1.155 + collection = pattern.endsWith("/") 1.156 + } 1.157 +} 1.158 +