org.apache.camel.support.RestConsumerContextPathMatcher Maven / Gradle / Ivy
The newest version!
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.support;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.OptionalInt;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* A context path matcher when using rest-dsl that allows components to reuse the same matching logic.
*
* The component should use the {@link #matchBestPath(String, String, List)} with the request details and the matcher
* returns the best matched, or null if none could be determined.
*
* The {@link ConsumerPath} is used for the components to provide the details to the matcher.
*/
public final class RestConsumerContextPathMatcher {
private static final Pattern CONSUMER_PATH_PARAMETER_PATTERN = Pattern.compile("([^{]*)(\\{.*?\\})([^{]*)");
private static final Map PATH_PATTERN = new ConcurrentHashMap<>();
private RestConsumerContextPathMatcher() {
}
/**
* Consumer path details which must be implemented and provided by the components.
*/
public interface ConsumerPath {
/**
* Any HTTP restrict method that would not be allowed
*/
String getRestrictMethod();
/**
* The consumer context-path which may include wildcards
*/
String getConsumerPath();
/**
* The consumer implementation
*/
T getConsumer();
/**
* Whether the consumer match on uri prefix
*/
boolean isMatchOnUriPrefix();
}
/**
* Does the incoming request match the given consumer path (ignore case)
*
* @param requestPath the incoming request context path
* @param consumerPath a consumer path
* @param matchOnUriPrefix whether to use the matchOnPrefix option
* @return true if matched, false otherwise
*/
public static boolean matchPath(String requestPath, String consumerPath, boolean matchOnUriPrefix) {
// deal with null parameters
if (requestPath == null && consumerPath == null) {
return true;
}
if (requestPath == null || consumerPath == null) {
return false;
}
// remove starting/ending slashes
requestPath = removePathSlashes(requestPath);
// remove starting/ending slashes
consumerPath = removePathSlashes(consumerPath);
if (matchOnUriPrefix && requestPath.toLowerCase(Locale.ENGLISH).startsWith(consumerPath.toLowerCase(Locale.ENGLISH))) {
return true;
}
if (requestPath.equalsIgnoreCase(consumerPath)) {
return true;
}
return false;
}
/**
* Finds the best matching of the list of consumer paths that should service the incoming request.
*
* @param requestMethod the incoming request HTTP method
* @param requestPath the incoming request context path
* @param consumerPaths the list of consumer context path details
* @return the best matched consumer, or null if none could be determined.
*/
public static <
T> ConsumerPath matchBestPath(String requestMethod, String requestPath, List> consumerPaths) {
ConsumerPath answer = null;
List> candidates = new ArrayList<>();
// first match by http method
for (ConsumerPath entry : consumerPaths) {
if (matchRestMethod(requestMethod, entry.getRestrictMethod())) {
candidates.add(entry);
}
}
// then see if we got a direct match
Iterator> it = candidates.iterator();
while (it.hasNext()) {
ConsumerPath consumer = it.next();
if (matchRestPath(requestPath, consumer.getConsumerPath(), false)) {
answer = consumer;
break;
}
}
// we could not find a direct match, and if the request is OPTIONS then we need all candidates
if (answer == null && isOptionsMethod(requestMethod)) {
candidates.clear();
candidates.addAll(consumerPaths);
// then try again to see if we can find a direct match
it = candidates.iterator();
while (it.hasNext()) {
ConsumerPath consumer = it.next();
if (matchRestPath(requestPath, consumer.getConsumerPath(), false)) {
answer = consumer;
break;
}
}
}
// if there are no uri template, then select the matching with the longest path
boolean noCurlyBraces = candidates.stream().allMatch(p -> countCurlyBraces(p.getConsumerPath()) == 0);
if (noCurlyBraces) {
// grab first which is the longest that matched the request path
answer = candidates.stream()
.filter(c -> matchPath(requestPath, c.getConsumerPath(), c.isMatchOnUriPrefix()))
// sort by longest by inverting the sort by multiply with -1
.sorted(Comparator.comparingInt(o -> -1 * o.getConsumerPath().length())).findFirst().orElse(null);
}
// is there a direct match by with a different VERB, as then this call is not allowed
if (answer == null) {
for (ConsumerPath entry : consumerPaths) {
if (matchRestPath(requestPath, entry.getConsumerPath(), false)) {
// okay we have direct match but for another VERB so this call is not allowed
return null;
}
}
}
if (answer != null) {
return answer;
}
// then match by uri template path
it = candidates.iterator();
List> uriTemplateCandidates = new ArrayList<>();
while (it.hasNext()) {
ConsumerPath consumer = it.next();
// filter non matching paths
if (matchRestPath(requestPath, consumer.getConsumerPath(), true)) {
uriTemplateCandidates.add(consumer);
}
}
// if there is multiple candidates with uri template then pick anyone with the least number of uri template
ConsumerPath best = null;
Map>> pathMap = new HashMap<>();
if (uriTemplateCandidates.size() > 1) {
it = uriTemplateCandidates.iterator();
while (it.hasNext()) {
ConsumerPath entry = it.next();
int curlyBraces = countCurlyBraces(entry.getConsumerPath());
if (curlyBraces > 0) {
List> consumerPathsList = pathMap.computeIfAbsent(curlyBraces, key -> new ArrayList<>());
consumerPathsList.add(entry);
}
}
OptionalInt min = pathMap.keySet().stream().mapToInt(Integer::intValue).min();
if (min.isPresent()) {
List> bestConsumerPaths = pathMap.get(min.getAsInt());
if (bestConsumerPaths.size() > 1 && !canBeAmbiguous(requestMethod, requestMethod)) {
String exceptionMsg = "Ambiguous paths " + bestConsumerPaths.stream().map(ConsumerPath::getConsumerPath)
.collect(Collectors.joining(",")) + " for request path " + requestPath;
throw new IllegalStateException(exceptionMsg);
}
best = bestConsumerPaths.get(0);
}
if (best != null) {
// pick the best among uri template
answer = best;
}
}
// if there is one left then it's our answer
if (answer == null && uriTemplateCandidates.size() == 1) {
return uriTemplateCandidates.get(0);
}
// last match by wildcard path
it = candidates.iterator();
while (it.hasNext()) {
ConsumerPath consumer = it.next();
// filter non matching paths
if (matchWildCard(requestPath, consumer.getConsumerPath())) {
answer = consumer;
break;
}
}
return answer;
}
/**
* Pre-compiled consumer path for wildcard match
*
* @param consumerPath a consumer path
*/
public static void register(String consumerPath) {
// Remove hyphens and underscores from parameter names
// as these are not supported in regex named group names
String regex = prepareConsumerPathRegex(consumerPath);
// Convert URI template to a regex pattern
regex = regex
.replace("/", "\\/")
.replace("-", "\\-")
.replace("{", "(?<")
.replace("}", ">[^\\/]+)");
// Add support for wildcard * as path suffix
regex = regex.replace("*", ".*");
// Match the provided path against the regex pattern
Pattern pattern = Pattern.compile(regex);
PATH_PATTERN.put(consumerPath, pattern);
}
/**
* if the rest consumer is removed, we also remove pattern cache.
*
* @param consumerPath a consumer path
*/
public static void unRegister(String consumerPath) {
PATH_PATTERN.remove(consumerPath);
}
/**
*
* @param requestMethod The request method
* @param requestPath The request path
* @return if the request method and path can escape from the ambiguous exception
*/
private static boolean canBeAmbiguous(String requestMethod, String requestPath) {
return requestMethod.equalsIgnoreCase("options");
}
/**
* Matches the given request HTTP method with the configured HTTP method of the consumer.
*
* @param method the request HTTP method
* @param restrict the consumer configured HTTP restrict method
* @return true if matched, false otherwise
*/
private static boolean matchRestMethod(String method, String restrict) {
if (restrict == null) {
return true;
}
return restrict.toLowerCase(Locale.ENGLISH).contains(method.toLowerCase(Locale.ENGLISH));
}
/**
* Is the request method OPTIONS
*
* @return true if matched, false otherwise
*/
private static boolean isOptionsMethod(String method) {
return "options".equalsIgnoreCase(method);
}
/**
* Matches the given request path with the configured consumer path
*
* @param requestPath the request path
* @param isUriTemplate the consumer path which may use { } tokens
* @return true if matched, false otherwise
*/
private static boolean matchRestPath(String requestPath, String consumerPath, boolean isUriTemplate) {
// deal with null parameters
if (requestPath == null && consumerPath == null) {
return true;
}
if (requestPath == null || consumerPath == null) {
return false;
}
// remove starting/ending slashes
requestPath = removePathSlashes(requestPath);
// remove starting/ending slashes
consumerPath = removePathSlashes(consumerPath);
// split using single char / is optimized in the jdk
String[] requestPaths = requestPath.split("/");
String[] consumerPaths = consumerPath.split("/");
// must be same number of path's
if (requestPaths.length != consumerPaths.length) {
return false;
}
for (int i = 0; i < requestPaths.length; i++) {
String p1 = requestPaths[i];
String p2 = consumerPaths[i];
if (isUriTemplate && p2.startsWith("{") && p2.endsWith("}")) {
// always matches
continue;
}
if (!matchPath(p1, p2, false)) {
return false;
}
}
// assume matching
return true;
}
private static String removePathSlashes(String path) {
if (path.startsWith("/")) {
path = path.substring(1);
}
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
return path;
}
/**
* Counts the number of uri template's curlyBraces in the path
*
* @param consumerPath the consumer path which may use { } tokens
* @return number of curlyBraces, or 0 if no curlyBraces
*/
private static int countCurlyBraces(String consumerPath) {
int curlyBraces = 0;
// remove starting/ending slashes
consumerPath = removePathSlashes(consumerPath);
String[] consumerPaths = consumerPath.split("/");
for (String p2 : consumerPaths) {
if (p2.startsWith("{") && p2.endsWith("}")) {
curlyBraces++;
}
}
return curlyBraces;
}
private static boolean matchWildCard(String requestPath, String consumerPath) {
if (!requestPath.endsWith("/")) {
requestPath = requestPath + "/";
}
Pattern pattern = PATH_PATTERN.get(consumerPath);
if (pattern == null) {
return false;
}
Matcher matcher = pattern.matcher(requestPath);
return matcher.matches();
}
/**
* Removes any hyphens or underscores from parameter names to create valid java regex named group names
*
* @param consumerPath
* @return
*/
private static String prepareConsumerPathRegex(String consumerPath) {
Matcher m = CONSUMER_PATH_PARAMETER_PATTERN.matcher(consumerPath);
StringBuilder regexBuilder = new StringBuilder(256);
while (m.find()) {
m.appendReplacement(regexBuilder, m.group(1) + m.group(2).replaceAll("[\\_\\-]", "") + m.group(3));
}
// No matches so return the original path
if (regexBuilder.isEmpty()) {
return consumerPath;
} else {
return regexBuilder.toString();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy