Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support regex-style patterns in @Filter #5954

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
racevedoo marked this conversation as resolved.
Show resolved Hide resolved
@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