All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.micronaut.web.router.DefaultRouter Maven / Gradle / Ivy

There is a newer version: 4.7.9
Show newest version
/*
 * 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.AnnotationMetadata;
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.HttpAttributes;
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.HttpFilter;
import io.micronaut.http.filter.HttpServerFilterResolver;
import io.micronaut.http.uri.UriMatchTemplate;
import io.micronaut.web.router.exceptions.RoutingException;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;

/**
 * 

The default {@link Router} implementation. This implementation does not perform any additional caching of * route discovery.

* * @author Graeme Rocher * @since 1.0 */ @Singleton public class DefaultRouter implements Router, HttpServerFilterResolver> { private final Map> routesByMethod = new HashMap<>(); private final List statusRoutes = new ArrayList<>(); private final List errorRoutes = new ArrayList<>(); private final Set exposedPorts; private final List alwaysMatchesFilterRoutes = new ArrayList<>(); private final List preconditionFilterRoutes = new ArrayList<>(); private final Supplier> alwaysMatchesHttpFilters = SupplierUtil.memoized(() -> { if (alwaysMatchesFilterRoutes.isEmpty()) { return Collections.emptyList(); } List httpFilters = new ArrayList<>(alwaysMatchesFilterRoutes.size()); for (FilterRoute filterRoute : alwaysMatchesFilterRoutes) { httpFilters.add(filterRoute.getFilter()); } httpFilters.sort(OrderUtil.COMPARATOR); return httpFilters; }); /** * 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 builders) { Set exposedPorts = new HashSet<>(5); List filterRoutes = new ArrayList<>(); for (RouteBuilder builder : builders) { List constructedRoutes = builder.getUriRoutes(); for (UriRoute route : constructedRoutes) { String key = route.getHttpMethodName(); routesByMethod.computeIfAbsent(key, x -> new ArrayList<>()).add(route); } for (StatusRoute statusRoute : builder.getStatusRoutes()) { if (statusRoutes.contains(statusRoute)) { final StatusRoute existing = statusRoutes.stream().filter(r -> r.equals(statusRoute)).findFirst().orElse(null); throw new RoutingException("Attempted to register multiple local routes for http status [" + statusRoute.status() + "]. New route: " + statusRoute + ". Existing: " + existing); } this.statusRoutes.add(statusRoute); } for (ErrorRoute errorRoute : builder.getErrorRoutes()) { if (errorRoutes.contains(errorRoute)) { final ErrorRoute existing = errorRoutes.stream().filter(r -> r.equals(errorRoute)).findFirst().orElse(null); throw new RoutingException("Attempted to register multiple local routes for error [" + errorRoute.exceptionType().getSimpleName() + "]. New route: " + errorRoute + ". Existing: " + existing); } this.errorRoutes.add(errorRoute); } filterRoutes.addAll(builder.getFilterRoutes()); exposedPorts.addAll(builder.getExposedPorts()); } if (CollectionUtils.isNotEmpty(exposedPorts)) { this.exposedPorts = exposedPorts; } else { this.exposedPorts = Collections.emptySet(); } routesByMethod.values().forEach(this::finalizeRoutes); for (FilterRoute filterRoute : filterRoutes) { if (isMatchesAll(filterRoute)) { alwaysMatchesFilterRoutes.add(filterRoute); } else { preconditionFilterRoutes.add(filterRoute); } } } 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 getExposedPorts() { return exposedPorts; } @Override public void applyDefaultPorts(List ports) { Predicate> portMatches = (httpRequest -> ports.contains(httpRequest.getServerAddress().getPort())); for (List routes : routesByMethod.values()) { for (int i = 0; i < routes.size(); i++) { UriRoute route = routes.get(i); if (route.getPort() == null) { routes.set(i, route.where(portMatches)); } } } } @NonNull @Override public Stream> find(@NonNull HttpRequest request, @NonNull CharSequence uri) { return this.find(request.getMethodName(), uri, null).stream(); } @NonNull @Override public Stream> find(@NonNull HttpRequest request) { boolean permitsBody = HttpMethod.permitsRequestBody(request.getMethod()); return this.find(request, request.getPath()) .filter(match -> match.test(request) && (!permitsBody || match.doesConsume(request.getContentType().orElse(null)))); } @NonNull @Override public Stream> find(@NonNull HttpMethod httpMethod, @NonNull CharSequence uri, @Nullable HttpRequest context) { return this.find(httpMethod.name(), uri, null).stream(); } @NonNull @Override public Stream uriRoutes() { return routesByMethod.values().stream().flatMap(List::stream); } @NonNull @Override public List> findAllClosest(@NonNull HttpRequest request) { final HttpMethod httpMethod = request.getMethod(); final MediaType contentType = request.getContentType().orElse(null); boolean permitsBody = HttpMethod.permitsRequestBody(httpMethod); final Collection acceptedProducedTypes = request.accept(); List> uriRoutes = this.find( request.getMethodName(), request.getPath(), routeMatch -> routeMatch.test(request) && (!permitsBody || routeMatch.doesConsume(contentType)) && routeMatch.doesProduce(acceptedProducedTypes) ); int routeCount = uriRoutes.size(); if (routeCount <= 1) { return uriRoutes; } // if there are multiple routes, try to resolve the ambiguity if (CollectionUtils.isNotEmpty(acceptedProducedTypes)) { // take the highest priority accepted type final MediaType mediaType = acceptedProducedTypes.iterator().next(); List> mostSpecific = new ArrayList<>(uriRoutes.size()); for (UriRouteMatch routeMatch : uriRoutes) { if (routeMatch.explicitlyProduces(mediaType)) { mostSpecific.add(routeMatch); } } if (!mostSpecific.isEmpty()) { uriRoutes = mostSpecific; } } routeCount = uriRoutes.size(); if (routeCount > 1 && permitsBody) { List> explicitlyConsumedRoutes = new ArrayList<>(routeCount); List> consumesRoutes = new ArrayList<>(routeCount); for (UriRouteMatch match: uriRoutes) { if (match.explicitlyConsumes(contentType != null ? contentType : MediaType.ALL_TYPE)) { explicitlyConsumedRoutes.add(match); } if (explicitlyConsumedRoutes.isEmpty() && match.doesConsume(contentType)) { 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; List> closestMatches = new ArrayList<>(routeCount); for (int i = 0; i < routeCount; i++) { UriRouteMatch match = uriRoutes.get(i); UriMatchTemplate template = match.getRoute().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; } @NonNull @Override public Optional> route(@NonNull HttpMethod httpMethod, @NonNull CharSequence uri) { List routes = routesByMethod.getOrDefault(httpMethod.name(), Collections.emptyList()); for (UriRoute uriRoute : routes) { Optional match = uriRoute.match(uri.toString()); if (match.isPresent()) { return (Optional) match; } } return Optional.empty(); } @Override public Optional> route(@NonNull HttpStatus status) { for (StatusRoute statusRoute : statusRoutes) { if (statusRoute.originatingType() == null) { Optional> match = statusRoute.match(status); if (match.isPresent()) { return match; } } } return Optional.empty(); } @Override public Optional> route(@NonNull Class originatingClass, @NonNull HttpStatus status) { for (StatusRoute statusRoute : statusRoutes) { Optional> match = statusRoute.match(originatingClass, status); if (match.isPresent()) { return match; } } return Optional.empty(); } @Override public Optional> route(@NonNull Class originatingClass, @NonNull Throwable error) { Map> matchedRoutes = new LinkedHashMap<>(); for (ErrorRoute errorRoute : errorRoutes) { Optional> match = errorRoute.match(originatingClass, error); match.ifPresent(m -> matchedRoutes.put(errorRoute, m) ); } return findRouteMatch(matchedRoutes, error); } @Override public Optional> findErrorRoute( @NonNull Class originatingClass, @NonNull Throwable error, HttpRequest request) { return findErrorRouteInternal(originatingClass, error, request); } private Optional> findErrorRouteInternal( @Nullable Class originatingClass, @NonNull Throwable error, HttpRequest request) { Collection accept = request.accept(); final boolean hasAcceptHeader = CollectionUtils.isNotEmpty(accept); if (hasAcceptHeader) { Map> matchedRoutes = new LinkedHashMap<>(); for (ErrorRoute errorRoute : errorRoutes) { @SuppressWarnings("unchecked") final RouteMatch match = (RouteMatch) errorRoute .match(originatingClass, error).orElse(null); if (match != null && match.doesProduce(accept)) { matchedRoutes.put(errorRoute, match); } } return findRouteMatch(matchedRoutes, error); } else { Map> producesAllMatchedRoutes = new LinkedHashMap<>(); Map> producesSpecificMatchedRoutes = new LinkedHashMap<>(); for (ErrorRoute errorRoute : errorRoutes) { @SuppressWarnings("unchecked") final RouteMatch match = (RouteMatch) errorRoute .match(originatingClass, error).orElse(null); if (match != null) { final List produces = match.getProduces(); if (CollectionUtils.isEmpty(produces) || produces.contains(MediaType.ALL_TYPE)) { producesAllMatchedRoutes.put(errorRoute, match); } else { producesSpecificMatchedRoutes.put(errorRoute, match); } } } if (producesAllMatchedRoutes.isEmpty()) { return findRouteMatch(producesSpecificMatchedRoutes, error); } return findRouteMatch(producesAllMatchedRoutes, error); } } @Override public Optional> findErrorRoute(@NonNull Throwable error, HttpRequest request) { return findErrorRouteInternal(null, error, request); } @Override public Optional> findStatusRoute( @NonNull Class originatingClass, @NonNull HttpStatus status, HttpRequest request) { return findStatusInternal(originatingClass, status, request); } @Override public Optional> findStatusRoute(@NonNull HttpStatus status, HttpRequest request) { return findStatusInternal(null, status, request); } private Optional> findStatusInternal(@Nullable Class originatingClass, @NonNull HttpStatus status, HttpRequest request) { Collection accept = request.accept(); final boolean hasAcceptHeader = CollectionUtils.isNotEmpty(accept); if (hasAcceptHeader) { for (StatusRoute statusRoute : statusRoutes) { @SuppressWarnings("unchecked") final RouteMatch match = (RouteMatch) statusRoute .match(originatingClass, status).orElse(null); if (match != null && match.doesProduce(accept)) { return Optional.of(match); } } } else { RouteMatch firstMatch = null; for (StatusRoute errorRoute : statusRoutes) { @SuppressWarnings("unchecked") final RouteMatch match = (RouteMatch) errorRoute .match(originatingClass, status).orElse(null); if (match != null) { final List produces = match.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 Optional> route(@NonNull Throwable error) { Map> matchedRoutes = new LinkedHashMap<>(); for (ErrorRoute errorRoute : errorRoutes) { if (errorRoute.originatingType() == null) { Optional> match = errorRoute.match(error); match.ifPresent(m -> matchedRoutes.put(errorRoute, m)); } } return findRouteMatch(matchedRoutes, error); } @NonNull @Override public List findFilters(@NonNull HttpRequest request) { if (preconditionFilterRoutes.isEmpty()) { return alwaysMatchesHttpFilters.get(); } List httpFilters = new ArrayList<>(alwaysMatchesFilterRoutes.size() + preconditionFilterRoutes.size()); httpFilters.addAll(alwaysMatchesHttpFilters.get()); RouteMatch routeMatch = (RouteMatch) request.getAttribute(HttpAttributes.ROUTE_MATCH).filter(o -> o instanceof RouteMatch).orElse(null); HttpMethod method = request.getMethod(); URI uri = request.getUri(); for (FilterRoute filterRoute : preconditionFilterRoutes) { if (routeMatch != null) { if (!matchesFilterMatcher(filterRoute, routeMatch)) { continue; } } filterRoute.match(method, uri).ifPresent(httpFilters::add); } httpFilters.sort(OrderUtil.COMPARATOR); return Collections.unmodifiableList(httpFilters); } @SuppressWarnings("unchecked") @NonNull @Override public Stream> findAny(@NonNull CharSequence uri, @Nullable HttpRequest context) { List matchedRoutes = new ArrayList<>(5); final String uriStr = uri.toString(); for (List routes : routesByMethod.values()) { for (UriRoute route : routes) { final UriRouteMatch match = route.match(uriStr).orElse(null); if (match != null && match.test(context)) { matchedRoutes.add(match); } } } return matchedRoutes.stream(); } private List> find(String httpMethodName, CharSequence uri, @Nullable Predicate predicate) { List routes = routesByMethod.getOrDefault(httpMethodName, Collections.emptyList()); if (CollectionUtils.isNotEmpty(routes)) { final String uriStr = uri.toString(); List> routeMatches = new LinkedList<>(); for (UriRoute route : routes) { Optional match = route.match(uriStr); if (predicate != null) { match = match.filter(predicate); } match.ifPresent(routeMatches::add); } return routeMatches; } else { //noinspection unchecked return Collections.emptyList(); } } private UriRoute[] finalizeRoutes(List routes) { Collections.sort(routes); return routes.toArray(new UriRoute[0]); } private Optional> findRouteMatch(Map> matchedRoutes, Throwable error) { if (matchedRoutes.size() == 1) { return matchedRoutes.values().stream().findFirst(); } else if (matchedRoutes.size() > 1) { int minCount = Integer.MAX_VALUE; Supplier> hierarchySupplier = () -> ClassUtils.resolveHierarchy(error.getClass()); Optional> match = Optional.empty(); Class errorClass = error.getClass(); for (Map.Entry> entry: matchedRoutes.entrySet()) { Class exceptionType = entry.getKey().exceptionType(); if (exceptionType.equals(errorClass)) { match = Optional.of(entry.getValue()); break; } else { List 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(entry.getValue()); } } } return match; } return Optional.empty(); } @Override public List> resolveFilterEntries(RouteMatch routeMatch) { if (preconditionFilterRoutes.isEmpty()) { return (List) alwaysMatchesFilterRoutes; } List> 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 resolveFilters(HttpRequest request, List> filterEntries) { List httpFilters = new ArrayList<>(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) { AnnotationMetadata annotationMetadata = filterRoute.getAnnotationMetadata(); boolean matches = !annotationMetadata.hasStereotype(FilterMatcher.NAME); if (!matches) { String filterAnnotation = annotationMetadata.getAnnotationNameByStereotype(FilterMatcher.NAME).orElse(null); if (filterAnnotation != null) { matches = context.getAnnotationMetadata().hasStereotype(filterAnnotation); } } return matches; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy