io.micronaut.web.router.DefaultRouter Maven / Gradle / Ivy
/*
* 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();
}
}