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) 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> 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() + } }