Support for functional routing by API version
See gh-35113
This commit is contained in:
parent
224f1af08e
commit
5a6c019413
|
@ -50,6 +50,7 @@ import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
|
import org.springframework.web.reactive.accept.ApiVersionStrategy;
|
||||||
import org.springframework.web.reactive.function.BodyExtractor;
|
import org.springframework.web.reactive.function.BodyExtractor;
|
||||||
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
|
@ -94,6 +95,8 @@ public final class MockServerRequest implements ServerRequest {
|
||||||
|
|
||||||
private final List<HttpMessageReader<?>> messageReaders;
|
private final List<HttpMessageReader<?>> messageReaders;
|
||||||
|
|
||||||
|
private final @Nullable ApiVersionStrategy versionStrategy;
|
||||||
|
|
||||||
private final @Nullable ServerWebExchange exchange;
|
private final @Nullable ServerWebExchange exchange;
|
||||||
|
|
||||||
|
|
||||||
|
@ -102,7 +105,8 @@ public final class MockServerRequest implements ServerRequest {
|
||||||
Map<String, Object> attributes, MultiValueMap<String, String> queryParams,
|
Map<String, Object> attributes, MultiValueMap<String, String> queryParams,
|
||||||
Map<String, String> pathVariables, @Nullable WebSession session, @Nullable Principal principal,
|
Map<String, String> pathVariables, @Nullable WebSession session, @Nullable Principal principal,
|
||||||
@Nullable InetSocketAddress remoteAddress, @Nullable InetSocketAddress localAddress,
|
@Nullable InetSocketAddress remoteAddress, @Nullable InetSocketAddress localAddress,
|
||||||
List<HttpMessageReader<?>> messageReaders, @Nullable ServerWebExchange exchange) {
|
List<HttpMessageReader<?>> messageReaders, @Nullable ApiVersionStrategy versionStrategy,
|
||||||
|
@Nullable ServerWebExchange exchange) {
|
||||||
|
|
||||||
this.method = method;
|
this.method = method;
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
|
@ -118,6 +122,7 @@ public final class MockServerRequest implements ServerRequest {
|
||||||
this.remoteAddress = remoteAddress;
|
this.remoteAddress = remoteAddress;
|
||||||
this.localAddress = localAddress;
|
this.localAddress = localAddress;
|
||||||
this.messageReaders = messageReaders;
|
this.messageReaders = messageReaders;
|
||||||
|
this.versionStrategy = versionStrategy;
|
||||||
this.exchange = exchange;
|
this.exchange = exchange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,6 +172,11 @@ public final class MockServerRequest implements ServerRequest {
|
||||||
return this.messageReaders;
|
return this.messageReaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable ApiVersionStrategy apiVersionStrategy() {
|
||||||
|
return this.versionStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public <S> S body(BodyExtractor<S, ? super ServerHttpRequest> extractor) {
|
public <S> S body(BodyExtractor<S, ? super ServerHttpRequest> extractor) {
|
||||||
|
@ -313,6 +323,8 @@ public final class MockServerRequest implements ServerRequest {
|
||||||
|
|
||||||
Builder messageReaders(List<HttpMessageReader<?>> messageReaders);
|
Builder messageReaders(List<HttpMessageReader<?>> messageReaders);
|
||||||
|
|
||||||
|
Builder apiVersionStrategy(@Nullable ApiVersionStrategy versionStrategy);
|
||||||
|
|
||||||
Builder exchange(ServerWebExchange exchange);
|
Builder exchange(ServerWebExchange exchange);
|
||||||
|
|
||||||
MockServerRequest body(Object body);
|
MockServerRequest body(Object body);
|
||||||
|
@ -351,6 +363,8 @@ public final class MockServerRequest implements ServerRequest {
|
||||||
|
|
||||||
private List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
|
private List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
|
||||||
|
|
||||||
|
private @Nullable ApiVersionStrategy versionStrategy;
|
||||||
|
|
||||||
private @Nullable ServerWebExchange exchange;
|
private @Nullable ServerWebExchange exchange;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -483,6 +497,12 @@ public final class MockServerRequest implements ServerRequest {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Builder apiVersionStrategy(@Nullable ApiVersionStrategy versionStrategy) {
|
||||||
|
this.versionStrategy = versionStrategy;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Builder exchange(ServerWebExchange exchange) {
|
public Builder exchange(ServerWebExchange exchange) {
|
||||||
Assert.notNull(exchange, "'exchange' must not be null");
|
Assert.notNull(exchange, "'exchange' must not be null");
|
||||||
|
@ -496,7 +516,7 @@ public final class MockServerRequest implements ServerRequest {
|
||||||
return new MockServerRequest(this.method, this.uri, this.contextPath, this.headers,
|
return new MockServerRequest(this.method, this.uri, this.contextPath, this.headers,
|
||||||
this.cookies, this.body, this.attributes, this.queryParams, this.pathVariables,
|
this.cookies, this.body, this.attributes, this.queryParams, this.pathVariables,
|
||||||
this.session, this.principal, this.remoteAddress, this.localAddress,
|
this.session, this.principal, this.remoteAddress, this.localAddress,
|
||||||
this.messageReaders, this.exchange);
|
this.messageReaders, this.versionStrategy, this.exchange);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -504,7 +524,7 @@ public final class MockServerRequest implements ServerRequest {
|
||||||
return new MockServerRequest(this.method, this.uri, this.contextPath, this.headers,
|
return new MockServerRequest(this.method, this.uri, this.contextPath, this.headers,
|
||||||
this.cookies, null, this.attributes, this.queryParams, this.pathVariables,
|
this.cookies, null, this.attributes, this.queryParams, this.pathVariables,
|
||||||
this.session, this.principal, this.remoteAddress, this.localAddress,
|
this.session, this.principal, this.remoteAddress, this.localAddress,
|
||||||
this.messageReaders, this.exchange);
|
this.messageReaders, this.versionStrategy, this.exchange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,16 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
|
||||||
return this.defaultVersion;
|
return this.defaultVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the strategy is configured to detect supported versions.
|
||||||
|
* If this is set to {@code false} then {@link #addMappedVersion} is ignored
|
||||||
|
* and the list of supported versions can be built explicitly through calls
|
||||||
|
* to {@link #addSupportedVersion}.
|
||||||
|
*/
|
||||||
|
public boolean detectSupportedVersions() {
|
||||||
|
return this.detectSupportedVersions;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add to the list of supported versions to check against in
|
* Add to the list of supported versions to check against in
|
||||||
* {@link ApiVersionStrategy#validateVersion} before raising
|
* {@link ApiVersionStrategy#validateVersion} before raising
|
||||||
|
|
|
@ -246,10 +246,13 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public RouterFunctionMapping routerFunctionMapping(ServerCodecConfigurer serverCodecConfigurer) {
|
public RouterFunctionMapping routerFunctionMapping(
|
||||||
|
ServerCodecConfigurer serverCodecConfigurer, @Nullable ApiVersionStrategy apiVersionStrategy) {
|
||||||
|
|
||||||
RouterFunctionMapping mapping = createRouterFunctionMapping();
|
RouterFunctionMapping mapping = createRouterFunctionMapping();
|
||||||
mapping.setOrder(-1); // go before RequestMappingHandlerMapping
|
mapping.setOrder(-1); // go before RequestMappingHandlerMapping
|
||||||
mapping.setMessageReaders(serverCodecConfigurer.getReaders());
|
mapping.setMessageReaders(serverCodecConfigurer.getReaders());
|
||||||
|
mapping.setApiVersionStrategy(apiVersionStrategy);
|
||||||
configureAbstractHandlerMapping(mapping, getPathMatchConfigurer());
|
configureAbstractHandlerMapping(mapping, getPathMatchConfigurer());
|
||||||
return mapping;
|
return mapping;
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,7 @@ import org.springframework.validation.BindException;
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.bind.support.WebExchangeDataBinder;
|
import org.springframework.web.bind.support.WebExchangeDataBinder;
|
||||||
|
import org.springframework.web.reactive.accept.ApiVersionStrategy;
|
||||||
import org.springframework.web.reactive.function.BodyExtractor;
|
import org.springframework.web.reactive.function.BodyExtractor;
|
||||||
import org.springframework.web.reactive.function.BodyExtractors;
|
import org.springframework.web.reactive.function.BodyExtractors;
|
||||||
import org.springframework.web.reactive.function.UnsupportedMediaTypeException;
|
import org.springframework.web.reactive.function.UnsupportedMediaTypeException;
|
||||||
|
@ -92,10 +93,20 @@ class DefaultServerRequest implements ServerRequest {
|
||||||
|
|
||||||
private final List<HttpMessageReader<?>> messageReaders;
|
private final List<HttpMessageReader<?>> messageReaders;
|
||||||
|
|
||||||
|
private final @Nullable ApiVersionStrategy versionStrategy;
|
||||||
|
|
||||||
|
|
||||||
DefaultServerRequest(ServerWebExchange exchange, List<HttpMessageReader<?>> messageReaders) {
|
DefaultServerRequest(ServerWebExchange exchange, List<HttpMessageReader<?>> messageReaders) {
|
||||||
|
this(exchange, messageReaders, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultServerRequest(
|
||||||
|
ServerWebExchange exchange, List<HttpMessageReader<?>> messageReaders,
|
||||||
|
@Nullable ApiVersionStrategy versionStrategy) {
|
||||||
|
|
||||||
this.exchange = exchange;
|
this.exchange = exchange;
|
||||||
this.messageReaders = List.copyOf(messageReaders);
|
this.messageReaders = List.copyOf(messageReaders);
|
||||||
|
this.versionStrategy = versionStrategy;
|
||||||
this.headers = new DefaultHeaders();
|
this.headers = new DefaultHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,6 +173,11 @@ class DefaultServerRequest implements ServerRequest {
|
||||||
return this.messageReaders;
|
return this.messageReaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable ApiVersionStrategy apiVersionStrategy() {
|
||||||
|
return this.versionStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
|
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
|
||||||
return bodyInternal(extractor, Hints.from(Hints.LOG_PREFIX_HINT, exchange().getLogPrefix()));
|
return bodyInternal(extractor, Hints.from(Hints.LOG_PREFIX_HINT, exchange().getLogPrefix()));
|
||||||
|
|
|
@ -54,6 +54,7 @@ import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.reactive.accept.ApiVersionStrategy;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.springframework.web.server.WebSession;
|
import org.springframework.web.server.WebSession;
|
||||||
import org.springframework.web.util.UriUtils;
|
import org.springframework.web.util.UriUtils;
|
||||||
|
@ -69,6 +70,8 @@ class DefaultServerRequestBuilder implements ServerRequest.Builder {
|
||||||
|
|
||||||
private final List<HttpMessageReader<?>> messageReaders;
|
private final List<HttpMessageReader<?>> messageReaders;
|
||||||
|
|
||||||
|
private final @Nullable ApiVersionStrategy versionStrategy;
|
||||||
|
|
||||||
private final ServerWebExchange exchange;
|
private final ServerWebExchange exchange;
|
||||||
|
|
||||||
private HttpMethod method;
|
private HttpMethod method;
|
||||||
|
@ -89,6 +92,7 @@ class DefaultServerRequestBuilder implements ServerRequest.Builder {
|
||||||
DefaultServerRequestBuilder(ServerRequest other) {
|
DefaultServerRequestBuilder(ServerRequest other) {
|
||||||
Assert.notNull(other, "ServerRequest must not be null");
|
Assert.notNull(other, "ServerRequest must not be null");
|
||||||
this.messageReaders = other.messageReaders();
|
this.messageReaders = other.messageReaders();
|
||||||
|
this.versionStrategy = other.apiVersionStrategy();
|
||||||
this.exchange = other.exchange();
|
this.exchange = other.exchange();
|
||||||
this.method = other.method();
|
this.method = other.method();
|
||||||
this.uri = other.uri();
|
this.uri = other.uri();
|
||||||
|
@ -195,7 +199,7 @@ class DefaultServerRequestBuilder implements ServerRequest.Builder {
|
||||||
this.method, this.uri, this.contextPath, this.headers, this.cookies, this.body, this.attributes);
|
this.method, this.uri, this.contextPath, this.headers, this.cookies, this.body, this.attributes);
|
||||||
ServerWebExchange exchange = new DelegatingServerWebExchange(
|
ServerWebExchange exchange = new DelegatingServerWebExchange(
|
||||||
serverHttpRequest, this.attributes, this.exchange, this.messageReaders);
|
serverHttpRequest, this.attributes, this.exchange, this.messageReaders);
|
||||||
return new DefaultServerRequest(exchange, this.messageReaders);
|
return new DefaultServerRequest(exchange, this.messageReaders, this.versionStrategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,8 @@ import org.springframework.util.MimeTypeUtils;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.cors.reactive.CorsUtils;
|
import org.springframework.web.cors.reactive.CorsUtils;
|
||||||
|
import org.springframework.web.reactive.HandlerMapping;
|
||||||
|
import org.springframework.web.reactive.accept.ApiVersionStrategy;
|
||||||
import org.springframework.web.reactive.function.BodyExtractor;
|
import org.springframework.web.reactive.function.BodyExtractor;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.springframework.web.server.WebSession;
|
import org.springframework.web.server.WebSession;
|
||||||
|
@ -182,6 +184,25 @@ public abstract class RequestPredicates {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code RequestPredicate} to match to the request API version extracted
|
||||||
|
* from and parsed with the configured {@link ApiVersionStrategy}.
|
||||||
|
* <p>The version may be one of the following:
|
||||||
|
* <ul>
|
||||||
|
* <li>Fixed version ("1.2") -- match this version only.
|
||||||
|
* <li>Baseline version ("1.2+") -- match this and subsequent versions.
|
||||||
|
* </ul>
|
||||||
|
* <p>A baseline version allows n endpoint route to continue to work in
|
||||||
|
* subsequent versions if it remains compatible until an incompatible change
|
||||||
|
* eventually leads to the creation of a new route.
|
||||||
|
* @param version the version to use
|
||||||
|
* @return the created predicate instance
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public static RequestPredicate version(Object version) {
|
||||||
|
return new ApiVersionPredicate(version);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a {@code RequestPredicate} that matches if request's HTTP method is {@code GET}
|
* Return a {@code RequestPredicate} that matches if request's HTTP method is {@code GET}
|
||||||
* and the given {@code pattern} matches against the request path.
|
* and the given {@code pattern} matches against the request path.
|
||||||
|
@ -390,6 +411,14 @@ public abstract class RequestPredicates {
|
||||||
*/
|
*/
|
||||||
void queryParam(String name, String value);
|
void queryParam(String name, String value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive notification of an API version predicate. The version could
|
||||||
|
* be fixed ("1.2") or baseline ("1.2+").
|
||||||
|
* @param version the configured version
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
void version(String version);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receive first notification of a logical AND predicate.
|
* Receive first notification of a logical AND predicate.
|
||||||
* The first subsequent notification will contain the left-hand side of the AND-predicate;
|
* The first subsequent notification will contain the left-hand side of the AND-predicate;
|
||||||
|
@ -831,6 +860,69 @@ public abstract class RequestPredicates {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class ApiVersionPredicate implements RequestPredicate {
|
||||||
|
|
||||||
|
private final String version;
|
||||||
|
|
||||||
|
private final boolean baselineVersion;
|
||||||
|
|
||||||
|
private @Nullable Comparable<?> parsedVersion;
|
||||||
|
|
||||||
|
public ApiVersionPredicate(Object version) {
|
||||||
|
if (version instanceof String s) {
|
||||||
|
this.baselineVersion = s.endsWith("+");
|
||||||
|
this.version = initVersion(s, this.baselineVersion);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.baselineVersion = false;
|
||||||
|
this.version = version.toString();
|
||||||
|
this.parsedVersion = (Comparable<?>) version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String initVersion(String version, boolean baselineVersion) {
|
||||||
|
return (baselineVersion ? version.substring(0, version.length() - 1) : version);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(ServerRequest request) {
|
||||||
|
if (this.parsedVersion == null) {
|
||||||
|
ApiVersionStrategy strategy = request.apiVersionStrategy();
|
||||||
|
Assert.state(strategy != null, "No ApiVersionStrategy to parse version with");
|
||||||
|
this.parsedVersion = strategy.parseVersion(this.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
Comparable<?> requestVersion =
|
||||||
|
(Comparable<?>) request.attribute(HandlerMapping.API_VERSION_ATTRIBUTE).orElse(null);
|
||||||
|
|
||||||
|
if (requestVersion == null) {
|
||||||
|
traceMatch("Version", this.version, null, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int result = compareVersions(this.parsedVersion, requestVersion);
|
||||||
|
boolean match = (this.baselineVersion ? result <= 0 : result == 0);
|
||||||
|
traceMatch("Version", this.version, requestVersion, match);
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <V extends Comparable<V>> int compareVersions(Object v1, Object v2) {
|
||||||
|
return ((V) v1).compareTo((V) v2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(Visitor visitor) {
|
||||||
|
visitor.version(this.version + (this.baselineVersion ? "+" : ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Deprecated(since = "7.0", forRemoval = true)
|
@Deprecated(since = "7.0", forRemoval = true)
|
||||||
private static class PathExtensionPredicate implements RequestPredicate {
|
private static class PathExtensionPredicate implements RequestPredicate {
|
||||||
|
|
||||||
|
@ -1189,6 +1281,11 @@ public abstract class RequestPredicates {
|
||||||
return this.delegate.messageReaders();
|
return this.delegate.messageReaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable ApiVersionStrategy apiVersionStrategy() {
|
||||||
|
return this.delegate.apiVersionStrategy();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
|
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
|
||||||
return this.delegate.body(extractor);
|
return this.delegate.body(extractor);
|
||||||
|
|
|
@ -49,6 +49,7 @@ import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
|
import org.springframework.web.reactive.accept.ApiVersionStrategy;
|
||||||
import org.springframework.web.reactive.function.BodyExtractor;
|
import org.springframework.web.reactive.function.BodyExtractor;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.springframework.web.server.WebSession;
|
import org.springframework.web.server.WebSession;
|
||||||
|
@ -130,6 +131,12 @@ public interface ServerRequest {
|
||||||
*/
|
*/
|
||||||
List<HttpMessageReader<?>> messageReaders();
|
List<HttpMessageReader<?>> messageReaders();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the configured {@link ApiVersionStrategy}, or {@code null}.
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
@Nullable ApiVersionStrategy apiVersionStrategy();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the body with the given {@code BodyExtractor}.
|
* Extract the body with the given {@code BodyExtractor}.
|
||||||
* @param extractor the {@code BodyExtractor} that reads from the request
|
* @param extractor the {@code BodyExtractor} that reads from the request
|
||||||
|
@ -424,6 +431,23 @@ public interface ServerRequest {
|
||||||
return new DefaultServerRequest(exchange, messageReaders);
|
return new DefaultServerRequest(exchange, messageReaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@code ServerRequest} based on the given {@code ServerWebExchange} and
|
||||||
|
* message readers.
|
||||||
|
* @param exchange the exchange
|
||||||
|
* @param messageReaders the message readers
|
||||||
|
* @param versionStrategy a strategy to use to parse version
|
||||||
|
* @return the created {@code ServerRequest}
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
static ServerRequest create(
|
||||||
|
ServerWebExchange exchange, List<HttpMessageReader<?>> messageReaders,
|
||||||
|
@Nullable ApiVersionStrategy versionStrategy) {
|
||||||
|
|
||||||
|
return new DefaultServerRequest(exchange, messageReaders, versionStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a builder with the {@linkplain HttpMessageReader message readers},
|
* Create a builder with the {@linkplain HttpMessageReader message readers},
|
||||||
* method name, URI, headers, cookies, and attributes of the given request.
|
* method name, URI, headers, cookies, and attributes of the given request.
|
||||||
|
|
|
@ -119,6 +119,11 @@ class ToStringVisitor implements RouterFunctions.Visitor, RequestPredicates.Visi
|
||||||
this.builder.append(String.format("?%s == %s", name, value));
|
this.builder.append(String.format("?%s == %s", name, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void version(String version) {
|
||||||
|
this.builder.append(String.format("version: %s", version));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void startAnd() {
|
public void startAnd() {
|
||||||
this.builder.append('(');
|
this.builder.append('(');
|
||||||
|
|
|
@ -29,6 +29,8 @@ import org.springframework.http.codec.HttpMessageReader;
|
||||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||||
import org.springframework.http.server.reactive.observation.ServerRequestObservationContext;
|
import org.springframework.http.server.reactive.observation.ServerRequestObservationContext;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.web.reactive.accept.ApiVersionStrategy;
|
||||||
|
import org.springframework.web.reactive.accept.DefaultApiVersionStrategy;
|
||||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
|
@ -54,6 +56,8 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
|
||||||
|
|
||||||
private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
|
private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
|
||||||
|
|
||||||
|
private @Nullable ApiVersionStrategy versionStrategy;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an empty {@code RouterFunctionMapping}.
|
* Create an empty {@code RouterFunctionMapping}.
|
||||||
|
@ -92,6 +96,16 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
|
||||||
this.messageReaders = messageReaders;
|
this.messageReaders = messageReaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a strategy to manage API versioning.
|
||||||
|
* @param strategy the strategy to use
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public void setApiVersionStrategy(@Nullable ApiVersionStrategy strategy) {
|
||||||
|
this.versionStrategy = strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet() throws Exception {
|
public void afterPropertiesSet() throws Exception {
|
||||||
if (CollectionUtils.isEmpty(this.messageReaders)) {
|
if (CollectionUtils.isEmpty(this.messageReaders)) {
|
||||||
|
@ -102,8 +116,14 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
|
||||||
if (this.routerFunction == null) {
|
if (this.routerFunction == null) {
|
||||||
initRouterFunctions();
|
initRouterFunctions();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.routerFunction != null) {
|
if (this.routerFunction != null) {
|
||||||
RouterFunctions.changeParser(this.routerFunction, getPathPatternParser());
|
RouterFunctions.changeParser(this.routerFunction, getPathPatternParser());
|
||||||
|
if (this.versionStrategy instanceof DefaultApiVersionStrategy davs) {
|
||||||
|
if (davs.detectSupportedVersions()) {
|
||||||
|
this.routerFunction.accept(new SupportedVersionVisitor(davs));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -149,14 +169,26 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
|
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
|
||||||
if (this.routerFunction != null) {
|
|
||||||
ServerRequest request = ServerRequest.create(exchange, this.messageReaders);
|
if (this.routerFunction == null) {
|
||||||
return this.routerFunction.route(request)
|
|
||||||
.doOnNext(handler -> setAttributes(exchange.getAttributes(), request, handler));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.versionStrategy != null) {
|
||||||
|
Comparable<?> version = exchange.getAttribute(API_VERSION_ATTRIBUTE);
|
||||||
|
if (version == null) {
|
||||||
|
version = this.versionStrategy.resolveParseAndValidateVersion(exchange);
|
||||||
|
if (version != null) {
|
||||||
|
exchange.getAttributes().put(API_VERSION_ATTRIBUTE, version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerRequest request = ServerRequest.create(
|
||||||
|
exchange, this.messageReaders, this.versionStrategy);
|
||||||
|
|
||||||
|
return this.routerFunction.route(request)
|
||||||
|
.doOnNext(handler -> setAttributes(exchange.getAttributes(), request, handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
|
|
@ -44,6 +44,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
|
import org.springframework.web.reactive.accept.ApiVersionStrategy;
|
||||||
import org.springframework.web.reactive.function.BodyExtractor;
|
import org.springframework.web.reactive.function.BodyExtractor;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
@ -131,6 +132,11 @@ public class ServerRequestWrapper implements ServerRequest {
|
||||||
return this.delegate.messageReaders();
|
return this.delegate.messageReaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable ApiVersionStrategy apiVersionStrategy() {
|
||||||
|
return this.delegate.apiVersionStrategy();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
|
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
|
||||||
return this.delegate.body(extractor);
|
return this.delegate.body(extractor);
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-present the original author or 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 org.springframework.web.reactive.function.server.support;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.web.reactive.accept.DefaultApiVersionStrategy;
|
||||||
|
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.RequestPredicate;
|
||||||
|
import org.springframework.web.reactive.function.server.RequestPredicates;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RequestPredicates.Visitor} that discovers versions used in routes in
|
||||||
|
* order to add them to the list of supported versions.
|
||||||
|
*
|
||||||
|
* @author Rossen Stoyanchev
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
final class SupportedVersionVisitor implements RouterFunctions.Visitor, RequestPredicates.Visitor {
|
||||||
|
|
||||||
|
private final DefaultApiVersionStrategy versionStrategy;
|
||||||
|
|
||||||
|
|
||||||
|
SupportedVersionVisitor(DefaultApiVersionStrategy versionStrategy) {
|
||||||
|
this.versionStrategy = versionStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// RouterFunctions.Visitor
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startNested(RequestPredicate predicate) {
|
||||||
|
predicate.accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endNested(RequestPredicate predicate) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void route(RequestPredicate predicate, HandlerFunction<?> handlerFunction) {
|
||||||
|
predicate.accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resources(Function<ServerRequest, Mono<Resource>> lookupFunction) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void attributes(Map<String, Object> attributes) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unknown(RouterFunction<?> routerFunction) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// RequestPredicates.Visitor
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void method(Set<HttpMethod> methods) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void path(String pattern) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("removal")
|
||||||
|
@Override
|
||||||
|
public void pathExtension(String extension) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void header(String name, String value) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void queryParam(String name, String value) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void version(String version) {
|
||||||
|
this.versionStrategy.addMappedVersion(
|
||||||
|
(version.endsWith("+") ? version.substring(0, version.length() - 1) : version));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startAnd() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void and() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endAnd() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startOr() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void or() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endOr() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startNegate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endNegate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unknown(RequestPredicate predicate) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -26,6 +26,10 @@ import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||||
|
import org.springframework.web.reactive.HandlerMapping;
|
||||||
|
import org.springframework.web.reactive.accept.ApiVersionStrategy;
|
||||||
|
import org.springframework.web.reactive.accept.DefaultApiVersionStrategy;
|
||||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||||
import org.springframework.web.testfixture.server.MockServerWebExchange;
|
import org.springframework.web.testfixture.server.MockServerWebExchange;
|
||||||
import org.springframework.web.util.pattern.PathPatternParser;
|
import org.springframework.web.util.pattern.PathPatternParser;
|
||||||
|
@ -359,4 +363,24 @@ class RequestPredicatesTests {
|
||||||
assertThat(predicate.test(request)).isFalse();
|
assertThat(predicate.test(request)).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void version() {
|
||||||
|
assertThat(RequestPredicates.version("1.1").test(serverRequest("1.1"))).isTrue();
|
||||||
|
assertThat(RequestPredicates.version("1.1+").test(serverRequest("1.5"))).isTrue();
|
||||||
|
assertThat(RequestPredicates.version("1.1").test(serverRequest("1.5"))).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DefaultServerRequest serverRequest(String version) {
|
||||||
|
ApiVersionStrategy versionStrategy = apiVersionStrategy();
|
||||||
|
Comparable<?> parsedVersion = versionStrategy.parseVersion(version);
|
||||||
|
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("https://localhost"));
|
||||||
|
exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, parsedVersion);
|
||||||
|
return new DefaultServerRequest(exchange, Collections.emptyList(), versionStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DefaultApiVersionStrategy apiVersionStrategy() {
|
||||||
|
return new DefaultApiVersionStrategy(
|
||||||
|
List.of(exchange -> null), new SemanticApiVersionParser(), true, null, false, null);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-present the original author or 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 org.springframework.web.reactive.function.server.support;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.web.reactive.config.ApiVersionConfigurer;
|
||||||
|
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||||
|
import org.springframework.web.reactive.config.WebFluxConfigurer;
|
||||||
|
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||||
|
import org.springframework.web.testfixture.server.MockServerWebExchange;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.web.reactive.function.server.RequestPredicates.version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RouterFunctionMapping} integration tests for API versioning.
|
||||||
|
* @author Rossen Stoyanchev
|
||||||
|
*/
|
||||||
|
public class RouterFunctionMappingVersionTests {
|
||||||
|
|
||||||
|
private RouterFunctionMapping mapping;
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext();
|
||||||
|
wac.register(WebConfig.class);
|
||||||
|
wac.refresh();
|
||||||
|
|
||||||
|
this.mapping = wac.getBean(RouterFunctionMapping.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mapVersion() {
|
||||||
|
testGetHandler("1.0", "none");
|
||||||
|
testGetHandler("1.1", "none");
|
||||||
|
testGetHandler("1.2", "1.2");
|
||||||
|
testGetHandler("1.3", "1.2");
|
||||||
|
testGetHandler("1.5", "1.5");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void testGetHandler(String version, String expectedBody) {
|
||||||
|
|
||||||
|
MockServerWebExchange exchange = MockServerWebExchange.from(
|
||||||
|
MockServerHttpRequest.get("/").header("X-API-Version", version));
|
||||||
|
|
||||||
|
Mono<?> result = this.mapping.getHandler(exchange);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.consumeNextWith(handler -> assertThat(((TestHandler) handler).body()).isEqualTo(expectedBody))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@EnableWebFlux
|
||||||
|
private static class WebConfig implements WebFluxConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureApiVersioning(ApiVersionConfigurer configurer) {
|
||||||
|
configurer.useRequestHeader("X-API-Version").addSupportedVersions("1", "1.1", "1.3");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
RouterFunction<?> routerFunction() {
|
||||||
|
return RouterFunctions.route()
|
||||||
|
.path("/", builder -> builder
|
||||||
|
.GET(version("1.5"), new TestHandler("1.5"))
|
||||||
|
.GET(version("1.2+"), new TestHandler("1.2"))
|
||||||
|
.GET(new TestHandler("none")))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private record TestHandler(String body) implements HandlerFunction<ServerResponse> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<ServerResponse> handle(ServerRequest request) {
|
||||||
|
return ServerResponse.ok().bodyValue(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -544,13 +544,15 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
|
||||||
@Bean
|
@Bean
|
||||||
public RouterFunctionMapping routerFunctionMapping(
|
public RouterFunctionMapping routerFunctionMapping(
|
||||||
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
|
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
|
||||||
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
|
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider,
|
||||||
|
@Qualifier("mvcApiVersionStrategy") @Nullable ApiVersionStrategy versionStrategy) {
|
||||||
|
|
||||||
RouterFunctionMapping mapping = new RouterFunctionMapping();
|
RouterFunctionMapping mapping = new RouterFunctionMapping();
|
||||||
mapping.setOrder(-1); // go before RequestMappingHandlerMapping
|
mapping.setOrder(-1); // go before RequestMappingHandlerMapping
|
||||||
mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
|
mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
|
||||||
mapping.setCorsConfigurations(getCorsConfigurations());
|
mapping.setCorsConfigurations(getCorsConfigurations());
|
||||||
mapping.setMessageConverters(getMessageConverters());
|
mapping.setMessageConverters(getMessageConverters());
|
||||||
|
mapping.setApiVersionStrategy(versionStrategy);
|
||||||
|
|
||||||
PathPatternParser patternParser = getPathMatchConfigurer().getPatternParser();
|
PathPatternParser patternParser = getPathMatchConfigurer().getPatternParser();
|
||||||
if (patternParser != null) {
|
if (patternParser != null) {
|
||||||
|
|
|
@ -71,6 +71,7 @@ import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||||
|
import org.springframework.web.accept.ApiVersionStrategy;
|
||||||
import org.springframework.web.bind.ServletRequestDataBinder;
|
import org.springframework.web.bind.ServletRequestDataBinder;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.context.request.ServletWebRequest;
|
import org.springframework.web.context.request.ServletWebRequest;
|
||||||
|
@ -97,6 +98,8 @@ class DefaultServerRequest implements ServerRequest {
|
||||||
|
|
||||||
private final List<HttpMessageConverter<?>> messageConverters;
|
private final List<HttpMessageConverter<?>> messageConverters;
|
||||||
|
|
||||||
|
private final @Nullable ApiVersionStrategy versionStrategy;
|
||||||
|
|
||||||
private final MultiValueMap<String, String> params;
|
private final MultiValueMap<String, String> params;
|
||||||
|
|
||||||
private final Map<String, Object> attributes;
|
private final Map<String, Object> attributes;
|
||||||
|
@ -105,8 +108,16 @@ class DefaultServerRequest implements ServerRequest {
|
||||||
|
|
||||||
|
|
||||||
public DefaultServerRequest(HttpServletRequest servletRequest, List<HttpMessageConverter<?>> messageConverters) {
|
public DefaultServerRequest(HttpServletRequest servletRequest, List<HttpMessageConverter<?>> messageConverters) {
|
||||||
|
this(servletRequest, messageConverters, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultServerRequest(
|
||||||
|
HttpServletRequest servletRequest, List<HttpMessageConverter<?>> messageConverters,
|
||||||
|
@Nullable ApiVersionStrategy versionStrategy) {
|
||||||
|
|
||||||
this.serverHttpRequest = new ServletServerHttpRequest(servletRequest);
|
this.serverHttpRequest = new ServletServerHttpRequest(servletRequest);
|
||||||
this.messageConverters = List.copyOf(messageConverters);
|
this.messageConverters = List.copyOf(messageConverters);
|
||||||
|
this.versionStrategy = versionStrategy;
|
||||||
|
|
||||||
this.headers = new DefaultRequestHeaders(this.serverHttpRequest.getHeaders());
|
this.headers = new DefaultRequestHeaders(this.serverHttpRequest.getHeaders());
|
||||||
this.params = CollectionUtils.toMultiValueMap(new ServletParametersMap(servletRequest));
|
this.params = CollectionUtils.toMultiValueMap(new ServletParametersMap(servletRequest));
|
||||||
|
@ -172,6 +183,11 @@ class DefaultServerRequest implements ServerRequest {
|
||||||
return this.messageConverters;
|
return this.messageConverters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable ApiVersionStrategy apiVersionStrategy() {
|
||||||
|
return this.versionStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T body(Class<T> bodyType) throws IOException, ServletException {
|
public <T> T body(Class<T> bodyType) throws IOException, ServletException {
|
||||||
return bodyInternal(bodyType, bodyType);
|
return bodyInternal(bodyType, bodyType);
|
||||||
|
|
|
@ -57,6 +57,7 @@ import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||||
|
import org.springframework.web.accept.ApiVersionStrategy;
|
||||||
import org.springframework.web.bind.ServletRequestDataBinder;
|
import org.springframework.web.bind.ServletRequestDataBinder;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.util.UriBuilder;
|
import org.springframework.web.util.UriBuilder;
|
||||||
|
@ -74,6 +75,8 @@ class DefaultServerRequestBuilder implements ServerRequest.Builder {
|
||||||
|
|
||||||
private final List<HttpMessageConverter<?>> messageConverters;
|
private final List<HttpMessageConverter<?>> messageConverters;
|
||||||
|
|
||||||
|
private final @Nullable ApiVersionStrategy versionStrategy;
|
||||||
|
|
||||||
private HttpMethod method;
|
private HttpMethod method;
|
||||||
|
|
||||||
private URI uri;
|
private URI uri;
|
||||||
|
@ -95,6 +98,7 @@ class DefaultServerRequestBuilder implements ServerRequest.Builder {
|
||||||
Assert.notNull(other, "ServerRequest must not be null");
|
Assert.notNull(other, "ServerRequest must not be null");
|
||||||
this.servletRequest = other.servletRequest();
|
this.servletRequest = other.servletRequest();
|
||||||
this.messageConverters = new ArrayList<>(other.messageConverters());
|
this.messageConverters = new ArrayList<>(other.messageConverters());
|
||||||
|
this.versionStrategy = other.apiVersionStrategy();
|
||||||
this.method = other.method();
|
this.method = other.method();
|
||||||
this.uri = other.uri();
|
this.uri = other.uri();
|
||||||
headers(headers -> headers.addAll(other.headers().asHttpHeaders()));
|
headers(headers -> headers.addAll(other.headers().asHttpHeaders()));
|
||||||
|
@ -203,7 +207,8 @@ class DefaultServerRequestBuilder implements ServerRequest.Builder {
|
||||||
@Override
|
@Override
|
||||||
public ServerRequest build() {
|
public ServerRequest build() {
|
||||||
return new BuiltServerRequest(this.servletRequest, this.method, this.uri, this.headers, this.cookies,
|
return new BuiltServerRequest(this.servletRequest, this.method, this.uri, this.headers, this.cookies,
|
||||||
this.attributes, this.params, this.remoteAddress, this.body, this.messageConverters);
|
this.attributes, this.params, this.remoteAddress, this.body,
|
||||||
|
this.messageConverters, this.versionStrategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -225,6 +230,8 @@ class DefaultServerRequestBuilder implements ServerRequest.Builder {
|
||||||
|
|
||||||
private final List<HttpMessageConverter<?>> messageConverters;
|
private final List<HttpMessageConverter<?>> messageConverters;
|
||||||
|
|
||||||
|
private final @Nullable ApiVersionStrategy versionStrategy;
|
||||||
|
|
||||||
private final MultiValueMap<String, String> params;
|
private final MultiValueMap<String, String> params;
|
||||||
|
|
||||||
private final @Nullable InetSocketAddress remoteAddress;
|
private final @Nullable InetSocketAddress remoteAddress;
|
||||||
|
@ -232,7 +239,9 @@ class DefaultServerRequestBuilder implements ServerRequest.Builder {
|
||||||
public BuiltServerRequest(HttpServletRequest servletRequest, HttpMethod method, URI uri,
|
public BuiltServerRequest(HttpServletRequest servletRequest, HttpMethod method, URI uri,
|
||||||
HttpHeaders headers, MultiValueMap<String, Cookie> cookies,
|
HttpHeaders headers, MultiValueMap<String, Cookie> cookies,
|
||||||
Map<String, Object> attributes, MultiValueMap<String, String> params,
|
Map<String, Object> attributes, MultiValueMap<String, String> params,
|
||||||
@Nullable InetSocketAddress remoteAddress, byte[] body, List<HttpMessageConverter<?>> messageConverters) {
|
@Nullable InetSocketAddress remoteAddress, byte[] body,
|
||||||
|
List<HttpMessageConverter<?>> messageConverters,
|
||||||
|
@Nullable ApiVersionStrategy versionStrategy) {
|
||||||
|
|
||||||
this.servletRequest = servletRequest;
|
this.servletRequest = servletRequest;
|
||||||
this.method = method;
|
this.method = method;
|
||||||
|
@ -244,6 +253,7 @@ class DefaultServerRequestBuilder implements ServerRequest.Builder {
|
||||||
this.remoteAddress = remoteAddress;
|
this.remoteAddress = remoteAddress;
|
||||||
this.body = body;
|
this.body = body;
|
||||||
this.messageConverters = messageConverters;
|
this.messageConverters = messageConverters;
|
||||||
|
this.versionStrategy = versionStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -289,6 +299,11 @@ class DefaultServerRequestBuilder implements ServerRequest.Builder {
|
||||||
return this.messageConverters;
|
return this.messageConverters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable ApiVersionStrategy apiVersionStrategy() {
|
||||||
|
return this.versionStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T body(Class<T> bodyType) throws IOException, ServletException {
|
public <T> T body(Class<T> bodyType) throws IOException, ServletException {
|
||||||
return bodyInternal(bodyType, bodyType);
|
return bodyInternal(bodyType, bodyType);
|
||||||
|
|
|
@ -54,8 +54,10 @@ import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.MimeTypeUtils;
|
import org.springframework.util.MimeTypeUtils;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
|
import org.springframework.web.accept.ApiVersionStrategy;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.cors.CorsUtils;
|
import org.springframework.web.cors.CorsUtils;
|
||||||
|
import org.springframework.web.servlet.HandlerMapping;
|
||||||
import org.springframework.web.util.UriBuilder;
|
import org.springframework.web.util.UriBuilder;
|
||||||
import org.springframework.web.util.UriUtils;
|
import org.springframework.web.util.UriUtils;
|
||||||
import org.springframework.web.util.pattern.PathPattern;
|
import org.springframework.web.util.pattern.PathPattern;
|
||||||
|
@ -181,6 +183,25 @@ public abstract class RequestPredicates {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code RequestPredicate} to match to the request API version extracted
|
||||||
|
* from and parsed with the configured {@link ApiVersionStrategy}.
|
||||||
|
* <p>The version may be one of the following:
|
||||||
|
* <ul>
|
||||||
|
* <li>Fixed version ("1.2") -- match this version only.
|
||||||
|
* <li>Baseline version ("1.2+") -- match this and subsequent versions.
|
||||||
|
* </ul>
|
||||||
|
* <p>A baseline version allows n endpoint route to continue to work in
|
||||||
|
* subsequent versions if it remains compatible until an incompatible change
|
||||||
|
* eventually leads to the creation of a new route.
|
||||||
|
* @param version the version to use
|
||||||
|
* @return the created predicate instance
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public static RequestPredicate version(Object version) {
|
||||||
|
return new ApiVersionPredicate(version);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a {@code RequestPredicate} that matches if request's HTTP method is {@code GET}
|
* Return a {@code RequestPredicate} that matches if request's HTTP method is {@code GET}
|
||||||
* and the given {@code pattern} matches against the request path.
|
* and the given {@code pattern} matches against the request path.
|
||||||
|
@ -388,6 +409,14 @@ public abstract class RequestPredicates {
|
||||||
*/
|
*/
|
||||||
void param(String name, String value);
|
void param(String name, String value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive notification of an API version predicate. The version could
|
||||||
|
* be fixed ("1.2") or baseline ("1.2+").
|
||||||
|
* @param version the configured version
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
void version(String version);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receive first notification of a logical AND predicate.
|
* Receive first notification of a logical AND predicate.
|
||||||
* The first subsequent notification will contain the left-hand side of the AND-predicate;
|
* The first subsequent notification will contain the left-hand side of the AND-predicate;
|
||||||
|
@ -829,6 +858,69 @@ public abstract class RequestPredicates {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class ApiVersionPredicate implements RequestPredicate {
|
||||||
|
|
||||||
|
private final String version;
|
||||||
|
|
||||||
|
private final boolean baselineVersion;
|
||||||
|
|
||||||
|
private @Nullable Comparable<?> parsedVersion;
|
||||||
|
|
||||||
|
public ApiVersionPredicate(Object version) {
|
||||||
|
if (version instanceof String s) {
|
||||||
|
this.baselineVersion = s.endsWith("+");
|
||||||
|
this.version = initVersion(s, this.baselineVersion);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.baselineVersion = false;
|
||||||
|
this.version = version.toString();
|
||||||
|
this.parsedVersion = (Comparable<?>) version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String initVersion(String version, boolean baselineVersion) {
|
||||||
|
return (baselineVersion ? version.substring(0, version.length() - 1) : version);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(ServerRequest request) {
|
||||||
|
if (this.parsedVersion == null) {
|
||||||
|
ApiVersionStrategy strategy = request.apiVersionStrategy();
|
||||||
|
Assert.state(strategy != null, "No ApiVersionStrategy to parse version with");
|
||||||
|
this.parsedVersion = strategy.parseVersion(this.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
Comparable<?> requestVersion =
|
||||||
|
(Comparable<?>) request.attribute(HandlerMapping.API_VERSION_ATTRIBUTE).orElse(null);
|
||||||
|
|
||||||
|
if (requestVersion == null) {
|
||||||
|
traceMatch("Version", this.version, null, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int result = compareVersions(this.parsedVersion, requestVersion);
|
||||||
|
boolean match = (this.baselineVersion ? result <= 0 : result == 0);
|
||||||
|
traceMatch("Version", this.version, requestVersion, match);
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <V extends Comparable<V>> int compareVersions(Object v1, Object v2) {
|
||||||
|
return ((V) v1).compareTo((V) v2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(Visitor visitor) {
|
||||||
|
visitor.version(this.version + (this.baselineVersion ? "+" : ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Deprecated(since = "7.0", forRemoval = true)
|
@Deprecated(since = "7.0", forRemoval = true)
|
||||||
private static class PathExtensionPredicate implements RequestPredicate {
|
private static class PathExtensionPredicate implements RequestPredicate {
|
||||||
|
|
||||||
|
@ -1182,6 +1274,11 @@ public abstract class RequestPredicates {
|
||||||
return this.delegate.messageConverters();
|
return this.delegate.messageConverters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable ApiVersionStrategy apiVersionStrategy() {
|
||||||
|
return this.delegate.apiVersionStrategy();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T body(Class<T> bodyType) throws ServletException, IOException {
|
public <T> T body(Class<T> bodyType) throws ServletException, IOException {
|
||||||
return this.delegate.body(bodyType);
|
return this.delegate.body(bodyType);
|
||||||
|
|
|
@ -48,6 +48,7 @@ import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
|
import org.springframework.web.accept.ApiVersionStrategy;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.util.ServletRequestPathUtils;
|
import org.springframework.web.util.ServletRequestPathUtils;
|
||||||
import org.springframework.web.util.UriBuilder;
|
import org.springframework.web.util.UriBuilder;
|
||||||
|
@ -116,6 +117,13 @@ public interface ServerRequest {
|
||||||
*/
|
*/
|
||||||
List<HttpMessageConverter<?>> messageConverters();
|
List<HttpMessageConverter<?>> messageConverters();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the configured {@link ApiVersionStrategy}, or {@code null}.
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
ApiVersionStrategy apiVersionStrategy();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the body as an object of the given type.
|
* Extract the body as an object of the given type.
|
||||||
* @param bodyType the type of return value
|
* @param bodyType the type of return value
|
||||||
|
@ -373,6 +381,22 @@ public interface ServerRequest {
|
||||||
return new DefaultServerRequest(servletRequest, messageReaders);
|
return new DefaultServerRequest(servletRequest, messageReaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@code ServerRequest} based on the given {@code HttpServletRequest} and
|
||||||
|
* message converters.
|
||||||
|
* @param servletRequest the request
|
||||||
|
* @param messageReaders the message readers
|
||||||
|
* @param versionStrategy a strategy to use to parse version
|
||||||
|
* @return the created {@code ServerRequest}
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
static ServerRequest create(
|
||||||
|
HttpServletRequest servletRequest, List<HttpMessageConverter<?>> messageReaders,
|
||||||
|
@Nullable ApiVersionStrategy versionStrategy) {
|
||||||
|
|
||||||
|
return new DefaultServerRequest(servletRequest, messageReaders, versionStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a builder with the status, headers, and cookies of the given request.
|
* Create a builder with the status, headers, and cookies of the given request.
|
||||||
* @param other the response to copy the status, headers, and cookies from
|
* @param other the response to copy the status, headers, and cookies from
|
||||||
|
|
|
@ -118,6 +118,11 @@ class ToStringVisitor implements RouterFunctions.Visitor, RequestPredicates.Visi
|
||||||
this.builder.append(String.format("?%s == %s", name, value));
|
this.builder.append(String.format("?%s == %s", name, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void version(String version) {
|
||||||
|
this.builder.append(String.format("version: %s", version));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void startAnd() {
|
public void startAnd() {
|
||||||
this.builder.append('(');
|
this.builder.append('(');
|
||||||
|
|
|
@ -31,6 +31,8 @@ import org.springframework.http.converter.HttpMessageConverter;
|
||||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.web.accept.ApiVersionStrategy;
|
||||||
|
import org.springframework.web.accept.DefaultApiVersionStrategy;
|
||||||
import org.springframework.web.filter.ServerHttpObservationFilter;
|
import org.springframework.web.filter.ServerHttpObservationFilter;
|
||||||
import org.springframework.web.servlet.function.HandlerFunction;
|
import org.springframework.web.servlet.function.HandlerFunction;
|
||||||
import org.springframework.web.servlet.function.RouterFunction;
|
import org.springframework.web.servlet.function.RouterFunction;
|
||||||
|
@ -62,6 +64,8 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
|
||||||
|
|
||||||
private List<HttpMessageConverter<?>> messageConverters = Collections.emptyList();
|
private List<HttpMessageConverter<?>> messageConverters = Collections.emptyList();
|
||||||
|
|
||||||
|
private @Nullable ApiVersionStrategy versionStrategy;
|
||||||
|
|
||||||
private boolean detectHandlerFunctionsInAncestorContexts = false;
|
private boolean detectHandlerFunctionsInAncestorContexts = false;
|
||||||
|
|
||||||
|
|
||||||
|
@ -110,6 +114,15 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
|
||||||
this.messageConverters = messageConverters;
|
this.messageConverters = messageConverters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a strategy to manage API versioning.
|
||||||
|
* @param strategy the strategy to use
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public void setApiVersionStrategy(@Nullable ApiVersionStrategy strategy) {
|
||||||
|
this.versionStrategy = strategy;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set whether to detect handler functions in ancestor ApplicationContexts.
|
* Set whether to detect handler functions in ancestor ApplicationContexts.
|
||||||
* <p>Default is "false": Only handler functions in the current ApplicationContext
|
* <p>Default is "false": Only handler functions in the current ApplicationContext
|
||||||
|
@ -128,9 +141,11 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
|
||||||
if (this.routerFunction == null) {
|
if (this.routerFunction == null) {
|
||||||
initRouterFunctions();
|
initRouterFunctions();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CollectionUtils.isEmpty(this.messageConverters)) {
|
if (CollectionUtils.isEmpty(this.messageConverters)) {
|
||||||
initMessageConverters();
|
initMessageConverters();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.routerFunction != null) {
|
if (this.routerFunction != null) {
|
||||||
PathPatternParser patternParser = getPatternParser();
|
PathPatternParser patternParser = getPatternParser();
|
||||||
if (patternParser == null) {
|
if (patternParser == null) {
|
||||||
|
@ -138,6 +153,12 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
|
||||||
setPatternParser(patternParser);
|
setPatternParser(patternParser);
|
||||||
}
|
}
|
||||||
RouterFunctions.changeParser(this.routerFunction, patternParser);
|
RouterFunctions.changeParser(this.routerFunction, patternParser);
|
||||||
|
|
||||||
|
if (this.versionStrategy instanceof DefaultApiVersionStrategy davs) {
|
||||||
|
if (davs.detectSupportedVersions()) {
|
||||||
|
this.routerFunction.accept(new SupportedVersionVisitor(davs));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,15 +218,27 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected @Nullable Object getHandlerInternal(HttpServletRequest servletRequest) throws Exception {
|
protected @Nullable Object getHandlerInternal(HttpServletRequest servletRequest) throws Exception {
|
||||||
if (this.routerFunction != null) {
|
|
||||||
ServerRequest request = ServerRequest.create(servletRequest, this.messageConverters);
|
if (this.routerFunction == null) {
|
||||||
HandlerFunction<?> handlerFunction = this.routerFunction.route(request).orElse(null);
|
|
||||||
setAttributes(servletRequest, request, handlerFunction);
|
|
||||||
return handlerFunction;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.versionStrategy != null) {
|
||||||
|
Comparable<?> version = (Comparable<?>) servletRequest.getAttribute(API_VERSION_ATTRIBUTE);
|
||||||
|
if (version == null) {
|
||||||
|
version = this.versionStrategy.resolveParseAndValidateVersion(servletRequest);
|
||||||
|
if (version != null) {
|
||||||
|
servletRequest.setAttribute(API_VERSION_ATTRIBUTE, version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerRequest request =
|
||||||
|
ServerRequest.create(servletRequest, this.messageConverters, this.versionStrategy);
|
||||||
|
|
||||||
|
HandlerFunction<?> handlerFunction = this.routerFunction.route(request).orElse(null);
|
||||||
|
setAttributes(servletRequest, request, handlerFunction);
|
||||||
|
return handlerFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setAttributes(HttpServletRequest servletRequest, ServerRequest request,
|
private void setAttributes(HttpServletRequest servletRequest, ServerRequest request,
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-present the original author or 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 org.springframework.web.servlet.function.support;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.web.accept.DefaultApiVersionStrategy;
|
||||||
|
import org.springframework.web.servlet.function.HandlerFunction;
|
||||||
|
import org.springframework.web.servlet.function.RequestPredicate;
|
||||||
|
import org.springframework.web.servlet.function.RequestPredicates;
|
||||||
|
import org.springframework.web.servlet.function.RouterFunction;
|
||||||
|
import org.springframework.web.servlet.function.RouterFunctions;
|
||||||
|
import org.springframework.web.servlet.function.ServerRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RequestPredicates.Visitor} that discovers versions used in routes in
|
||||||
|
* order to add them to the list of supported versions.
|
||||||
|
*
|
||||||
|
* @author Rossen Stoyanchev
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
final class SupportedVersionVisitor implements RouterFunctions.Visitor, RequestPredicates.Visitor {
|
||||||
|
|
||||||
|
private final DefaultApiVersionStrategy versionStrategy;
|
||||||
|
|
||||||
|
|
||||||
|
SupportedVersionVisitor(DefaultApiVersionStrategy versionStrategy) {
|
||||||
|
this.versionStrategy = versionStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// RouterFunctions.Visitor
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startNested(RequestPredicate predicate) {
|
||||||
|
predicate.accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endNested(RequestPredicate predicate) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void route(RequestPredicate predicate, HandlerFunction<?> handlerFunction) {
|
||||||
|
predicate.accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resources(Function<ServerRequest, Optional<Resource>> lookupFunction) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void attributes(Map<String, Object> attributes) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unknown(RouterFunction<?> routerFunction) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// RequestPredicates.Visitor
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void method(Set<HttpMethod> methods) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void path(String pattern) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("removal")
|
||||||
|
@Override
|
||||||
|
public void pathExtension(String extension) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void header(String name, String value) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void param(String name, String value) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void version(String version) {
|
||||||
|
this.versionStrategy.addMappedVersion(
|
||||||
|
(version.endsWith("+") ? version.substring(0, version.length() - 1) : version));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startAnd() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void and() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endAnd() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startOr() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void or() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endOr() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startNegate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endNegate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unknown(RequestPredicate predicate) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -27,11 +27,15 @@ import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.accept.ApiVersionStrategy;
|
||||||
|
import org.springframework.web.accept.DefaultApiVersionStrategy;
|
||||||
|
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||||
import org.springframework.web.servlet.handler.PathPatternsTestUtils;
|
import org.springframework.web.servlet.handler.PathPatternsTestUtils;
|
||||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||||
import org.springframework.web.util.pattern.PathPatternParser;
|
import org.springframework.web.util.pattern.PathPatternParser;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.web.servlet.HandlerMapping.API_VERSION_ATTRIBUTE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
|
@ -266,12 +270,30 @@ class RequestPredicatesTests {
|
||||||
assertThat(predicate.test(request)).isFalse();
|
assertThat(predicate.test(request)).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void version() {
|
||||||
|
assertThat(RequestPredicates.version("1.1").test(serverRequest("1.1"))).isTrue();
|
||||||
|
assertThat(RequestPredicates.version("1.1+").test(serverRequest("1.5"))).isTrue();
|
||||||
|
assertThat(RequestPredicates.version("1.1").test(serverRequest("1.5"))).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
private ServerRequest initRequest(String httpMethod, String requestUri) {
|
private static ServerRequest serverRequest(String version) {
|
||||||
|
|
||||||
|
ApiVersionStrategy strategy = new DefaultApiVersionStrategy(
|
||||||
|
List.of(exchange -> null), new SemanticApiVersionParser(), true, null, false, null);
|
||||||
|
|
||||||
|
MockHttpServletRequest servletRequest =
|
||||||
|
PathPatternsTestUtils.initRequest("GET", null, "/path", true,
|
||||||
|
req -> req.setAttribute(API_VERSION_ATTRIBUTE, strategy.parseVersion(version)));
|
||||||
|
|
||||||
|
return new DefaultServerRequest(servletRequest, Collections.emptyList(), strategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServerRequest initRequest(String httpMethod, String requestUri) {
|
||||||
return initRequest(httpMethod, requestUri, null);
|
return initRequest(httpMethod, requestUri, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServerRequest initRequest(
|
private static ServerRequest initRequest(
|
||||||
String httpMethod, String requestUri, @Nullable Consumer<MockHttpServletRequest> initializer) {
|
String httpMethod, String requestUri, @Nullable Consumer<MockHttpServletRequest> initializer) {
|
||||||
|
|
||||||
return new DefaultServerRequest(
|
return new DefaultServerRequest(
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-present the original author or 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 org.springframework.web.servlet.function.support;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;
|
||||||
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
import org.springframework.web.servlet.function.HandlerFunction;
|
||||||
|
import org.springframework.web.servlet.function.RouterFunction;
|
||||||
|
import org.springframework.web.servlet.function.RouterFunctions;
|
||||||
|
import org.springframework.web.servlet.function.ServerRequest;
|
||||||
|
import org.springframework.web.servlet.function.ServerResponse;
|
||||||
|
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||||
|
import org.springframework.web.testfixture.servlet.MockServletContext;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.web.servlet.function.RequestPredicates.version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RouterFunctionMapping} integration tests for API versioning.
|
||||||
|
* @author Rossen Stoyanchev
|
||||||
|
*/
|
||||||
|
public class RouterFunctionMappingVersionTests {
|
||||||
|
|
||||||
|
private final MockServletContext servletContext = new MockServletContext();
|
||||||
|
|
||||||
|
private RouterFunctionMapping mapping;
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
AnnotationConfigWebApplicationContext wac = new AnnotationConfigWebApplicationContext();
|
||||||
|
wac.setServletContext(this.servletContext);
|
||||||
|
wac.register(WebConfig.class);
|
||||||
|
wac.refresh();
|
||||||
|
|
||||||
|
this.mapping = wac.getBean(RouterFunctionMapping.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mapVersion() throws Exception {
|
||||||
|
testGetHandler("1.0", "none");
|
||||||
|
testGetHandler("1.1", "none");
|
||||||
|
testGetHandler("1.2", "1.2");
|
||||||
|
testGetHandler("1.3", "1.2");
|
||||||
|
testGetHandler("1.5", "1.5");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void testGetHandler(String version, String expectedBody) throws Exception {
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
|
||||||
|
request.addHeader("X-API-Version", version);
|
||||||
|
HandlerFunction<?> handler = (HandlerFunction<?>) this.mapping.getHandler(request).getHandler();
|
||||||
|
assertThat(((TestHandler) handler).body()).isEqualTo(expectedBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@EnableWebMvc
|
||||||
|
private static class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureApiVersioning(ApiVersionConfigurer configurer) {
|
||||||
|
configurer.useRequestHeader("X-API-Version").addSupportedVersions("1", "1.1", "1.3");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
RouterFunction<?> routerFunction() {
|
||||||
|
return RouterFunctions.route()
|
||||||
|
.path("/", builder -> builder
|
||||||
|
.GET(version("1.5"), new TestHandler("1.5"))
|
||||||
|
.GET(version("1.2+"), new TestHandler("1.2"))
|
||||||
|
.GET(new TestHandler("none")))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private record TestHandler(String body) implements HandlerFunction<ServerResponse> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServerResponse handle(ServerRequest request) {
|
||||||
|
return ServerResponse.ok().body(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue