RouteVersionFilter.java
/*
* Copyright 2017-2023 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.version;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.core.version.annotation.Version;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.HttpRequest;
import io.micronaut.web.router.UriRouteMatch;
import io.micronaut.web.router.version.resolution.HeaderVersionResolverConfiguration;
import io.micronaut.web.router.version.resolution.RequestVersionResolver;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD;
/**
* Implementation of {@link io.micronaut.web.router.filter.RouteMatchFilter} responsible for filtering route matches on {@link Version}.
*
* @author Bogdan Oros
* @since 1.1.0
*/
@Singleton
@Requires(beans = RoutesVersioningConfiguration.class)
public class RouteVersionFilter implements VersionRouteMatchFilter {
private static final Logger LOG = LoggerFactory.getLogger(RouteVersionFilter.class);
private final List<RequestVersionResolver> resolvingStrategies;
private final DefaultVersionProvider defaultVersionProvider;
@Nullable
private final RoutesVersioningConfiguration routesVersioningConfiguration;
@Nullable
private final HeaderVersionResolverConfiguration headerVersionResolverConfiguration;
/**
* Creates a {@link RouteVersionFilter} with a collection of {@link RequestVersionResolver}.
*
* @param resolvingStrategies A list of {@link RequestVersionResolver} beans to extract version from HTTP request
* @param defaultVersionProvider The Default Version Provider
* @deprecated Use {@link RouteVersionFilter(List, DefaultVersionProvider, RoutesVersioningConfiguration, HeaderVersionResolverConfiguration)} instead.
*/
@Deprecated
public RouteVersionFilter(List<RequestVersionResolver> resolvingStrategies,
@Nullable DefaultVersionProvider defaultVersionProvider) {
this(resolvingStrategies, defaultVersionProvider, null, null);
}
/**
* Creates a {@link RouteVersionFilter} with a collection of {@link RequestVersionResolver}.
*
* @param resolvingStrategies A list of {@link RequestVersionResolver} beans to extract version from HTTP request
* @param defaultVersionProvider The Default Version Provider
* @param routesVersioningConfiguration Configuration for routes versioning
* @param headerVersionResolverConfiguration Configuration for Header Version resolution
*/
@Inject
public RouteVersionFilter(List<RequestVersionResolver> resolvingStrategies,
@Nullable DefaultVersionProvider defaultVersionProvider,
@Nullable RoutesVersioningConfiguration routesVersioningConfiguration,
@Nullable HeaderVersionResolverConfiguration headerVersionResolverConfiguration) {
this.resolvingStrategies = resolvingStrategies;
this.defaultVersionProvider = defaultVersionProvider;
this.routesVersioningConfiguration = routesVersioningConfiguration;
this.headerVersionResolverConfiguration = headerVersionResolverConfiguration;
}
/**
* Filters route matches by specified version.
*
* @param <T> The target type
* @param <R> The return type
* @param request The HTTP request
* @return A filtered list of route matches
*/
@Override
public <T, R> Predicate<UriRouteMatch<T, R>> filter(HttpRequest<?> request) {
ArgumentUtils.requireNonNull("request", request);
if (resolvingStrategies == null || resolvingStrategies.isEmpty()) {
return match -> true;
}
Optional<String> defaultVersion = defaultVersionProvider == null ? Optional.empty() : Optional.of(defaultVersionProvider.resolveDefaultVersion());
Optional<String> version = resolveVersion(request);
return match -> {
Optional<String> routeVersion = getVersion(match);
if (routeVersion.isPresent()) {
if (shouldFilterReturnTrueForPreFlightRequest(request)) {
return true;
}
return matchIfRouteIsVersioned(request, version.orElse(defaultVersion.orElse(null)), routeVersion.get());
}
return matchIfRouteIsNotVersioned(request, version.orElse(null));
};
}
/**
*
* @param request HTTP Request
* @param version The version resolved evaluating the HTTP Request with beans of type {@link RequestVersionResolver}
* @return {@code true} if no version was resolved from the request
*/
protected boolean matchIfRouteIsNotVersioned(@NonNull HttpRequest<?> request,
@Nullable String version) {
//route is not versioned but request is
if (version != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Route does not specify a version but the version {} was resolved for request to URI {}", version, request.getUri());
}
return false;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Route does not specify a version and no version was resolved for request to URI {}", request.getUri());
}
return true;
}
/**
*
* @param request HTTP Request
* @param resolvedVersion The version resolved evaluating the HTTP Request with beans of type {@link RequestVersionResolver} and the {@link RoutesVersioningConfiguration#getDefaultVersion()}.
* @param routeVersion The route's version. For example, it could specify by the {@link Version} annotation.
* @return {@code true} if the resolved version matches the route version or if the resolved version is {@code null}
*/
protected boolean matchIfRouteIsVersioned(@NonNull HttpRequest<?> request,
@Nullable String resolvedVersion,
@NonNull String routeVersion) {
//no version found and no default version configured
if (resolvedVersion == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Route specifies a version {} and no version information resolved for request to URI {}", routeVersion, request.getUri());
}
return true;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Route specifies a version {} and the version {} was resolved for request to URI {}", routeVersion, resolvedVersion, request.getUri());
}
return resolvedVersion.equals(routeVersion);
}
/**
*
* @param request HTTP Request
* @return the resolved requested version wrapped in an {@link Optional}
*/
@NonNull
protected Optional<String> resolveVersion(@NonNull HttpRequest<?> request) {
return resolvingStrategies.stream()
.map(strategy -> strategy.resolve(request).orElse(null))
.filter(Objects::nonNull)
.findFirst();
}
/**
*
* @param routeMatch the route match
* @param <T> The target type
* @param <R> The return type
* @return Returns the value of the annotation {@link Version} in the route method wrapped in an {@link Optional}.
*/
protected <T, R> Optional<String> getVersion(UriRouteMatch<T, R> routeMatch) {
return routeMatch.getExecutableMethod().stringValue(Version.class);
}
private boolean shouldFilterReturnTrueForPreFlightRequest(HttpRequest<?> request) {
if (!isPreflightRequest(request)) {
return false;
}
if (routesVersioningConfiguration != null && routesVersioningConfiguration.isEnabled() && headerVersionResolverConfiguration != null) {
return headerVersionResolverConfiguration.getNames().stream().anyMatch(expectedHeaderName -> request.getHeaders().getAll(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS).contains(expectedHeaderName));
}
return true;
}
private static boolean isPreflightRequest(HttpRequest<?> request) {
HttpHeaders headers = request.getHeaders();
Optional<String> origin = headers.getOrigin();
return origin.isPresent() && headers.contains(ACCESS_CONTROL_REQUEST_METHOD) && HttpMethod.OPTIONS == request.getMethod();
}
}