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