diff --git a/core/src/main/java/io/micronaut/core/util/PathMatcher.java b/core/src/main/java/io/micronaut/core/util/PathMatcher.java
index a337475433e..7f71cff3a79 100644
--- a/core/src/main/java/io/micronaut/core/util/PathMatcher.java
+++ b/core/src/main/java/io/micronaut/core/util/PathMatcher.java
@@ -25,9 +25,13 @@
*/
public interface PathMatcher {
/**
- * The default Ant style patch matcher.
+ * The default Ant style path matcher.
*/
AntPathMatcher ANT = new AntPathMatcher();
+ /**
+ * The default regex style path matcher.
+ */
+ RegexPathMatcher REGEX = new RegexPathMatcher();
/**
* Returns true
if the given source
matches the specified pattern
,
diff --git a/core/src/main/java/io/micronaut/core/util/RegexPathMatcher.java b/core/src/main/java/io/micronaut/core/util/RegexPathMatcher.java
new file mode 100644
index 00000000000..fd21202a310
--- /dev/null
+++ b/core/src/main/java/io/micronaut/core/util/RegexPathMatcher.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017-2021 original authors
+ *
+ * Licensed 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
+ *
+ * https://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 io.micronaut.core.util;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
+
+/**
+ * PathMatcher implementation for regex-style patterns.
+ * @author Rafael Acevedo
+ * @since 3.1
+ */
+public class RegexPathMatcher implements PathMatcher {
+ private final Map compiledPatterns = new ConcurrentHashMap<>();
+
+ @Override
+ public boolean matches(String pattern, String source) {
+ if (pattern == null || source == null) return false;
+ return compiledPatterns.computeIfAbsent(pattern, Pattern::compile).matcher(source).matches();
+ }
+}
diff --git a/core/src/test/groovy/io/micronaut/core/util/RegexPathMatcherTest.groovy b/core/src/test/groovy/io/micronaut/core/util/RegexPathMatcherTest.groovy
new file mode 100644
index 00000000000..9341bf03209
--- /dev/null
+++ b/core/src/test/groovy/io/micronaut/core/util/RegexPathMatcherTest.groovy
@@ -0,0 +1,14 @@
+package io.micronaut.core.util
+
+import spock.lang.Specification
+
+class RegexPathMatcherTest extends Specification {
+
+ void 'test matches()'() {
+ given:
+ def matcher = new RegexPathMatcher()
+ expect:
+ matcher.matches('^.*$', "/api/v2/endpoint")
+ !matcher.matches('^/api/v1/.*', "/api/v2/endpoint")
+ }
+}
diff --git a/http-client-core/src/main/java/io/micronaut/http/client/filter/DefaultHttpClientFilterResolver.java b/http-client-core/src/main/java/io/micronaut/http/client/filter/DefaultHttpClientFilterResolver.java
index 364e7b46cbd..428cca882a4 100644
--- a/http-client-core/src/main/java/io/micronaut/http/client/filter/DefaultHttpClientFilterResolver.java
+++ b/http-client-core/src/main/java/io/micronaut/http/client/filter/DefaultHttpClientFilterResolver.java
@@ -20,13 +20,13 @@
import io.micronaut.core.annotation.AnnotationMetadataResolver;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.util.ArrayUtils;
-import io.micronaut.core.util.PathMatcher;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.util.Toggleable;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.annotation.FilterMatcher;
+import io.micronaut.http.filter.FilterPatternStyle;
import io.micronaut.http.filter.HttpClientFilter;
import io.micronaut.http.filter.HttpClientFilterResolver;
import jakarta.inject.Singleton;
@@ -73,6 +73,8 @@ public List> resolveFilterEntries(ClientFilterReso
.map(httpClientFilter -> {
AnnotationMetadata annotationMetadata = annotationMetadataResolver.resolveMetadata(httpClientFilter);
HttpMethod[] methods = annotationMetadata.enumValues(Filter.class, "methods", HttpMethod.class);
+ FilterPatternStyle patternStyle = annotationMetadata.enumValue(Filter.class,
+ "patternStyle", FilterPatternStyle.class).orElse(FilterPatternStyle.ANT);
final Set httpMethods = new HashSet<>(Arrays.asList(methods));
if (annotationMetadata.hasStereotype(FilterMatcher.class)) {
httpMethods.addAll(
@@ -84,6 +86,7 @@ public List> resolveFilterEntries(ClientFilterReso
httpClientFilter,
annotationMetadata,
httpMethods,
+ patternStyle,
annotationMetadata.stringValues(Filter.class)
);
}).filter(entry -> {
@@ -120,7 +123,7 @@ public List resolveFilters(HttpRequest> request, List clientIdentifiers, String[
return Arrays.stream(clients).anyMatch(clientIdentifiers::contains);
}
- private boolean anyPatternMatches(String requestPath, String[] patterns) {
- return Arrays.stream(patterns).anyMatch(pattern -> PathMatcher.ANT.matches(pattern, requestPath));
+ private boolean anyPatternMatches(String requestPath, String[] patterns, FilterPatternStyle patternStyle) {
+ return Arrays.stream(patterns).anyMatch(pattern -> patternStyle.getPathMatcher().matches(pattern, requestPath));
}
private boolean anyMethodMatches(HttpMethod requestMethod, Collection methods) {
diff --git a/http/src/main/java/io/micronaut/http/annotation/Filter.java b/http/src/main/java/io/micronaut/http/annotation/Filter.java
index 2bbd45611c1..cc02abdc72c 100644
--- a/http/src/main/java/io/micronaut/http/annotation/Filter.java
+++ b/http/src/main/java/io/micronaut/http/annotation/Filter.java
@@ -17,6 +17,7 @@
import io.micronaut.context.annotation.AliasFor;
import io.micronaut.http.HttpMethod;
+import io.micronaut.http.filter.FilterPatternStyle;
import jakarta.inject.Singleton;
import java.lang.annotation.Documented;
@@ -51,6 +52,11 @@
*/
String[] value() default {};
+ /**
+ * @return The style of pattern this filter uses
+ */
+ FilterPatternStyle patternStyle() default FilterPatternStyle.ANT;
+
/**
* Same as {@link #value()}.
*
diff --git a/http/src/main/java/io/micronaut/http/filter/DefaultFilterEntry.java b/http/src/main/java/io/micronaut/http/filter/DefaultFilterEntry.java
index e41374296af..bfa448c0198 100644
--- a/http/src/main/java/io/micronaut/http/filter/DefaultFilterEntry.java
+++ b/http/src/main/java/io/micronaut/http/filter/DefaultFilterEntry.java
@@ -39,23 +39,27 @@ final class DefaultFilterEntry implements HttpFilterResolv
private final String[] patterns;
private final boolean hasMethods;
private final boolean hasPatterns;
+ private final FilterPatternStyle patternStyle;
/**
* Default constructor.
* @param filter The filter
* @param annotationMetadata The annotation metadata
* @param httpMethods The methods
+ * @param patternStyle the pattern style
* @param patterns THe patterns
*/
DefaultFilterEntry(
T filter,
AnnotationMetadata annotationMetadata,
Set httpMethods,
+ FilterPatternStyle patternStyle,
String[] patterns) {
this.httpFilter = filter;
this.annotationMetadata = annotationMetadata;
this.filterMethods = httpMethods != null ? Collections.unmodifiableSet(httpMethods) : Collections.emptySet();
this.patterns = patterns != null ? patterns : StringUtils.EMPTY_STRING_ARRAY;
+ this.patternStyle = patternStyle != null ? patternStyle : FilterPatternStyle.defaultStyle();
this.hasMethods = CollectionUtils.isNotEmpty(filterMethods);
this.hasPatterns = ArrayUtils.isNotEmpty(patterns);
}
@@ -81,6 +85,11 @@ public String[] getPatterns() {
return patterns;
}
+ @Override
+ public FilterPatternStyle getPatternStyle() {
+ return patternStyle;
+ }
+
@Override
public boolean hasMethods() {
return hasMethods;
diff --git a/http/src/main/java/io/micronaut/http/filter/FilterPatternStyle.java b/http/src/main/java/io/micronaut/http/filter/FilterPatternStyle.java
new file mode 100644
index 00000000000..d97459c5d4e
--- /dev/null
+++ b/http/src/main/java/io/micronaut/http/filter/FilterPatternStyle.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017-2021 original authors
+ *
+ * Licensed 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
+ *
+ * https://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 io.micronaut.http.filter;
+
+import io.micronaut.core.util.PathMatcher;
+
+public enum FilterPatternStyle {
+ /**
+ * Ant-style pattern matching.
+ * @see io.micronaut.core.util.AntPathMatcher
+ */
+ ANT,
+ /**
+ * Regex-style pattern matching.
+ * @see io.micronaut.core.util.RegexPathMatcher
+ */
+ REGEX;
+
+ public PathMatcher getPathMatcher() {
+ return this.equals(FilterPatternStyle.REGEX) ? PathMatcher.REGEX : PathMatcher.ANT;
+ }
+
+ public static FilterPatternStyle defaultStyle() {
+ return ANT;
+ }
+}
diff --git a/http/src/main/java/io/micronaut/http/filter/HttpFilterResolver.java b/http/src/main/java/io/micronaut/http/filter/HttpFilterResolver.java
index 76f058b16bf..50b2fa58c06 100644
--- a/http/src/main/java/io/micronaut/http/filter/HttpFilterResolver.java
+++ b/http/src/main/java/io/micronaut/http/filter/HttpFilterResolver.java
@@ -77,6 +77,13 @@ interface FilterEntry extends AnnotationMetadataProvider {
*/
@NonNull String[] getPatterns();
+ /**
+ * @return The filter patterns
+ */
+ default FilterPatternStyle getPatternStyle() {
+ return FilterPatternStyle.defaultStyle();
+ }
+
/**
* @return Does the entry define any methods.
*/
@@ -103,13 +110,39 @@ default boolean hasPatterns() {
static FilterEntry of(
@NonNull FT filter,
@Nullable AnnotationMetadata annotationMetadata,
- @Nullable Set methods, String...patterns) {
+ @Nullable Set methods,
+ String...patterns) {
return new DefaultFilterEntry<>(
Objects.requireNonNull(filter, "Filter cannot be null"),
annotationMetadata != null ? annotationMetadata : AnnotationMetadata.EMPTY_METADATA,
methods,
+ null,
patterns
);
}
+
+ /**
+ * Creates a filter entry for the given arguments.
+ * @param filter The filter
+ * @param annotationMetadata The annotation metadata
+ * @param methods The methods
+ * @param patternStyle the pattern style
+ * @param patterns The patterns
+ * @return The filter entry
+ * @param the filter type
+ */
+ static FilterEntry of(
+ @NonNull FT filter,
+ @Nullable AnnotationMetadata annotationMetadata,
+ @Nullable Set methods,
+ @NonNull FilterPatternStyle patternStyle, String...patterns) {
+ return new DefaultFilterEntry<>(
+ Objects.requireNonNull(filter, "Filter cannot be null"),
+ annotationMetadata != null ? annotationMetadata : AnnotationMetadata.EMPTY_METADATA,
+ methods,
+ patternStyle,
+ patterns
+ );
+ }
}
}
diff --git a/router/src/main/java/io/micronaut/web/router/AnnotatedFilterRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/AnnotatedFilterRouteBuilder.java
index 40cfe44d59e..40b1214f78a 100644
--- a/router/src/main/java/io/micronaut/web/router/AnnotatedFilterRouteBuilder.java
+++ b/router/src/main/java/io/micronaut/web/router/AnnotatedFilterRouteBuilder.java
@@ -25,6 +25,7 @@
import io.micronaut.http.HttpMethod;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.context.ServerContextPathProvider;
+import io.micronaut.http.filter.FilterPatternStyle;
import io.micronaut.http.filter.HttpClientFilter;
import io.micronaut.http.filter.HttpFilter;
import io.micronaut.inject.BeanDefinition;
@@ -71,6 +72,8 @@ public void process(BeanDefinition> beanDefinition, BeanContext beanContext) {
String[] patterns = getPatterns(beanDefinition);
if (ArrayUtils.isNotEmpty(patterns)) {
HttpMethod[] methods = beanDefinition.enumValues(Filter.class, "methods", HttpMethod.class);
+ FilterPatternStyle patternStyle = beanDefinition.enumValue(Filter.class, "patternStyle",
+ FilterPatternStyle.class).orElse(FilterPatternStyle.ANT);
String first = patterns[0];
@SuppressWarnings("unchecked")
FilterRoute filterRoute = addFilter(first, beanContext, (BeanDefinition extends HttpFilter>) beanDefinition);
@@ -83,6 +86,7 @@ public void process(BeanDefinition> beanDefinition, BeanContext beanContext) {
if (ArrayUtils.isNotEmpty(methods)) {
filterRoute.methods(methods);
}
+ filterRoute.patternStyle(patternStyle);
}
}
diff --git a/router/src/main/java/io/micronaut/web/router/DefaultFilterRoute.java b/router/src/main/java/io/micronaut/web/router/DefaultFilterRoute.java
index 08d5148faa8..552e1989ef7 100644
--- a/router/src/main/java/io/micronaut/web/router/DefaultFilterRoute.java
+++ b/router/src/main/java/io/micronaut/web/router/DefaultFilterRoute.java
@@ -20,6 +20,7 @@
import io.micronaut.core.annotation.AnnotationMetadataResolver;
import io.micronaut.core.util.*;
import io.micronaut.http.HttpMethod;
+import io.micronaut.http.filter.FilterPatternStyle;
import io.micronaut.http.filter.HttpFilter;
import java.net.URI;
@@ -44,6 +45,7 @@ class DefaultFilterRoute implements FilterRoute {
private final Supplier filterSupplier;
private final AnnotationMetadataResolver annotationMetadataResolver;
private Set httpMethods;
+ private FilterPatternStyle patternStyle;
private HttpFilter filter;
private AnnotationMetadata annotationMetadata;
@@ -111,13 +113,18 @@ public String[] getPatterns() {
return patterns.toArray(StringUtils.EMPTY_STRING_ARRAY);
}
+ @Override
+ public FilterPatternStyle getPatternStyle() {
+ return patternStyle != null ? patternStyle : FilterPatternStyle.defaultStyle();
+ }
+
@Override
public Optional match(HttpMethod method, URI uri) {
if (httpMethods != null && !httpMethods.contains(method)) {
return Optional.empty();
}
String uriStr = uri.getPath();
- AntPathMatcher matcher = PathMatcher.ANT;
+ PathMatcher matcher = getPatternStyle().getPathMatcher();
for (String pattern : patterns) {
if (matcher.matches(pattern, uriStr)) {
HttpFilter filter = getFilter();
@@ -148,4 +155,10 @@ public FilterRoute methods(HttpMethod... methods) {
}
return this;
}
+
+ @Override
+ public FilterRoute patternStyle(FilterPatternStyle patternStyle) {
+ this.patternStyle = patternStyle;
+ return this;
+ }
}
diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java
index d6c19649bae..181c2990c7c 100644
--- a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java
+++ b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java
@@ -21,11 +21,11 @@
import io.micronaut.core.order.OrderUtil;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.core.util.CollectionUtils;
-import io.micronaut.core.util.PathMatcher;
import io.micronaut.core.util.SupplierUtil;
import io.micronaut.http.*;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.annotation.FilterMatcher;
+import io.micronaut.http.filter.FilterPatternStyle;
import io.micronaut.http.filter.HttpFilter;
import io.micronaut.http.filter.HttpServerFilterResolver;
import io.micronaut.http.uri.UriMatchTemplate;
@@ -550,12 +550,15 @@ public List resolveFilters(HttpRequest> request, List
* @return This route
*/
FilterRoute methods(HttpMethod... methods);
+
+ /**
+ * Sets the pattern style that this filter route matches.
+ *
+ * @param patternStyle The pattern style
+ * @return This route
+ */
+ FilterRoute patternStyle(FilterPatternStyle patternStyle);
}
diff --git a/router/src/test/groovy/io/micronaut/web/router/DefaultFilterRouteSpec.groovy b/router/src/test/groovy/io/micronaut/web/router/DefaultFilterRouteSpec.groovy
index 05d410e0979..2fd8fa3768c 100644
--- a/router/src/test/groovy/io/micronaut/web/router/DefaultFilterRouteSpec.groovy
+++ b/router/src/test/groovy/io/micronaut/web/router/DefaultFilterRouteSpec.groovy
@@ -19,6 +19,7 @@ import io.micronaut.http.HttpMethod
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.filter.FilterChain
+import io.micronaut.http.filter.FilterPatternStyle
import io.micronaut.http.filter.HttpFilter
import org.reactivestreams.Publisher
import spock.lang.Specification
@@ -72,4 +73,27 @@ class DefaultFilterRouteSpec extends Specification {
route.match(HttpMethod.POST, URI.create('/foo')).isPresent()
route.match(HttpMethod.PUT, URI.create('/foo')).isPresent()
}
+
+ void "test filter route matching with regex pattern style specified"() {
+ given:
+ def filter = new HttpFilter() {
+ @Override
+ Publisher extends HttpResponse>> doFilter(HttpRequest> request, FilterChain chain) {
+ return null
+ }
+ }
+
+ when:
+ def route = new DefaultFilterRoute('/fo(a|o)$', new Supplier() {
+ @Override
+ HttpFilter get() {
+ return filter
+ }
+ }).patternStyle(FilterPatternStyle.REGEX)
+
+ then: //get does not match
+ route.match(HttpMethod.GET, URI.create('/foo')).isPresent()
+ !route.match(HttpMethod.POST, URI.create('/foe')).isPresent()
+ route.match(HttpMethod.PUT, URI.create('/foa')).isPresent()
+ }
}