You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2023/10/19 15:25:40 UTC
[camel] branch main updated: CAMEL-8306: Add support for wildcards to match on prefix (#11638)
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 0c3ba620fa0 CAMEL-8306: Add support for wildcards to match on prefix (#11638)
0c3ba620fa0 is described below
commit 0c3ba620fa040d8afc270e52a6094370d389a939
Author: TUCJVXCB <tt...@hotmail.com>
AuthorDate: Thu Oct 19 23:25:33 2023 +0800
CAMEL-8306: Add support for wildcards to match on prefix (#11638)
* CAMEL-8306: Add support for wildcards to match on prefix
* CAMEL-8306: replace * import
* CAMEL-8306: add unit test
* CAMEL-8306: update docs
* CAMEL-8306: adjust test unit
* CAMEL-8306: adjust priority
* CAMEL-8306: pre-compiled consumer path
* CAMEL-8306: update unit test
* CAMEL-8306: add unit test
---
.../org/apache/camel/catalog/components/rest.json | 2 +-
.../org/apache/camel/http/common/CamelServlet.java | 3 +
.../apache/camel/http/common/CamelServletTest.java | 11 +-
.../HttpServerMultiplexChannelHandler.java | 2 +
.../org/apache/camel/component/rest/rest.json | 2 +-
.../apache/camel/component/rest/RestEndpoint.java | 2 +-
.../undertow/handlers/RestRootHandler.java | 2 +
.../support/RestConsumerContextPathMatcher.java | 117 ++++++++++++++++-----
.../RestConsumerContextPathMatcherTest.java | 46 ++++++++
9 files changed, 154 insertions(+), 33 deletions(-)
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/rest.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/rest.json
index a1a9b9e973c..e11adcdfaa6 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/rest.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/rest.json
@@ -42,7 +42,7 @@
},
"properties": {
"method": { "index": 0, "kind": "path", "displayName": "Method", "group": "common", "label": "common", "required": true, "type": "string", "javaType": "java.lang.String", "enum": [ "get", "post", "put", "delete", "patch", "head", "trace", "connect", "options" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "HTTP method to use." },
- "path": { "index": 1, "kind": "path", "displayName": "Path", "group": "common", "label": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The base path" },
+ "path": { "index": 1, "kind": "path", "displayName": "Path", "group": "common", "label": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The base path, can use * as path suffix to support wildcard HTTP route matching." },
"uriTemplate": { "index": 2, "kind": "path", "displayName": "Uri Template", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The uri template" },
"consumes": { "index": 3, "kind": "parameter", "displayName": "Consumes", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Media type such as: 'text\/xml', or 'application\/json' this REST service accepts. By default we accept all kinds of types." },
"inType": { "index": 4, "kind": "parameter", "displayName": "In Type", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "To declare the incoming POJO binding type as a FQN class name" },
diff --git a/components/camel-http-common/src/main/java/org/apache/camel/http/common/CamelServlet.java b/components/camel-http-common/src/main/java/org/apache/camel/http/common/CamelServlet.java
index d25aa8ae152..25e68e105c8 100644
--- a/components/camel-http-common/src/main/java/org/apache/camel/http/common/CamelServlet.java
+++ b/components/camel-http-common/src/main/java/org/apache/camel/http/common/CamelServlet.java
@@ -43,6 +43,7 @@ import org.apache.camel.Processor;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.spi.ExecutorServiceManager;
import org.apache.camel.support.LifecycleStrategySupport;
+import org.apache.camel.support.RestConsumerContextPathMatcher;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -420,12 +421,14 @@ public class CamelServlet extends HttpServlet implements HttpRegistryProvider {
throw new IllegalStateException("Duplicate request path for " + endpointUri);
}
consumers.put(endpointUri, consumer);
+ RestConsumerContextPathMatcher.register(consumer.getPath());
}
@Override
public void disconnect(HttpConsumer consumer) {
log.debug("Disconnecting consumer: {}", consumer);
consumers.remove(consumer.getEndpoint().getEndpointUri());
+ RestConsumerContextPathMatcher.unRegister(consumer.getPath());
}
@Override
diff --git a/components/camel-http-common/src/test/java/org/apache/camel/http/common/CamelServletTest.java b/components/camel-http-common/src/test/java/org/apache/camel/http/common/CamelServletTest.java
index 1384628122e..7d40887f28a 100644
--- a/components/camel-http-common/src/test/java/org/apache/camel/http/common/CamelServletTest.java
+++ b/components/camel-http-common/src/test/java/org/apache/camel/http/common/CamelServletTest.java
@@ -16,10 +16,16 @@
*/
package org.apache.camel.http.common;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.LinkedHashMap;
+
import org.apache.camel.Consumer;
import org.apache.camel.Processor;
import org.apache.camel.Producer;
import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.util.URISupport;
+import org.apache.camel.util.UnsafeUriCharactersEncoder;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -28,7 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
public class CamelServletTest {
@Test
- public void testDuplicatedServletPath() {
+ public void testDuplicatedServletPath() throws URISyntaxException {
CamelServlet camelServlet = new CamelServlet();
HttpCommonEndpoint httpCommonEndpoint = new HttpCommonEndpoint() {
@@ -47,6 +53,9 @@ public class CamelServletTest {
DefaultCamelContext dc = new DefaultCamelContext();
httpCommonEndpoint.setEndpointUriIfNotSpecified("rest:post://camel.apache.org");
+ httpCommonEndpoint.setHttpUri(URISupport.createRemainingURI(
+ new URI(UnsafeUriCharactersEncoder.encodeHttpURI("servlet:/camel.apache.org?httpMethodRestrict=GET")),
+ new LinkedHashMap<>()));
httpCommonEndpoint.setCamelContext(dc);
HttpConsumer httpConsumer1 = new HttpConsumer(httpCommonEndpoint, null);
diff --git a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerMultiplexChannelHandler.java b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerMultiplexChannelHandler.java
index 151c32beae7..7f69da8fc92 100644
--- a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerMultiplexChannelHandler.java
+++ b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerMultiplexChannelHandler.java
@@ -87,6 +87,7 @@ public class HttpServerMultiplexChannelHandler extends SimpleChannelInboundHandl
@Override
public void addConsumer(NettyHttpConsumer consumer) {
consumers.add(new HttpServerChannelHandler(consumer));
+ RestConsumerContextPathMatcher.register(consumer.getConfiguration().getPath());
}
@Override
@@ -94,6 +95,7 @@ public class HttpServerMultiplexChannelHandler extends SimpleChannelInboundHandl
for (HttpServerChannelHandler handler : consumers) {
if (handler.getConsumer() == consumer) {
consumers.remove(handler);
+ RestConsumerContextPathMatcher.unRegister(consumer.getConfiguration().getPath());
}
}
}
diff --git a/components/camel-rest/src/generated/resources/org/apache/camel/component/rest/rest.json b/components/camel-rest/src/generated/resources/org/apache/camel/component/rest/rest.json
index a1a9b9e973c..e11adcdfaa6 100644
--- a/components/camel-rest/src/generated/resources/org/apache/camel/component/rest/rest.json
+++ b/components/camel-rest/src/generated/resources/org/apache/camel/component/rest/rest.json
@@ -42,7 +42,7 @@
},
"properties": {
"method": { "index": 0, "kind": "path", "displayName": "Method", "group": "common", "label": "common", "required": true, "type": "string", "javaType": "java.lang.String", "enum": [ "get", "post", "put", "delete", "patch", "head", "trace", "connect", "options" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "HTTP method to use." },
- "path": { "index": 1, "kind": "path", "displayName": "Path", "group": "common", "label": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The base path" },
+ "path": { "index": 1, "kind": "path", "displayName": "Path", "group": "common", "label": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The base path, can use * as path suffix to support wildcard HTTP route matching." },
"uriTemplate": { "index": 2, "kind": "path", "displayName": "Uri Template", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The uri template" },
"consumes": { "index": 3, "kind": "parameter", "displayName": "Consumes", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Media type such as: 'text\/xml', or 'application\/json' this REST service accepts. By default we accept all kinds of types." },
"inType": { "index": 4, "kind": "parameter", "displayName": "In Type", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "To declare the incoming POJO binding type as a FQN class name" },
diff --git a/components/camel-rest/src/main/java/org/apache/camel/component/rest/RestEndpoint.java b/components/camel-rest/src/main/java/org/apache/camel/component/rest/RestEndpoint.java
index 8d0dc8c01e9..064232a1f7c 100644
--- a/components/camel-rest/src/main/java/org/apache/camel/component/rest/RestEndpoint.java
+++ b/components/camel-rest/src/main/java/org/apache/camel/component/rest/RestEndpoint.java
@@ -131,7 +131,7 @@ public class RestEndpoint extends DefaultEndpoint {
}
/**
- * The base path
+ * The base path, can use * as path suffix to support wildcard HTTP route matching.
*/
public void setPath(String path) {
this.path = path;
diff --git a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/RestRootHandler.java b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/RestRootHandler.java
index 93ae1c6e88e..ca9d12ffc87 100644
--- a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/RestRootHandler.java
+++ b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/RestRootHandler.java
@@ -62,6 +62,7 @@ public class RestRootHandler implements HttpHandler {
*/
public void addConsumer(UndertowConsumer consumer) {
consumers.add(consumer);
+ RestConsumerContextPathMatcher.register(consumer.getEndpoint().getHttpURI().getPath());
}
/**
@@ -69,6 +70,7 @@ public class RestRootHandler implements HttpHandler {
*/
public void removeConsumer(UndertowConsumer consumer) {
consumers.remove(consumer);
+ RestConsumerContextPathMatcher.unRegister(consumer.getEndpoint().getHttpURI().getPath());
}
/**
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/RestConsumerContextPathMatcher.java b/core/camel-support/src/main/java/org/apache/camel/support/RestConsumerContextPathMatcher.java
index bda24e6bc0d..d8183fad16c 100644
--- a/core/camel-support/src/main/java/org/apache/camel/support/RestConsumerContextPathMatcher.java
+++ b/core/camel-support/src/main/java/org/apache/camel/support/RestConsumerContextPathMatcher.java
@@ -24,18 +24,23 @@ 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.
* <p/>
- * The component should use the {@link #matchBestPath(String, String, java.util.List)} with the request details and the
- * matcher returns the best matched, or <tt>null</tt> if none could be determined.
+ * The component should use the {@link #matchBestPath(String, String, List)} with the request details and the matcher
+ * returns the best matched, or <tt>null</tt> if none could be determined.
* <p/>
* The {@link ConsumerPath} is used for the components to provide the details to the matcher.
*/
public final class RestConsumerContextPathMatcher {
+ private static final Map<String, Pattern> PATH_PATTERN = new ConcurrentHashMap<>();
+
private RestConsumerContextPathMatcher() {
}
@@ -156,9 +161,9 @@ public final class RestConsumerContextPathMatcher {
}
}
- // if there are no wildcards, then select the matching with the longest path
- boolean noWildcards = candidates.stream().allMatch(p -> countWildcards(p.getConsumerPath()) == 0);
- if (noWildcards) {
+ // 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()))
@@ -170,27 +175,28 @@ public final class RestConsumerContextPathMatcher {
return answer;
}
- // then match by wildcard path
+ // then match by uri template path
it = candidates.iterator();
+ List<ConsumerPath<T>> uriTemplateCandidates = new ArrayList<>();
while (it.hasNext()) {
- ConsumerPath<?> consumer = it.next();
+ ConsumerPath<T> consumer = it.next();
// filter non matching paths
- if (!matchRestPath(requestPath, consumer.getConsumerPath(), true)) {
- it.remove();
+ if (matchRestPath(requestPath, consumer.getConsumerPath(), true)) {
+ uriTemplateCandidates.add(consumer);
}
}
- // if there is multiple candidates with wildcards then pick anyone with the least number of wildcards
+ // if there is multiple candidates with uri template then pick anyone with the least number of uri template
ConsumerPath<T> best = null;
Map<Integer, List<ConsumerPath<T>>> pathMap = new HashMap<>();
- if (candidates.size() > 1) {
- it = candidates.iterator();
+ if (uriTemplateCandidates.size() > 1) {
+ it = uriTemplateCandidates.iterator();
while (it.hasNext()) {
ConsumerPath<T> entry = it.next();
- int wildcards = countWildcards(entry.getConsumerPath());
- if (wildcards > 0) {
- List<ConsumerPath<T>> consumerPathsLst = pathMap.computeIfAbsent(wildcards, key -> new ArrayList<>());
- consumerPathsLst.add(entry);
+ int curlyBraces = countCurlyBraces(entry.getConsumerPath());
+ if (curlyBraces > 0) {
+ List<ConsumerPath<T>> consumerPathsList = pathMap.computeIfAbsent(curlyBraces, key -> new ArrayList<>());
+ consumerPathsList.add(entry);
}
}
@@ -206,19 +212,57 @@ public final class RestConsumerContextPathMatcher {
}
if (best != null) {
- // pick the best among the wildcards
+ // pick the best among uri template
answer = best;
}
}
- // if there is one left then its our answer
- if (answer == null && candidates.size() == 1) {
- answer = candidates.get(0);
+ // 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<T> 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) {
+ // Convert URI template to a regex pattern
+ String regex = consumerPath
+ .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
@@ -257,10 +301,10 @@ public final class RestConsumerContextPathMatcher {
* Matches the given request path with the configured consumer path
*
* @param requestPath the request path
- * @param consumerPath the consumer path which may use { } tokens
+ * @param isUriTemplate the consumer path which may use { } tokens
* @return <tt>true</tt> if matched, <tt>false</tt> otherwise
*/
- private static boolean matchRestPath(String requestPath, String consumerPath, boolean wildcard) {
+ private static boolean matchRestPath(String requestPath, String consumerPath, boolean isUriTemplate) {
// deal with null parameters
if (requestPath == null && consumerPath == null) {
return true;
@@ -297,7 +341,7 @@ public final class RestConsumerContextPathMatcher {
String p1 = requestPaths[i];
String p2 = consumerPaths[i];
- if (wildcard && p2.startsWith("{") && p2.endsWith("}")) {
+ if (isUriTemplate && p2.startsWith("{") && p2.endsWith("}")) {
// always matches
continue;
}
@@ -312,13 +356,13 @@ public final class RestConsumerContextPathMatcher {
}
/**
- * Counts the number of wildcards in the path
+ * Counts the number of uri template's curlyBraces in the path
*
* @param consumerPath the consumer path which may use { } tokens
- * @return number of wildcards, or <tt>0</tt> if no wildcards
+ * @return number of curlyBraces, or <tt>0</tt> if no curlyBraces
*/
- private static int countWildcards(String consumerPath) {
- int wildcards = 0;
+ private static int countCurlyBraces(String consumerPath) {
+ int curlyBraces = 0;
// remove starting/ending slashes
if (consumerPath.startsWith("/")) {
@@ -331,11 +375,26 @@ public final class RestConsumerContextPathMatcher {
String[] consumerPaths = consumerPath.split("/");
for (String p2 : consumerPaths) {
if (p2.startsWith("{") && p2.endsWith("}")) {
- wildcards++;
+ curlyBraces++;
}
}
- return wildcards;
+ 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();
}
}
diff --git a/core/camel-support/src/test/java/org/apache/camel/support/RestConsumerContextPathMatcherTest.java b/core/camel-support/src/test/java/org/apache/camel/support/RestConsumerContextPathMatcherTest.java
index a9b3fac1b72..d079e03b701 100644
--- a/core/camel-support/src/test/java/org/apache/camel/support/RestConsumerContextPathMatcherTest.java
+++ b/core/camel-support/src/test/java/org/apache/camel/support/RestConsumerContextPathMatcherTest.java
@@ -79,4 +79,50 @@ public class RestConsumerContextPathMatcherTest {
"/camel/a/b/3", consumerPaths);
assertEquals(path.getConsumerPath(), "/camel/a/b/{c}");
}
+
+ @Test
+ public void testRestConsumerContextPathMatcherWithWildcard() {
+ List<RestConsumerContextPathMatcher.ConsumerPath<MockConsumerPath>> consumerPaths = new ArrayList<>();
+ consumerPaths.add(new MockConsumerPath("GET", "/camel/myapp/info"));
+ consumerPaths.add(new MockConsumerPath("GET", "/camel/myapp/{id}"));
+ consumerPaths.add(new MockConsumerPath("GET", "/camel/myapp/order/*"));
+
+ RestConsumerContextPathMatcher.register("/camel/myapp/order/*");
+
+ RestConsumerContextPathMatcher.ConsumerPath<?> path1 = RestConsumerContextPathMatcher.matchBestPath("GET",
+ "/camel/myapp/info", consumerPaths);
+
+ RestConsumerContextPathMatcher.ConsumerPath<?> path2 = RestConsumerContextPathMatcher.matchBestPath("GET",
+ "/camel/myapp/1", consumerPaths);
+
+ RestConsumerContextPathMatcher.ConsumerPath<?> path3 = RestConsumerContextPathMatcher.matchBestPath("GET",
+ "/camel/myapp/order/foo", consumerPaths);
+
+ assertEquals(path1.getConsumerPath(), "/camel/myapp/info");
+ assertEquals(path2.getConsumerPath(), "/camel/myapp/{id}");
+ assertEquals(path3.getConsumerPath(), "/camel/myapp/order/*");
+ }
+
+ @Test
+ public void testRestConsumerContextPathMatcherOrder() {
+ List<RestConsumerContextPathMatcher.ConsumerPath<MockConsumerPath>> consumerPaths = new ArrayList<>();
+ consumerPaths.add(new MockConsumerPath("GET", "/camel/*"));
+ consumerPaths.add(new MockConsumerPath("GET", "/camel/foo"));
+ consumerPaths.add(new MockConsumerPath("GET", "/camel/foo/{id}"));
+
+ RestConsumerContextPathMatcher.register("/camel/*");
+
+ RestConsumerContextPathMatcher.ConsumerPath<?> path1 = RestConsumerContextPathMatcher.matchBestPath("GET",
+ "/camel/foo", consumerPaths);
+
+ RestConsumerContextPathMatcher.ConsumerPath<?> path2 = RestConsumerContextPathMatcher.matchBestPath("GET",
+ "/camel/foo/bar", consumerPaths);
+
+ RestConsumerContextPathMatcher.ConsumerPath<?> path3 = RestConsumerContextPathMatcher.matchBestPath("GET",
+ "/camel/foo/bar/1", consumerPaths);
+
+ assertEquals(path1.getConsumerPath(), "/camel/foo");
+ assertEquals(path2.getConsumerPath(), "/camel/foo/{id}");
+ assertEquals(path3.getConsumerPath(), "/camel/*");
+ }
}