001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.support; 018 019import java.util.ArrayList; 020import java.util.Comparator; 021import java.util.Iterator; 022import java.util.List; 023import java.util.Locale; 024import java.util.stream.Collectors; 025 026/** 027 * A context path matcher when using rest-dsl that allows components to reuse the same matching logic. 028 * <p/> 029 * The component should use the {@link #matchBestPath(String, String, java.util.List)} with the request details 030 * and the matcher returns the best matched, or <tt>null</tt> if none could be determined. 031 * <p/> 032 * The {@link ConsumerPath} is used for the components to provide the details to the matcher. 033 */ 034public final class RestConsumerContextPathMatcher { 035 036 private RestConsumerContextPathMatcher() { 037 } 038 039 /** 040 * Consumer path details which must be implemented and provided by the components. 041 */ 042 public interface ConsumerPath<T> { 043 044 /** 045 * Any HTTP restrict method that would not be allowed 046 */ 047 String getRestrictMethod(); 048 049 /** 050 * The consumer context-path which may include wildcards 051 */ 052 String getConsumerPath(); 053 054 /** 055 * The consumer implementation 056 */ 057 T getConsumer(); 058 059 /** 060 * Whether the consumer match on uri prefix 061 */ 062 boolean isMatchOnUriPrefix(); 063 064 } 065 066 /** 067 * Does the incoming request match the given consumer path (ignore case) 068 * 069 * @param requestPath the incoming request context path 070 * @param consumerPath a consumer path 071 * @param matchOnUriPrefix whether to use the matchOnPrefix option 072 * @return <tt>true</tt> if matched, <tt>false</tt> otherwise 073 */ 074 public static boolean matchPath(String requestPath, String consumerPath, boolean matchOnUriPrefix) { 075 // deal with null parameters 076 if (requestPath == null && consumerPath == null) { 077 return true; 078 } 079 if (requestPath == null || consumerPath == null) { 080 return false; 081 } 082 083 // remove starting/ending slashes 084 if (requestPath.startsWith("/")) { 085 requestPath = requestPath.substring(1); 086 } 087 if (requestPath.endsWith("/")) { 088 requestPath = requestPath.substring(0, requestPath.length() - 1); 089 } 090 // remove starting/ending slashes 091 if (consumerPath.startsWith("/")) { 092 consumerPath = consumerPath.substring(1); 093 } 094 if (consumerPath.endsWith("/")) { 095 consumerPath = consumerPath.substring(0, consumerPath.length() - 1); 096 } 097 098 String p1 = requestPath.toLowerCase(Locale.ENGLISH); 099 String p2 = consumerPath.toLowerCase(Locale.ENGLISH); 100 101 if (p1.equals(p2)) { 102 return true; 103 } 104 105 if (matchOnUriPrefix && p1.startsWith(p2)) { 106 return true; 107 } 108 109 return false; 110 } 111 112 /** 113 * Finds the best matching of the list of consumer paths that should service the incoming request. 114 * 115 * @param requestMethod the incoming request HTTP method 116 * @param requestPath the incoming request context path 117 * @param consumerPaths the list of consumer context path details 118 * @return the best matched consumer, or <tt>null</tt> if none could be determined. 119 */ 120 public static ConsumerPath matchBestPath(String requestMethod, String requestPath, List<ConsumerPath> consumerPaths) { 121 ConsumerPath answer = null; 122 123 List<ConsumerPath> candidates = new ArrayList<ConsumerPath>(); 124 125 // first match by http method 126 for (ConsumerPath entry : consumerPaths) { 127 if (matchRestMethod(requestMethod, entry.getRestrictMethod())) { 128 candidates.add(entry); 129 } 130 } 131 132 // then see if we got a direct match 133 Iterator<ConsumerPath> it = candidates.iterator(); 134 while (it.hasNext()) { 135 ConsumerPath consumer = it.next(); 136 if (matchRestPath(requestPath, consumer.getConsumerPath(), false)) { 137 answer = consumer; 138 break; 139 } 140 } 141 142 // we could not find a direct match, and if the request is OPTIONS then we need all candidates 143 if (answer == null && isOptionsMethod(requestMethod)) { 144 candidates.clear(); 145 candidates.addAll(consumerPaths); 146 147 // then try again to see if we can find a direct match 148 it = candidates.iterator(); 149 while (it.hasNext()) { 150 ConsumerPath consumer = it.next(); 151 if (matchRestPath(requestPath, consumer.getConsumerPath(), false)) { 152 answer = consumer; 153 break; 154 } 155 } 156 } 157 158 // if there are no wildcards, then select the matching with the longest path 159 boolean noWildcards = candidates.stream().allMatch(p -> countWildcards(p.getConsumerPath()) == 0); 160 if (noWildcards) { 161 // grab first which is the longest that matched the request path 162 answer = candidates.stream() 163 .filter(c -> matchPath(requestPath, c.getConsumerPath(), c.isMatchOnUriPrefix())) 164 // sort by longest by inverting the sort by multiply with -1 165 .sorted(Comparator.comparingInt(o -> -1 * o.getConsumerPath().length())).findFirst().orElse(null); 166 } 167 168 // then match by wildcard path 169 if (answer == null) { 170 it = candidates.iterator(); 171 while (it.hasNext()) { 172 ConsumerPath consumer = it.next(); 173 // filter non matching paths 174 if (!matchRestPath(requestPath, consumer.getConsumerPath(), true)) { 175 it.remove(); 176 } 177 } 178 179 // if there is multiple candidates with wildcards then pick anyone with the least number of wildcards 180 int bestWildcard = Integer.MAX_VALUE; 181 ConsumerPath best = null; 182 if (candidates.size() > 1) { 183 it = candidates.iterator(); 184 while (it.hasNext()) { 185 ConsumerPath entry = it.next(); 186 int wildcards = countWildcards(entry.getConsumerPath()); 187 if (wildcards > 0) { 188 if (best == null || wildcards < bestWildcard) { 189 best = entry; 190 bestWildcard = wildcards; 191 } 192 } 193 } 194 195 if (best != null) { 196 // pick the best among the wildcards 197 answer = best; 198 } 199 } 200 201 // if there is one left then its our answer 202 if (answer == null && candidates.size() == 1) { 203 answer = candidates.get(0); 204 } 205 } 206 207 return answer; 208 } 209 210 /** 211 * Matches the given request HTTP method with the configured HTTP method of the consumer. 212 * 213 * @param method the request HTTP method 214 * @param restrict the consumer configured HTTP restrict method 215 * @return <tt>true</tt> if matched, <tt>false</tt> otherwise 216 */ 217 private static boolean matchRestMethod(String method, String restrict) { 218 if (restrict == null) { 219 return true; 220 } 221 222 return restrict.toLowerCase(Locale.ENGLISH).contains(method.toLowerCase(Locale.ENGLISH)); 223 } 224 225 /** 226 * Is the request method OPTIONS 227 * 228 * @return <tt>true</tt> if matched, <tt>false</tt> otherwise 229 */ 230 private static boolean isOptionsMethod(String method) { 231 return "options".equalsIgnoreCase(method); 232 } 233 234 /** 235 * Matches the given request path with the configured consumer path 236 * 237 * @param requestPath the request path 238 * @param consumerPath the consumer path which may use { } tokens 239 * @return <tt>true</tt> if matched, <tt>false</tt> otherwise 240 */ 241 private static boolean matchRestPath(String requestPath, String consumerPath, boolean wildcard) { 242 // deal with null parameters 243 if (requestPath == null && consumerPath == null) { 244 return true; 245 } 246 if (requestPath == null || consumerPath == null) { 247 return false; 248 } 249 250 // remove starting/ending slashes 251 if (requestPath.startsWith("/")) { 252 requestPath = requestPath.substring(1); 253 } 254 if (requestPath.endsWith("/")) { 255 requestPath = requestPath.substring(0, requestPath.length() - 1); 256 } 257 // remove starting/ending slashes 258 if (consumerPath.startsWith("/")) { 259 consumerPath = consumerPath.substring(1); 260 } 261 if (consumerPath.endsWith("/")) { 262 consumerPath = consumerPath.substring(0, consumerPath.length() - 1); 263 } 264 265 // split using single char / is optimized in the jdk 266 String[] requestPaths = requestPath.split("/"); 267 String[] consumerPaths = consumerPath.split("/"); 268 269 // must be same number of path's 270 if (requestPaths.length != consumerPaths.length) { 271 return false; 272 } 273 274 for (int i = 0; i < requestPaths.length; i++) { 275 String p1 = requestPaths[i]; 276 String p2 = consumerPaths[i]; 277 278 if (wildcard && p2.startsWith("{") && p2.endsWith("}")) { 279 // always matches 280 continue; 281 } 282 283 if (!matchPath(p1, p2, false)) { 284 return false; 285 } 286 } 287 288 // assume matching 289 return true; 290 } 291 292 /** 293 * Counts the number of wildcards in the path 294 * 295 * @param consumerPath the consumer path which may use { } tokens 296 * @return number of wildcards, or <tt>0</tt> if no wildcards 297 */ 298 private static int countWildcards(String consumerPath) { 299 int wildcards = 0; 300 301 // remove starting/ending slashes 302 if (consumerPath.startsWith("/")) { 303 consumerPath = consumerPath.substring(1); 304 } 305 if (consumerPath.endsWith("/")) { 306 consumerPath = consumerPath.substring(0, consumerPath.length() - 1); 307 } 308 309 String[] consumerPaths = consumerPath.split("/"); 310 for (String p2 : consumerPaths) { 311 if (p2.startsWith("{") && p2.endsWith("}")) { 312 wildcards++; 313 } 314 } 315 316 return wildcards; 317 } 318 319}