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.6.5
Show newest version
/*
 * Copyright 2017-2019 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
 *
 * http://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.order.OrderUtil;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.filter.HttpFilter;
import io.micronaut.http.uri.UriMatchTemplate;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.net.URI;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
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 { private final UriRoute[][] routesByMethod = new UriRoute[HttpMethod.values().length][]; private final Set statusRoutes = new HashSet<>(); private final Collection filterRoutes = new ArrayList<>(); private final Set errorRoutes = new HashSet<>(); /** * Construct a new router for the given route builders. * * @param builders The builders */ @Inject public DefaultRouter(Collection builders) { List getRoutes = new ArrayList<>(); List putRoutes = new ArrayList<>(); List postRoutes = new ArrayList<>(); List patchRoutes = new ArrayList<>(); List deleteRoutes = new ArrayList<>(); List optionsRoutes = new ArrayList<>(); List headRoutes = new ArrayList<>(); List connectRoutes = new ArrayList<>(); List traceRoutes = new ArrayList<>(); for (RouteBuilder builder : builders) { List constructedRoutes = builder.getUriRoutes(); for (UriRoute route : constructedRoutes) { switch (route.getHttpMethod()) { case GET: getRoutes.add(route); break; case PUT: putRoutes.add(route); break; case POST: postRoutes.add(route); break; case PATCH: patchRoutes.add(route); break; case DELETE: deleteRoutes.add(route); break; case OPTIONS: optionsRoutes.add(route); break; case HEAD: headRoutes.add(route); break; case CONNECT: connectRoutes.add(route); break; case TRACE: traceRoutes.add(route); break; default: // no-op } } this.statusRoutes.addAll(builder.getStatusRoutes()); this.errorRoutes.addAll(builder.getErrorRoutes()); this.filterRoutes.addAll(builder.getFilterRoutes()); } for (HttpMethod method : HttpMethod.values()) { switch (method) { case GET: routesByMethod[method.ordinal()] = finalizeRoutes(getRoutes); break; case PUT: routesByMethod[method.ordinal()] = finalizeRoutes(putRoutes); break; case POST: routesByMethod[method.ordinal()] = finalizeRoutes(postRoutes); break; case PATCH: routesByMethod[method.ordinal()] = finalizeRoutes(patchRoutes); break; case DELETE: routesByMethod[method.ordinal()] = finalizeRoutes(deleteRoutes); break; case OPTIONS: routesByMethod[method.ordinal()] = finalizeRoutes(optionsRoutes); break; case HEAD: routesByMethod[method.ordinal()] = finalizeRoutes(headRoutes); break; case CONNECT: routesByMethod[method.ordinal()] = finalizeRoutes(connectRoutes); break; case TRACE: routesByMethod[method.ordinal()] = finalizeRoutes(traceRoutes); break; default: // no-op } } } /** * Construct a new router for the given route builders. * * @param builders The builders */ public DefaultRouter(RouteBuilder... builders) { this(Arrays.asList(builders)); } @SuppressWarnings("unchecked") @Override public Stream> find(HttpMethod httpMethod, CharSequence uri) { UriRoute[] routes = routesByMethod[httpMethod.ordinal()]; String uriString = uri.toString(); return Arrays .stream(routes) .map((route -> (UriRouteMatch) route.match(uriString).orElse(null))) .filter(Objects::nonNull); } @Override public List> findAllClosest(HttpRequest request) { List> uriRoutes = this.find(request).collect(Collectors.toList()); boolean hasMultipleMatches = uriRoutes.size() > 1; if (hasMultipleMatches && HttpMethod.permitsRequestBody(request.getMethod())) { List> explicitAcceptRoutes = new ArrayList<>(uriRoutes.size()); List> acceptRoutes = new ArrayList<>(uriRoutes.size()); Optional contentType = request.getContentType(); for (UriRouteMatch match: uriRoutes) { if (match.explicitAccept(contentType.orElse(MediaType.ALL_TYPE))) { explicitAcceptRoutes.add(match); } if (explicitAcceptRoutes.isEmpty() && match.accept(contentType.orElse(null))) { acceptRoutes.add(match); } } uriRoutes = explicitAcceptRoutes.isEmpty() ? acceptRoutes : explicitAcceptRoutes; hasMultipleMatches = uriRoutes.size() > 1; } /** * Any changes to the logic below may also need changes to {@link io.micronaut.http.uri.UriTemplate#compareTo(UriTemplate)} */ if (hasMultipleMatches) { long variableCount = 0; long rawCount = 0; List> closestMatches = new ArrayList<>(uriRoutes.size()); for (int i = 0; i < uriRoutes.size(); i++) { UriRouteMatch match = uriRoutes.get(i); UriMatchTemplate template = match.getRoute().getUriMatchTemplate(); long variable = template.getPathVariableSegmentCount(); long raw = template.getRawSegmentCount(); if (i == 0) { variableCount = variable; rawCount = raw; } if (variable > variableCount || raw < rawCount) { break; } closestMatches.add(match); } uriRoutes = closestMatches; } return uriRoutes; } @Override public Stream> find(HttpRequest request) { HttpMethod httpMethod = request.getMethod(); boolean permitsBody = HttpMethod.permitsRequestBody(httpMethod); return this.find(httpMethod, request.getPath()) .filter((match) -> match.test(request) && (!permitsBody || match.accept(request.getContentType().orElse(null)))); } @Override public Stream uriRoutes() { return Arrays .stream(routesByMethod) .flatMap(Arrays::stream); } @Override public Optional> route(HttpMethod httpMethod, CharSequence uri) { UriRoute[] routes = routesByMethod[httpMethod.ordinal()]; Optional result = Arrays .stream(routes) .map((route -> route.match(uri.toString()))) .filter(Optional::isPresent) .map(Optional::get) .findFirst(); UriRouteMatch match = result.orElse(null); return Optional.ofNullable(match); } @Override public Optional> route(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(Class originatingClass, HttpStatus status) { for (StatusRoute statusRoute : statusRoutes) { Optional> match = statusRoute.match(originatingClass, status); if (match.isPresent()) { return match; } } return Optional.empty(); } @Override public Optional> route(Class originatingClass, 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> route(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); } @Override public List findFilters(HttpRequest request) { List httpFilters = new ArrayList<>(); HttpMethod method = request.getMethod(); URI uri = request.getUri(); for (FilterRoute filterRoute : filterRoutes) { Optional match = filterRoute.match(method, uri); match.ifPresent(httpFilters::add); } if (!httpFilters.isEmpty()) { OrderUtil.sort(httpFilters); return Collections.unmodifiableList(httpFilters); } else { return Collections.emptyList(); } } @SuppressWarnings("unchecked") @Override public Stream> findAny(CharSequence uri) { return Arrays .stream(routesByMethod) .filter(Objects::nonNull) .flatMap(Arrays::stream) .map(route -> route.match(uri.toString())) .filter(Optional::isPresent) .map(Optional::get); } 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(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy