DefaultRouter.java
/*
* Copyright 2017-2020 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.web.router;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.order.OrderUtil;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.SupplierUtil;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.annotation.FilterMatcher;
import io.micronaut.http.filter.FilterPatternStyle;
import io.micronaut.http.filter.FilterRunner;
import io.micronaut.http.filter.GenericHttpFilter;
import io.micronaut.http.filter.HttpServerFilterResolver;
import io.micronaut.http.uri.UriMatchTemplate;
import io.micronaut.web.router.exceptions.DuplicateRouteException;
import io.micronaut.web.router.exceptions.RoutingException;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Stream;
/**
* <p>The default {@link Router} implementation. This implementation does not perform any additional caching of
* route discovery.</p>
*
* @author Graeme Rocher
* @since 1.0
*/
@Singleton
public class DefaultRouter implements Router, HttpServerFilterResolver<RouteMatch<?>> {
private static final UriRouteInfo<Object, Object>[] EMPTY = new UriRouteInfo[0];
private final EnumMap<HttpMethod, UriRouteInfo<Object, Object>[]> methodRoutesByMethod;
private final Map<String, UriRouteInfo<Object, Object>[]> allRoutesByMethod;
private final StatusRouteInfo<Object, Object>[] statusRoutes;
private final ErrorRouteInfo<Object, Object>[] errorRoutes;
private final Set<Integer> exposedPorts;
@Nullable
private Set<Integer> ports;
private final List<FilterRoute> alwaysMatchesFilterRoutes;
private final List<FilterRoute> preconditionFilterRoutes;
private final List<FilterRoute> preMatchingAlwaysMatchesFilterRoutes;
private final List<FilterRoute> preMatchingPreconditionFilterRoutes;
// ArrayList to avoid interface checkcast
private final Supplier<ArrayList<GenericHttpFilter>> alwaysMatchesHttpFilters;
private final Supplier<ArrayList<GenericHttpFilter>> preMatchingAlwaysMatchesHttpFilters;
/**
* Construct a new router for the given route builders.
*
* @param builders The builders
*/
public DefaultRouter(RouteBuilder... builders) {
this(Arrays.asList(builders));
}
/**
* Construct a new router for the given route builders.
*
* @param builders The builders
*/
@Inject
public DefaultRouter(Collection<RouteBuilder> builders) {
Set<Integer> exposedPorts = new HashSet<>(5);
Map<String, List<UriRouteInfo<Object, Object>>> customRoutesByMethod = new HashMap<>();
EnumMap<HttpMethod, List<UriRouteInfo<Object, Object>>> routesByMethod = new EnumMap<>(HttpMethod.class);
Set<StatusRouteInfo<Object, Object>> statusRoutes = new LinkedHashSet<>();
Set<ErrorRouteInfo<Object, Object>> errorRoutes = new LinkedHashSet<>();
alwaysMatchesFilterRoutes = new ArrayList<>(20);
preconditionFilterRoutes = new ArrayList<>(20);
preMatchingAlwaysMatchesFilterRoutes = new ArrayList<>(10);
preMatchingPreconditionFilterRoutes = new ArrayList<>(10);
for (RouteBuilder builder : builders) {
List<UriRoute> constructedRoutes = builder.getUriRoutes();
for (UriRoute route : constructedRoutes) {
HttpMethod httpMethod = route.getHttpMethod();
UriRouteInfo<Object, Object> uriRouteInfo = route.toRouteInfo();
if (httpMethod == HttpMethod.CUSTOM) {
String key = route.getHttpMethodName();
customRoutesByMethod.computeIfAbsent(key, x -> new ArrayList<>()).add(uriRouteInfo);
} else {
routesByMethod.computeIfAbsent(httpMethod, x -> new ArrayList<>()).add(uriRouteInfo);
}
}
for (StatusRoute statusRoute : builder.getStatusRoutes()) {
StatusRouteInfo<Object, Object> routeInfo = statusRoute.toRouteInfo();
if (statusRoutes.contains(routeInfo)) {
final StatusRouteInfo<Object, Object> existing = statusRoutes.stream().filter(r -> r.equals(routeInfo)).findFirst().orElse(null);
throw new RoutingException("Attempted to register multiple local routes for http status [" + statusRoute.statusCode() + "]. New route: " + statusRoute + ". Existing: " + existing);
}
statusRoutes.add(routeInfo);
}
for (ErrorRoute errorRoute : builder.getErrorRoutes()) {
ErrorRouteInfo<Object, Object> routeInfo = errorRoute.toRouteInfo();
if (errorRoutes.contains(routeInfo)) {
final ErrorRouteInfo<Object, Object> existing = errorRoutes.stream().filter(r -> r.equals(routeInfo)).findFirst().orElse(null);
throw new RoutingException("Attempted to register multiple local routes for error [" + errorRoute.exceptionType().getSimpleName() + "]. New route: " + errorRoute + ". Existing: " + existing);
}
errorRoutes.add(routeInfo);
}
for (FilterRoute filterRoute : builder.getFilterRoutes()) {
if (filterRoute.isPreMatching()) {
if (isMatchesAll(filterRoute)) {
preMatchingAlwaysMatchesFilterRoutes.add(filterRoute);
} else {
preMatchingPreconditionFilterRoutes.add(filterRoute);
}
} else if (isMatchesAll(filterRoute)) {
alwaysMatchesFilterRoutes.add(filterRoute);
} else {
preconditionFilterRoutes.add(filterRoute);
}
}
exposedPorts.addAll(builder.getExposedPorts());
}
if (CollectionUtils.isNotEmpty(exposedPorts)) {
this.exposedPorts = exposedPorts;
} else {
this.exposedPorts = Collections.emptySet();
}
EnumMap<HttpMethod, UriRouteInfo<Object, Object>[]> methodMap = new EnumMap<>(HttpMethod.class);
Map<String, UriRouteInfo<Object, Object>[]> customMethodMap = CollectionUtils.newHashMap(routesByMethod.size() + customRoutesByMethod.size());
for (Map.Entry<HttpMethod, List<UriRouteInfo<Object, Object>>> e : routesByMethod.entrySet()) {
UriRouteInfo<Object, Object>[] values = finalizeRoutes(e.getValue());
methodMap.put(e.getKey(), values);
customMethodMap.put(e.getKey().name(), values);
}
for (Map.Entry<String, List<UriRouteInfo<Object, Object>>> e : customRoutesByMethod.entrySet()) {
customMethodMap.put(e.getKey(), finalizeRoutes(e.getValue()));
}
this.methodRoutesByMethod = methodMap;
this.allRoutesByMethod = customMethodMap;
this.statusRoutes = statusRoutes.toArray(StatusRouteInfo[]::new);
this.errorRoutes = errorRoutes.toArray(ErrorRouteInfo[]::new);
this.alwaysMatchesHttpFilters = SupplierUtil.memoized(() -> {
if (alwaysMatchesFilterRoutes.isEmpty()) {
return new ArrayList<>(0);
}
ArrayList<GenericHttpFilter> httpFilters = new ArrayList<>(alwaysMatchesFilterRoutes.size());
for (FilterRoute filterRoute : alwaysMatchesFilterRoutes) {
httpFilters.add(filterRoute.getFilter());
}
FilterRunner.sort(httpFilters);
return httpFilters;
});
this.preMatchingAlwaysMatchesHttpFilters = SupplierUtil.memoized(() -> {
if (preMatchingAlwaysMatchesFilterRoutes.isEmpty()) {
return new ArrayList<>(0);
}
ArrayList<GenericHttpFilter> httpFilters = new ArrayList<>(preMatchingAlwaysMatchesFilterRoutes.size());
for (FilterRoute filterRoute : preMatchingAlwaysMatchesFilterRoutes) {
httpFilters.add(filterRoute.getFilter());
}
FilterRunner.sort(httpFilters);
return httpFilters;
});
}
private boolean isMatchesAll(FilterRoute filterRoute) {
if (filterRoute.getAnnotationMetadata().hasStereotype(FilterMatcher.NAME)) {
return false;
}
if (filterRoute.hasMethods()) {
return false;
}
if (filterRoute.hasPatterns()) {
for (String pattern : filterRoute.getPatterns()) {
if (!Filter.MATCH_ALL_PATTERN.equals(pattern)) {
return false;
}
}
}
return true;
}
@Override
public Set<Integer> getExposedPorts() {
return exposedPorts;
}
@Override
public void applyDefaultPorts(List<Integer> ports) {
this.ports = new HashSet<>(ports);
}
@NonNull
@Override
public <T, R> Stream<UriRouteMatch<T, R>> find(@NonNull HttpRequest<?> request, @NonNull CharSequence uri) {
return this.<T, R>toMatches(uri.toString(), findInternal(request)).stream();
}
@NonNull
@Override
public <T, R> Stream<UriRouteMatch<T, R>> find(@NonNull HttpRequest<?> request) {
return this.<T, R>toMatches(request.getPath(), findInternal(request)).stream();
}
@NonNull
@Override
public <T, R> Stream<UriRouteMatch<T, R>> find(@NonNull HttpMethod httpMethod, @NonNull CharSequence uri, @Nullable HttpRequest<?> context) {
return this.<T, R>toMatches(
uri.toString(),
allRoutesByMethod.getOrDefault(httpMethod.name(), EMPTY)
).stream();
}
@NonNull
@Override
public Stream<UriRouteInfo<?, ?>> uriRoutes() {
return Stream.concat(
allRoutesByMethod.values().stream().flatMap(Arrays::stream),
allRoutesByMethod.values().stream().flatMap(Arrays::stream)
);
}
@Override
public <T, R> UriRouteMatch<T, R> findClosest(@NonNull HttpRequest<?> request) throws DuplicateRouteException {
List<UriRouteInfo<Object, Object>> routes = findInternal(request);
if (routes.isEmpty()) {
return null;
}
String path = request.getPath();
if (routes.size() == 1) {
Object o = routes.iterator().next();
// avoid type pollution perf issues
UriRouteInfo next = o instanceof DefaultUrlRouteInfo def ? def : (UriRouteInfo<Object, Object>) o;
return (UriRouteMatch) next.tryMatch(path);
}
List<UriRouteMatch<T, R>> uriRoutes = new ArrayList<>(routes.size());
for (UriRouteInfo<Object, Object> route : routes) {
UriRouteMatch match = route.tryMatch(path);
if (match != null) {
uriRoutes.add(match);
}
}
if (uriRoutes.size() == 1) {
Object obj = uriRoutes.get(0);
// type pollution avoidance (should be covered by type pollution test)
return obj instanceof DefaultUriRouteMatch<?, ?> def ? (DefaultUriRouteMatch<T, R>) def : (UriRouteMatch<T, R>) obj;
}
uriRoutes = resolveAmbiguity(request, uriRoutes);
if (uriRoutes.size() > 1) {
throw new DuplicateRouteException(path, (List) uriRoutes);
} else if (uriRoutes.size() == 1) {
return uriRoutes.get(0);
}
return null;
}
@NonNull
@Override
public <T, R> List<UriRouteMatch<T, R>> findAllClosest(@NonNull HttpRequest<?> request) {
List<UriRouteInfo<Object, Object>> routes = findInternal(request);
if (routes.isEmpty()) {
return Collections.emptyList();
}
List<UriRouteMatch<T, R>> uriRoutes = toMatches(request.getPath(), routes);
if (uriRoutes.size() == 1) {
return uriRoutes;
}
return resolveAmbiguity(request, uriRoutes);
}
private <T, R> List<UriRouteMatch<T, R>> resolveAmbiguity(HttpRequest<?> request,
List<UriRouteMatch<T, R>> uriRoutes) {
// if there are multiple routes, try to resolve the ambiguity
final Collection<MediaType> acceptedProducedTypes = request.accept();
if (CollectionUtils.isNotEmpty(acceptedProducedTypes)) {
// take the highest priority accepted type
final MediaType mediaType = acceptedProducedTypes.iterator().next();
var mostSpecific = new ArrayList<UriRouteMatch<T, R>>(uriRoutes.size());
for (UriRouteMatch<T, R> routeMatch : uriRoutes) {
if (routeMatch.getRouteInfo().explicitlyProduces(mediaType)) {
mostSpecific.add(routeMatch);
}
}
if (!mostSpecific.isEmpty()) {
uriRoutes = mostSpecific;
}
}
boolean permitsBody = request.getMethod().permitsRequestBody();
int routeCount = uriRoutes.size();
if (routeCount > 1 && permitsBody) {
final MediaType contentType = request.getContentType().orElse(MediaType.ALL_TYPE);
var explicitlyConsumedRoutes = new ArrayList<UriRouteMatch<T, R>>(routeCount);
var consumesRoutes = new ArrayList<UriRouteMatch<T, R>>(routeCount);
for (UriRouteMatch<T, R> match : uriRoutes) {
if (match.getRouteInfo().explicitlyConsumes(contentType)) {
explicitlyConsumedRoutes.add(match);
}
if (explicitlyConsumedRoutes.isEmpty()) {
consumesRoutes.add(match);
}
}
uriRoutes = explicitlyConsumedRoutes.isEmpty() ? consumesRoutes : explicitlyConsumedRoutes;
}
/*
* Any changes to the logic below may also need changes to {@link io.micronaut.http.uri.UriTemplate#compareTo(UriTemplate)}
*/
routeCount = uriRoutes.size();
if (routeCount > 1) {
long variableCount = 0;
long rawLength = 0;
var closestMatches = new ArrayList<UriRouteMatch<T, R>>(routeCount);
for (int i = 0; i < routeCount; i++) {
UriRouteMatch<T, R> match = uriRoutes.get(i);
UriMatchTemplate template = match.getRouteInfo().getUriMatchTemplate();
long variable = template.getPathVariableSegmentCount();
long raw = template.getRawSegmentLength();
if (i == 0) {
variableCount = variable;
rawLength = raw;
}
if (variable > variableCount || raw < rawLength) {
break;
}
closestMatches.add(match);
}
uriRoutes = closestMatches;
}
return uriRoutes;
}
private <T, R> List<UriRouteMatch<T, R>> toMatches(String path, List<UriRouteInfo<Object, Object>> routes) {
if (routes.size() == 1) {
UriRouteMatch match = routes.iterator().next().tryMatch(path);
if (match != null) {
return List.of(match);
}
return List.of();
}
var uriRoutes = new ArrayList<UriRouteMatch<T, R>>(routes.size());
for (UriRouteInfo<Object, Object> route : routes) {
UriRouteMatch match = route.tryMatch(path);
if (match != null) {
uriRoutes.add(match);
}
}
return uriRoutes;
}
private <T, R> List<UriRouteMatch<T, R>> toMatches(String path, UriRouteInfo<Object, Object>[] routes) {
if (routes.length == 1) {
UriRouteMatch match = routes[0].tryMatch(path);
if (match != null) {
return List.of(match);
}
return List.of();
}
var uriRoutes = new ArrayList<UriRouteMatch<T, R>>(routes.length);
for (UriRouteInfo<Object, Object> route : routes) {
UriRouteMatch match = route.tryMatch(path);
if (match != null) {
uriRoutes.add(match);
}
}
return uriRoutes;
}
@NonNull
@Override
public <T, R> Optional<UriRouteMatch<T, R>> route(@NonNull HttpMethod httpMethod, @NonNull CharSequence uri) {
for (UriRouteInfo<Object, Object> uriRouteInfo : methodRoutesByMethod.getOrDefault(httpMethod, EMPTY)) {
Optional<UriRouteMatch<Object, Object>> match = uriRouteInfo.match(uri.toString());
if (match.isPresent()) {
return (Optional) match;
}
}
return Optional.empty();
}
@Override
public <R> Optional<RouteMatch<R>> route(@NonNull HttpStatus status) {
for (StatusRouteInfo<Object, Object> statusRouteInfo : statusRoutes) {
if (statusRouteInfo.originatingType() == null) {
Optional<RouteMatch<Object>> match = statusRouteInfo.match(status);
if (match.isPresent()) {
return (Optional) match;
}
}
}
return Optional.empty();
}
@Override
public <R> Optional<RouteMatch<R>> route(@NonNull Class<?> originatingClass, @NonNull HttpStatus status) {
for (StatusRouteInfo<Object, Object> statusRouteInfo : statusRoutes) {
Optional<RouteMatch<Object>> match = statusRouteInfo.match(originatingClass, status);
if (match.isPresent()) {
return (Optional) match;
}
}
return Optional.empty();
}
@Override
public <R> Optional<RouteMatch<R>> route(@NonNull Class<?> originatingClass, @NonNull Throwable error) {
var matchedRoutes = new ArrayList<RouteMatch<R>>();
for (ErrorRouteInfo<Object, Object> errorRouteInfo : errorRoutes) {
Optional match = errorRouteInfo.match(originatingClass, error);
match.ifPresent(m ->
matchedRoutes.add((RouteMatch<R>) m)
);
}
return findRouteMatch(matchedRoutes, error);
}
@Override
public <R> Optional<RouteMatch<R>> findErrorRoute(
@NonNull Class<?> originatingClass,
@NonNull Throwable error,
HttpRequest<?> request) {
return findErrorRouteInternal(originatingClass, error, request);
}
private <R> Optional<RouteMatch<R>> findErrorRouteInternal(
@Nullable Class<?> originatingClass,
@NonNull Throwable error, HttpRequest<?> request) {
Collection<MediaType> accept = request.accept();
final boolean hasAcceptHeader = CollectionUtils.isNotEmpty(accept);
if (hasAcceptHeader) {
var matchedRoutes = new ArrayList<RouteMatch<R>>();
for (ErrorRouteInfo<Object, Object> errorRoute : errorRoutes) {
if (!errorRoute.doesProduce(accept)) {
continue;
}
if (!errorRoute.matching(request)) {
continue;
}
@SuppressWarnings("unchecked")
final var match = (RouteMatch<R>) errorRoute.match(originatingClass, error).orElse(null);
if (match != null) {
matchedRoutes.add(match);
}
}
return findRouteMatch(matchedRoutes, error);
} else {
var producesAllMatchedRoutes = new ArrayList<RouteMatch<R>>(errorRoutes.length);
var producesSpecificMatchedRoutes = new ArrayList<RouteMatch<R>>(errorRoutes.length);
for (ErrorRouteInfo<Object, Object> errorRouteInfo : errorRoutes) {
if (!errorRouteInfo.matching(request)) {
continue;
}
@SuppressWarnings("unchecked") final RouteMatch<R> match = (RouteMatch<R>) errorRouteInfo
.match(originatingClass, error).orElse(null);
if (match != null) {
final List<MediaType> produces = match.getRouteInfo().getProduces();
if (CollectionUtils.isEmpty(produces) || produces.contains(MediaType.ALL_TYPE)) {
producesAllMatchedRoutes.add(match);
} else {
producesSpecificMatchedRoutes.add(match);
}
}
}
if (producesAllMatchedRoutes.isEmpty()) {
return findRouteMatch(producesSpecificMatchedRoutes, error);
}
return findRouteMatch(producesAllMatchedRoutes, error);
}
}
@Override
public <R> Optional<RouteMatch<R>> findErrorRoute(@NonNull Throwable error, HttpRequest<?> request) {
return findErrorRouteInternal(null, error, request);
}
@Override
public <R> Optional<RouteMatch<R>> findStatusRoute(
@NonNull Class<?> originatingClass,
@NonNull HttpStatus status,
HttpRequest<?> request) {
return findStatusInternal(originatingClass, status.getCode(), request);
}
@Override
public <R> Optional<RouteMatch<R>> findStatusRoute(@NonNull HttpStatus status, HttpRequest<?> request) {
return findStatusInternal(null, status.getCode(), request);
}
@Override
public <R> Optional<RouteMatch<R>> findStatusRoute(@NonNull Class<?> originatingClass, int statusCode, HttpRequest<?> request) {
return findStatusInternal(originatingClass, statusCode, request);
}
@Override
public <R> Optional<RouteMatch<R>> findStatusRoute(int statusCode, HttpRequest<?> request) {
return findStatusInternal(null, statusCode, request);
}
private <R> Optional<RouteMatch<R>> findStatusInternal(@Nullable Class<?> originatingClass, int status, HttpRequest<?> request) {
Collection<MediaType> accept = request.accept();
final boolean hasAcceptHeader = CollectionUtils.isNotEmpty(accept);
if (hasAcceptHeader) {
for (StatusRouteInfo<Object, Object> statusRouteInfo : statusRoutes) {
if (!statusRouteInfo.doesProduce(accept)) {
continue;
}
if (!statusRouteInfo.matching(request)) {
continue;
}
@SuppressWarnings("unchecked") final RouteMatch<R> match = (RouteMatch<R>) statusRouteInfo
.match(originatingClass, status).orElse(null);
if (match != null) {
return Optional.of(match);
}
}
} else {
RouteMatch<R> firstMatch = null;
for (StatusRouteInfo<Object, Object> statusRouteInfo : statusRoutes) {
if (!statusRouteInfo.matching(request)) {
continue;
}
@SuppressWarnings("unchecked") final RouteMatch<R> match = (RouteMatch<R>) statusRouteInfo
.match(originatingClass, status).orElse(null);
if (match != null) {
final List<MediaType> produces = match.getRouteInfo().getProduces();
if (CollectionUtils.isEmpty(produces) || produces.contains(MediaType.ALL_TYPE)) {
return Optional.of(match);
} else if (firstMatch == null) {
firstMatch = match;
}
}
}
return Optional.ofNullable(firstMatch);
}
return Optional.empty();
}
@Override
public <R> Optional<RouteMatch<R>> route(@NonNull Throwable error) {
var matchedRoutes = new ArrayList<RouteMatch<R>>();
for (ErrorRouteInfo<Object, Object> errorRouteInfo : errorRoutes) {
if (errorRouteInfo.originatingType() == null) {
Optional match = errorRouteInfo.match(error);
match.ifPresent(m -> matchedRoutes.add((RouteMatch<R>) m));
}
}
return findRouteMatch(matchedRoutes, error);
}
@NonNull
@Override
public List<GenericHttpFilter> findFilters(@NonNull HttpRequest<?> request) {
if (preconditionFilterRoutes.isEmpty()) {
// for perf, this needs to be placed in an ArrayList variable first
@SuppressWarnings("UnnecessaryLocalVariable")
ArrayList<GenericHttpFilter> always = alwaysMatchesHttpFilters.get();
return always;
}
var httpFilters = new ArrayList<GenericHttpFilter>(alwaysMatchesFilterRoutes.size() + preconditionFilterRoutes.size());
httpFilters.addAll(alwaysMatchesHttpFilters.get());
var routeMatch = RouteAttributes.getRouteMatch(request).orElse(null);
HttpMethod method = request.getMethod();
String path = request.getPath();
for (FilterRoute filterRoute : preconditionFilterRoutes) {
if (routeMatch != null) {
if (!matchesFilterMatcher(filterRoute, routeMatch)) {
continue;
}
}
filterRoute.match(method, path).ifPresent(httpFilters::add);
}
FilterRunner.sort(httpFilters);
return Collections.unmodifiableList(httpFilters);
}
@NonNull
@Override
public List<GenericHttpFilter> findFilters(@NonNull HttpRequest<?> request, @Nullable RouteMatch<?> routeMatch) {
if (preconditionFilterRoutes.isEmpty()) {
// for perf, this needs to be placed in an ArrayList variable first
@SuppressWarnings("UnnecessaryLocalVariable")
ArrayList<GenericHttpFilter> always = alwaysMatchesHttpFilters.get();
return always;
}
var httpFilters = new ArrayList<GenericHttpFilter>(alwaysMatchesFilterRoutes.size() + preconditionFilterRoutes.size());
httpFilters.addAll(alwaysMatchesHttpFilters.get());
HttpMethod method = request.getMethod();
String path = request.getPath();
for (FilterRoute filterRoute : preconditionFilterRoutes) {
if (routeMatch != null && !matchesFilterMatcher(filterRoute, routeMatch)) {
continue;
}
filterRoute.match(method, path).ifPresent(httpFilters::add);
}
FilterRunner.sort(httpFilters);
return Collections.unmodifiableList(httpFilters);
}
@NonNull
@Override
public List<GenericHttpFilter> findPreMatchingFilters(@NonNull HttpRequest<?> request) {
if (preMatchingPreconditionFilterRoutes.isEmpty()) {
// for perf, this needs to be placed in an ArrayList variable first
@SuppressWarnings("UnnecessaryLocalVariable")
ArrayList<GenericHttpFilter> always = preMatchingAlwaysMatchesHttpFilters.get();
return always;
}
var httpFilters = new ArrayList<GenericHttpFilter>(preMatchingAlwaysMatchesFilterRoutes.size() + preMatchingPreconditionFilterRoutes.size());
httpFilters.addAll(preMatchingAlwaysMatchesHttpFilters.get());
HttpMethod method = request.getMethod();
String path = request.getPath();
for (FilterRoute filterRoute : preMatchingPreconditionFilterRoutes) {
filterRoute.match(method, path).ifPresent(httpFilters::add);
}
FilterRunner.sort(httpFilters);
return Collections.unmodifiableList(httpFilters);
}
@SuppressWarnings("unchecked")
@NonNull
@Override
public <T, R> Stream<UriRouteMatch<T, R>> findAny(@NonNull CharSequence uri, @Nullable HttpRequest<?> request) {
var matchedRoutes = new ArrayList<UriRouteMatch<T, R>>(5);
final String uriStr = uri.toString();
for (UriRouteInfo<Object, Object>[] routes : allRoutesByMethod.values()) {
for (UriRouteInfo<Object, Object> route : routes) {
if (request != null) {
if (shouldSkipForPort(request, route)) {
continue;
}
if (!route.matching(request)) {
continue;
}
}
UriRouteMatch match = route.tryMatch(uriStr);
if (match != null) {
matchedRoutes.add(match);
}
}
}
return matchedRoutes.stream();
}
@Override
@NonNull
public <T, R> List<UriRouteMatch<T, R>> findAny(@NonNull HttpRequest<?> request) {
String path = request.getPath();
var matchedRoutes = new ArrayList<UriRouteMatch<T, R>>(5);
for (UriRouteInfo<Object, Object>[] routes : allRoutesByMethod.values()) {
for (UriRouteInfo<Object, Object> route : routes) {
if (shouldSkipForPort(request, route)) {
continue;
}
if (!route.matching(request)) {
continue;
}
UriRouteMatch match = route.tryMatch(path);
if (match != null) {
matchedRoutes.add(match);
}
}
}
return matchedRoutes;
}
private List<UriRouteInfo<Object, Object>> findInternal(HttpRequest<?> request) {
HttpMethod httpMethod = request.getMethod();
boolean permitsBody = httpMethod.permitsRequestBody();
Collection<MediaType> acceptedProducedTypes = null;
MediaType contentType = null;
UriRouteInfo<Object, Object>[] routes = httpMethod == HttpMethod.CUSTOM ?
allRoutesByMethod.getOrDefault(request.getMethodName(), EMPTY) : methodRoutesByMethod.getOrDefault(httpMethod, EMPTY);
if (routes.length == 0) {
return Collections.emptyList();
}
var result = new ArrayList<UriRouteInfo<Object, Object>>(routes.length);
for (UriRouteInfo<Object, Object> route : routes) {
if (shouldSkipForPort(request, route)) {
continue;
}
if (permitsBody) {
if (!route.isPermitsRequestBody()) {
continue;
}
if (!route.consumesAll()) {
if (contentType == null) {
contentType = request.getContentType().orElse(null);
}
if (!route.doesConsume(contentType)) {
continue;
}
}
}
if (!route.producesAll()) {
if (acceptedProducedTypes == null) {
acceptedProducedTypes = request.accept();
}
if (!route.doesProduce(acceptedProducedTypes)) {
continue;
}
}
if (!route.matching(request)) {
continue;
}
result.add(route);
}
return result;
}
private boolean shouldSkipForPort(HttpRequest<?> request, UriRouteInfo<Object, Object> route) {
if (ports == null || route.getPort() != null) {
return false;
}
return !ports.contains(request.getServerAddress().getPort());
}
private UriRouteInfo<Object, Object>[] finalizeRoutes(List<UriRouteInfo<Object, Object>> routes) {
Collections.sort(routes);
return routes.toArray(EMPTY);
}
private <T> Optional<RouteMatch<T>> findRouteMatch(List<RouteMatch<T>> matchedRoutes, Throwable error) {
if (matchedRoutes.size() == 1) {
return matchedRoutes.stream().findFirst();
} else if (matchedRoutes.size() > 1) {
int minCount = Integer.MAX_VALUE;
Supplier<List<Class<?>>> hierarchySupplier = () -> ClassUtils.resolveHierarchy(error.getClass());
Optional<RouteMatch<T>> match = Optional.empty();
Class<?> errorClass = error.getClass();
for (RouteMatch<T> errorMatch : matchedRoutes) {
ErrorRouteInfo<T, ?> routeInfo = (ErrorRouteInfo<T, ?>) errorMatch.getRouteInfo();
Class<?> exceptionType = routeInfo.exceptionType();
if (exceptionType.equals(errorClass)) {
match = Optional.of(errorMatch);
break;
} else {
List<Class<?>> hierarchy = hierarchySupplier.get();
//measures the distance in the hierarchy from the error and the route error type
int index = hierarchy.indexOf(exceptionType);
//the class closest in the hierarchy should be chosen
if (index > -1 && index < minCount) {
minCount = index;
match = Optional.of(errorMatch);
}
}
}
return match;
}
return Optional.empty();
}
@Override
public List<FilterEntry> resolveFilterEntries(RouteMatch<?> routeMatch) {
if (preconditionFilterRoutes.isEmpty()) {
return new ArrayList<>(alwaysMatchesFilterRoutes);
}
List<FilterEntry> filterEntries = new ArrayList<>(alwaysMatchesFilterRoutes.size() + preconditionFilterRoutes.size());
filterEntries.addAll(alwaysMatchesFilterRoutes);
for (FilterRoute filterRoute : preconditionFilterRoutes) {
if (!matchesFilterMatcher(filterRoute, routeMatch)) {
filterEntries.add(filterRoute);
}
}
filterEntries.sort(OrderUtil.COMPARATOR);
return Collections.unmodifiableList(filterEntries);
}
@Override
public List<GenericHttpFilter> resolveFilters(HttpRequest<?> request, List<FilterEntry> filterEntries) {
var httpFilters = new ArrayList<GenericHttpFilter>(filterEntries.size());
for (FilterEntry entry : filterEntries) {
if (entry.hasMethods() && !entry.getFilterMethods().contains(request.getMethod())) {
continue;
}
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) || patternStyle.getPathMatcher().matches(pattern, path);
}
if (!matches) {
continue;
}
}
httpFilters.add(entry.getFilter());
}
httpFilters.sort(OrderUtil.COMPARATOR);
return Collections.unmodifiableList(httpFilters);
}
private boolean matchesFilterMatcher(FilterRoute filterRoute, RouteMatch<?> context) {
String matchingAnnotation = filterRoute.findMatchingAnnotation();
if (matchingAnnotation == null) {
return true;
}
return context.getRouteInfo().getAnnotationMetadata().hasStereotype(matchingAnnotation);
}
}