Skip to content

Commit

Permalink
Support regex-style patterns in @Filter
Browse files Browse the repository at this point in the history
Adds a attribute to the `Filter` annotation, specifying which style of
pattern to use, keeping Ant as the default (avoiding breaking changes).

Also adds proper documentation.

Fixes #5842
  • Loading branch information
racevedoo committed Aug 19, 2021
1 parent 5a14e02 commit e58cef0
Show file tree
Hide file tree
Showing 14 changed files with 212 additions and 11 deletions.
6 changes: 5 additions & 1 deletion core/src/main/java/io/micronaut/core/util/PathMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>true</code> if the given <code>source</code> matches the specified <code>pattern</code>,
Expand Down
35 changes: 35 additions & 0 deletions core/src/main/java/io/micronaut/core/util/RegexPathMatcher.java
Original file line number Diff line number Diff line change
@@ -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<String, Pattern> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -73,6 +73,8 @@ public List<FilterEntry<HttpClientFilter>> 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<HttpMethod> httpMethods = new HashSet<>(Arrays.asList(methods));
if (annotationMetadata.hasStereotype(FilterMatcher.class)) {
httpMethods.addAll(
Expand All @@ -84,6 +86,7 @@ public List<FilterEntry<HttpClientFilter>> resolveFilterEntries(ClientFilterReso
httpClientFilter,
annotationMetadata,
httpMethods,
patternStyle,
annotationMetadata.stringValues(Filter.class)
);
}).filter(entry -> {
Expand Down Expand Up @@ -120,7 +123,7 @@ public List<HttpClientFilter> resolveFilters(HttpRequest<?> request, List<Filter
matches = anyMethodMatches(method, filterEntry.getFilterMethods());
}
if (filterEntry.hasPatterns()) {
matches = matches && anyPatternMatches(requestPath, filterEntry.getPatterns());
matches = matches && anyPatternMatches(requestPath, filterEntry.getPatterns(), filterEntry.getPatternStyle());
}

if (matches) {
Expand All @@ -134,8 +137,8 @@ private boolean containsIdentifier(Collection<String> 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<HttpMethod> methods) {
Expand Down
6 changes: 6 additions & 0 deletions http/src/main/java/io/micronaut/http/annotation/Filter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,6 +52,11 @@
*/
String[] value() default {};

/**
* @return The style of pattern this filter uses
*/
FilterPatternStyle patternStyle() default FilterPatternStyle.ANT;

/**
* Same as {@link #value()}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,27 @@ final class DefaultFilterEntry<T extends HttpFilter> 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<HttpMethod> 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);
}
Expand All @@ -81,6 +85,11 @@ public String[] getPatterns() {
return patterns;
}

@Override
public FilterPatternStyle getPatternStyle() {
return patternStyle;
}

@Override
public boolean hasMethods() {
return hasMethods;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ interface FilterEntry<F> extends AnnotationMetadataProvider {
*/
@NonNull String[] getPatterns();

/**
* @return The filter patterns
*/
default FilterPatternStyle getPatternStyle() {
return FilterPatternStyle.defaultStyle();
}

/**
* @return Does the entry define any methods.
*/
Expand All @@ -103,13 +110,39 @@ default boolean hasPatterns() {
static <FT extends HttpFilter> FilterEntry<FT> of(
@NonNull FT filter,
@Nullable AnnotationMetadata annotationMetadata,
@Nullable Set<HttpMethod> methods, String...patterns) {
@Nullable Set<HttpMethod> 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 <FT> the filter type
*/
static <FT extends HttpFilter> FilterEntry<FT> of(
@NonNull FT filter,
@Nullable AnnotationMetadata annotationMetadata,
@Nullable Set<HttpMethod> 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
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -83,6 +86,7 @@ public void process(BeanDefinition<?> beanDefinition, BeanContext beanContext) {
if (ArrayUtils.isNotEmpty(methods)) {
filterRoute.methods(methods);
}
filterRoute.patternStyle(patternStyle);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -44,6 +45,7 @@ class DefaultFilterRoute implements FilterRoute {
private final Supplier<HttpFilter> filterSupplier;
private final AnnotationMetadataResolver annotationMetadataResolver;
private Set<HttpMethod> httpMethods;
private FilterPatternStyle patternStyle;
private HttpFilter filter;
private AnnotationMetadata annotationMetadata;

Expand Down Expand Up @@ -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<HttpFilter> 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();
Expand Down Expand Up @@ -148,4 +155,10 @@ public FilterRoute methods(HttpMethod... methods) {
}
return this;
}

@Override
public FilterRoute patternStyle(FilterPatternStyle patternStyle) {
this.patternStyle = patternStyle;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -550,12 +550,15 @@ public List<HttpFilter> resolveFilters(HttpRequest<?> request, List<FilterEntry<
if (entry.hasPatterns()) {
String path = request.getPath();
String[] patterns = entry.getPatterns();
FilterPatternStyle patternStyle = entry.getAnnotationMetadata()
.enumValue("patternStyle", FilterPatternStyle.class)
.orElse(FilterPatternStyle.ANT);
boolean matches = true;
for (String pattern : patterns) {
if (!matches) {
break;
}
matches = Filter.MATCH_ALL_PATTERN.equals(pattern) || PathMatcher.ANT.matches(pattern, path);
matches = Filter.MATCH_ALL_PATTERN.equals(pattern) || patternStyle.getPathMatcher().matches(pattern, path);
}
if (!matches) {
continue;
Expand Down
9 changes: 9 additions & 0 deletions router/src/main/java/io/micronaut/web/router/FilterRoute.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.micronaut.web.router;

import io.micronaut.http.HttpMethod;
import io.micronaut.http.filter.FilterPatternStyle;
import io.micronaut.http.filter.HttpFilter;
import io.micronaut.http.filter.HttpFilterResolver;

Expand Down Expand Up @@ -60,4 +61,12 @@ public interface FilterRoute extends HttpFilterResolver.FilterEntry<HttpFilter>
* @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);
}
Loading

0 comments on commit e58cef0

Please sign in to comment.