mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Refine handling of API version errors
Among HandlerMapping's some may not expect an API version. This is why those that do must be careful not to raise API validation errors if they don't match the request. Closes gh-36059
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.accept;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Simple container of the API version for a request (possibly {@code null}),
|
||||
* or an exception that resulted from trying to resolve, parse, and validate
|
||||
* the version.
|
||||
*
|
||||
* <p>While an API version needs to be initialized early, given that each
|
||||
* {@code HandlerMapping} may or may not expect an API version, it is important
|
||||
* to defer raising API version errors until it is known if the
|
||||
* {@code HandlerMapping} will handle the request.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
public final class ApiVersionHolder {
|
||||
|
||||
/** Static instance for a request without an API version. */
|
||||
public static final ApiVersionHolder EMPTY = ApiVersionHolder.fromVersion(null);
|
||||
|
||||
|
||||
private final @Nullable Comparable<?> version;
|
||||
|
||||
private final @Nullable RuntimeException exception;
|
||||
|
||||
|
||||
private ApiVersionHolder(@Nullable Comparable<?> version, @Nullable RuntimeException ex) {
|
||||
this.version = version;
|
||||
this.exception = ex;
|
||||
}
|
||||
|
||||
|
||||
public boolean hasVersion() {
|
||||
return (this.version != null);
|
||||
}
|
||||
|
||||
public boolean hasError() {
|
||||
return (this.exception != null);
|
||||
}
|
||||
|
||||
public Comparable<?> getVersion() {
|
||||
Assert.state(this.version != null, "No version");
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public @Nullable Comparable<?> getVersionIfPresent() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public RuntimeException getError() {
|
||||
Assert.state(this.exception != null, "No error");
|
||||
return this.exception;
|
||||
}
|
||||
|
||||
|
||||
public static ApiVersionHolder fromVersion(@Nullable Comparable<?> version) {
|
||||
return new ApiVersionHolder(version, null);
|
||||
}
|
||||
|
||||
public static ApiVersionHolder fromError(@Nullable RuntimeException ex) {
|
||||
return new ApiVersionHolder(null, ex);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package org.springframework.web.reactive;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
@@ -84,8 +85,9 @@ public interface HandlerMapping {
|
||||
String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes";
|
||||
|
||||
/**
|
||||
* Name of the {@link ServerWebExchange#getAttributes() attribute} containing
|
||||
* the resolved and parsed API version.
|
||||
* Name of the {@link ServerWebExchange#getAttributes() attribute} that
|
||||
* contains an {@link ApiVersionHolder} with the result of obtaining and
|
||||
* parsing the API version of the request.
|
||||
* @since 7.0
|
||||
*/
|
||||
String API_VERSION_ATTRIBUTE = HandlerMapping.class.getName() + ".apiVersion";
|
||||
|
||||
+7
-3
@@ -52,6 +52,7 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.cors.reactive.CorsUtils;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
@@ -892,14 +893,17 @@ public abstract class RequestPredicates {
|
||||
this.parsedVersion = strategy.parseVersion(this.version);
|
||||
}
|
||||
|
||||
Comparable<?> requestVersion =
|
||||
(Comparable<?>) request.attribute(HandlerMapping.API_VERSION_ATTRIBUTE).orElse(null);
|
||||
ApiVersionHolder requestVersionHolder =
|
||||
(ApiVersionHolder) request.attribute(HandlerMapping.API_VERSION_ATTRIBUTE)
|
||||
.orElseThrow(() -> new IllegalStateException("Expect API version attribute"));
|
||||
|
||||
if (requestVersion == null) {
|
||||
if (!requestVersionHolder.hasVersion()) {
|
||||
traceMatch("Version", this.version, null, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
Comparable<?> requestVersion = requestVersionHolder.getVersion();
|
||||
|
||||
int result = compareVersions(this.parsedVersion, requestVersion);
|
||||
boolean match = (this.baselineVersion ? result <= 0 : result == 0);
|
||||
traceMatch("Version", this.version, requestVersion, match);
|
||||
|
||||
+23
-10
@@ -28,6 +28,7 @@ import org.springframework.core.Ordered;
|
||||
import org.springframework.core.log.LogDelegateFactory;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.reactive.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.reactive.CorsProcessor;
|
||||
@@ -184,11 +185,14 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport
|
||||
|
||||
@Override
|
||||
public Mono<Object> getHandler(ServerWebExchange exchange) {
|
||||
initApiVersion(exchange);
|
||||
ApiVersionHolder versionHolder = initApiVersion(exchange);
|
||||
return getHandlerInternal(exchange).map(handler -> {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
|
||||
}
|
||||
if (versionHolder.hasError()) {
|
||||
throw versionHolder.getError();
|
||||
}
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
|
||||
CorsConfiguration config = (this.corsConfigurationSource != null ?
|
||||
@@ -204,8 +208,8 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport
|
||||
}
|
||||
}
|
||||
if (getApiVersionStrategy() != null) {
|
||||
Comparable<?> version = exchange.getAttribute(API_VERSION_ATTRIBUTE);
|
||||
if (version != null) {
|
||||
if (versionHolder.hasVersion()) {
|
||||
Comparable<?> version = versionHolder.getVersion();
|
||||
getApiVersionStrategy().handleDeprecations(version, handler, exchange);
|
||||
}
|
||||
}
|
||||
@@ -213,16 +217,25 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport
|
||||
});
|
||||
}
|
||||
|
||||
private void initApiVersion(ServerWebExchange exchange) {
|
||||
if (this.apiVersionStrategy != null) {
|
||||
Comparable<?> version = exchange.getAttribute(API_VERSION_ATTRIBUTE);
|
||||
if (version == null) {
|
||||
version = this.apiVersionStrategy.resolveParseAndValidateVersion(exchange);
|
||||
if (version != null) {
|
||||
exchange.getAttributes().put(API_VERSION_ATTRIBUTE, version);
|
||||
private ApiVersionHolder initApiVersion(ServerWebExchange exchange) {
|
||||
ApiVersionHolder versionHolder = exchange.getAttribute(API_VERSION_ATTRIBUTE);
|
||||
if (versionHolder == null) {
|
||||
if (this.apiVersionStrategy == null) {
|
||||
versionHolder = ApiVersionHolder.EMPTY;
|
||||
}
|
||||
else {
|
||||
Comparable<?> version;
|
||||
try {
|
||||
version = this.apiVersionStrategy.resolveParseAndValidateVersion(exchange);
|
||||
versionHolder = ApiVersionHolder.fromVersion(version);
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
versionHolder = ApiVersionHolder.fromError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
exchange.getAttributes().put(API_VERSION_ATTRIBUTE, versionHolder);
|
||||
return versionHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+9
-5
@@ -24,6 +24,7 @@ import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.NotAcceptableApiVersionException;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
@@ -99,12 +100,14 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||
|
||||
@Override
|
||||
public @Nullable VersionRequestCondition getMatchingCondition(ServerWebExchange exchange) {
|
||||
Comparable<?> requestVersion = exchange.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
ApiVersionHolder versionHolder = exchange.getRequiredAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
|
||||
if (this.version == null || requestVersion == null) {
|
||||
if (this.version == null || !versionHolder.hasVersion()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
Comparable<?> requestVersion = versionHolder.getVersion();
|
||||
|
||||
// Always use a baseline match here in order to select the highest version (baseline or fixed)
|
||||
// The fixed version match is enforced at the end in handleMatch()
|
||||
|
||||
@@ -130,8 +133,8 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||
else {
|
||||
// Prefer mappings with a version unless the request is without a version
|
||||
int result = this.version != null ? -1 : 1;
|
||||
Comparable<?> version = exchange.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
return (version == null ? -1 * result : result);
|
||||
ApiVersionHolder holder = exchange.getRequiredAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
return (!holder.hasVersion() ? -1 * result : result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +154,8 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||
*/
|
||||
public void handleMatch(ServerWebExchange exchange) {
|
||||
if (this.version != null && !this.baselineVersion) {
|
||||
Comparable<?> version = exchange.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
ApiVersionHolder holder = exchange.getRequiredAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
Comparable<?> version = holder.getVersionIfPresent();
|
||||
if (version != null && !this.version.equals(version)) {
|
||||
throw new NotAcceptableApiVersionException(version.toString());
|
||||
}
|
||||
|
||||
+3
-1
@@ -21,6 +21,7 @@ import java.util.Optional;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.MissingApiVersionException;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||
import org.springframework.web.reactive.BindingContext;
|
||||
@@ -45,7 +46,8 @@ public class ApiVersionMethodArgumentResolver implements SyncHandlerMethodArgume
|
||||
public @Nullable Object resolveArgumentValue(
|
||||
MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
|
||||
|
||||
Object version = exchange.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
ApiVersionHolder versionHolder = exchange.getRequiredAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
Object version = versionHolder.getVersionIfPresent();
|
||||
|
||||
if (parameter.getParameterType() == Optional.class) {
|
||||
return Optional.ofNullable(version);
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.accept.ApiVersionStrategy;
|
||||
@@ -374,7 +375,7 @@ class RequestPredicatesTests {
|
||||
ApiVersionStrategy versionStrategy = apiVersionStrategy();
|
||||
Comparable<?> parsedVersion = versionStrategy.parseVersion(version);
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("https://localhost"));
|
||||
exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, parsedVersion);
|
||||
exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.fromVersion(parsedVersion));
|
||||
return new DefaultServerRequest(exchange, Collections.emptyList(), versionStrategy);
|
||||
}
|
||||
|
||||
|
||||
+16
-10
@@ -26,7 +26,9 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.result.method.RequestMappingInfo;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||
@@ -99,7 +101,7 @@ class RequestMappingInfoTests {
|
||||
|
||||
@Test
|
||||
void matchPatternsCondition() {
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/foo"));
|
||||
MockServerWebExchange exchange = initExchange(MockServerHttpRequest.get("/foo"));
|
||||
|
||||
RequestMappingInfo info = paths("/foo*", "/bar").build();
|
||||
RequestMappingInfo expected = paths("/foo*").build();
|
||||
@@ -114,7 +116,7 @@ class RequestMappingInfoTests {
|
||||
|
||||
@Test
|
||||
void matchParamsCondition() {
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/foo?foo=bar"));
|
||||
ServerWebExchange exchange = initExchange(MockServerHttpRequest.get("/foo?foo=bar"));
|
||||
|
||||
RequestMappingInfo info = paths("/foo").params("foo=bar").build();
|
||||
RequestMappingInfo match = info.getMatchingCondition(exchange);
|
||||
@@ -129,8 +131,7 @@ class RequestMappingInfoTests {
|
||||
|
||||
@Test
|
||||
void matchHeadersCondition() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/foo").header("foo", "bar").build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
ServerWebExchange exchange = initExchange(MockServerHttpRequest.get("/foo").header("foo", "bar"));
|
||||
|
||||
RequestMappingInfo info = paths("/foo").headers("foo=bar").build();
|
||||
RequestMappingInfo match = info.getMatchingCondition(exchange);
|
||||
@@ -145,8 +146,8 @@ class RequestMappingInfoTests {
|
||||
|
||||
@Test
|
||||
void matchConsumesCondition() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/foo").contentType(MediaType.TEXT_PLAIN).build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
ServerWebExchange exchange = initExchange(
|
||||
MockServerHttpRequest.post("/foo").contentType(MediaType.TEXT_PLAIN));
|
||||
|
||||
RequestMappingInfo info = paths("/foo").consumes("text/plain").build();
|
||||
RequestMappingInfo match = info.getMatchingCondition(exchange);
|
||||
@@ -161,8 +162,7 @@ class RequestMappingInfoTests {
|
||||
|
||||
@Test
|
||||
void matchProducesCondition() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/foo").accept(MediaType.TEXT_PLAIN).build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
ServerWebExchange exchange = initExchange(MockServerHttpRequest.get("/foo").accept(MediaType.TEXT_PLAIN));
|
||||
|
||||
RequestMappingInfo info = paths("/foo").produces("text/plain").build();
|
||||
RequestMappingInfo match = info.getMatchingCondition(exchange);
|
||||
@@ -177,7 +177,7 @@ class RequestMappingInfoTests {
|
||||
|
||||
@Test
|
||||
void matchCustomCondition() {
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/foo?foo=bar"));
|
||||
ServerWebExchange exchange = initExchange(MockServerHttpRequest.get("/foo?foo=bar"));
|
||||
|
||||
RequestMappingInfo info = paths("/foo").params("foo=bar").build();
|
||||
RequestMappingInfo match = info.getMatchingCondition(exchange);
|
||||
@@ -198,7 +198,7 @@ class RequestMappingInfoTests {
|
||||
RequestMappingInfo oneMethod = paths().methods(RequestMethod.GET).build();
|
||||
RequestMappingInfo oneMethodOneParam = paths().methods(RequestMethod.GET).params("foo").build();
|
||||
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/foo"));
|
||||
ServerWebExchange exchange = initExchange(MockServerHttpRequest.get("/foo"));
|
||||
Comparator<RequestMappingInfo> comparator = (info, otherInfo) -> info.compareTo(otherInfo, exchange);
|
||||
|
||||
List<RequestMappingInfo> list = asList(none, oneMethod, oneMethodOneParam);
|
||||
@@ -327,4 +327,10 @@ class RequestMappingInfoTests {
|
||||
.containsOnly(MediaType.parseMediaType("application/hal+json"));
|
||||
}
|
||||
|
||||
private static MockServerWebExchange initExchange(MockServerHttpRequest.BaseBuilder<?> requestBuilder) {
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(requestBuilder.build());
|
||||
exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.EMPTY);
|
||||
return exchange;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-2
@@ -24,6 +24,7 @@ import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.NotAcceptableApiVersionException;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
@@ -179,13 +180,15 @@ public class VersionRequestConditionTests {
|
||||
}
|
||||
|
||||
private static MockServerWebExchange exchange() {
|
||||
return MockServerWebExchange.from(MockServerHttpRequest.get("/path"));
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path"));
|
||||
exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.EMPTY);
|
||||
return exchange;
|
||||
}
|
||||
|
||||
private ServerWebExchange exchangeWithVersion(String v) {
|
||||
Comparable<?> version = this.strategy.parseVersion(v);
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path"));
|
||||
exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, version);
|
||||
exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.fromVersion(version));
|
||||
return exchange;
|
||||
}
|
||||
|
||||
|
||||
+7
-3
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser.Version;
|
||||
import org.springframework.web.reactive.BindingContext;
|
||||
@@ -43,7 +44,7 @@ class ApiVersionMethodArgumentResolverTests {
|
||||
|
||||
private final ApiVersionMethodArgumentResolver resolver = new ApiVersionMethodArgumentResolver();
|
||||
|
||||
private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
|
||||
private MockServerWebExchange exchange;
|
||||
|
||||
private MethodParameter param;
|
||||
private MethodParameter nullableParam;
|
||||
@@ -54,6 +55,9 @@ class ApiVersionMethodArgumentResolverTests {
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
|
||||
this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
|
||||
this.exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.EMPTY);
|
||||
|
||||
Method method = getClass().getDeclaredMethod(
|
||||
"handle", Version.class, Version.class, Optional.class, int.class);
|
||||
|
||||
@@ -74,7 +78,7 @@ class ApiVersionMethodArgumentResolverTests {
|
||||
@Test
|
||||
void resolveArgument() throws Exception {
|
||||
Version version = new SemanticApiVersionParser().parseVersion("1.2");
|
||||
this.exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, version);
|
||||
this.exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.fromVersion(version));
|
||||
|
||||
Object actual = this.resolver.resolveArgumentValue(this.param, new BindingContext(), exchange);
|
||||
|
||||
@@ -92,7 +96,7 @@ class ApiVersionMethodArgumentResolverTests {
|
||||
@Test
|
||||
void resolveOptionalArgument() {
|
||||
Version version = new SemanticApiVersionParser().parseVersion("1.2");
|
||||
this.exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, version);
|
||||
this.exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.fromVersion(version));
|
||||
|
||||
Object actual = this.resolver.resolveArgumentValue(this.optionalParam, new BindingContext(), exchange);
|
||||
assertThat(actual).asInstanceOf(OPTIONAL).hasValue(version);
|
||||
|
||||
+56
@@ -22,11 +22,13 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Method;
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.cglib.proxy.Enhancer;
|
||||
import org.springframework.cglib.proxy.NoOp;
|
||||
@@ -35,6 +37,9 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.InvalidApiVersionException;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
@@ -44,7 +49,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.method.HandlerTypePredicate;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.accept.DefaultApiVersionStrategy;
|
||||
import org.springframework.web.reactive.accept.HeaderApiVersionResolver;
|
||||
import org.springframework.web.reactive.result.condition.ConsumesRequestCondition;
|
||||
import org.springframework.web.reactive.result.condition.MediaTypeExpression;
|
||||
import org.springframework.web.reactive.result.method.RequestMappingInfo;
|
||||
@@ -107,6 +116,35 @@ class RequestMappingHandlerMappingTests {
|
||||
assertThat(info.getPatternsCondition().getPatterns()).containsOnly(new PathPatternParser().parse("/api/user/{id}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void version() {
|
||||
ServerWebExchange exchange = initExchangeForVersionTest("1.1");
|
||||
HandlerMethod handlerMethod = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
|
||||
assertThat(handlerMethod.getMethod().getName()).isEqualTo("foo1_1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void versionInvalid() {
|
||||
ServerWebExchange exchange = initExchangeForVersionTest("99");
|
||||
StepVerifier.create(this.handlerMapping.getHandler(exchange))
|
||||
.verifyError(InvalidApiVersionException.class);
|
||||
}
|
||||
|
||||
private ServerWebExchange initExchangeForVersionTest(String version) {
|
||||
|
||||
((StaticWebApplicationContext) this.handlerMapping.getApplicationContext())
|
||||
.registerSingleton("controller", VersionController.class);
|
||||
|
||||
DefaultApiVersionStrategy versionStrategy = new DefaultApiVersionStrategy(
|
||||
List.of(new HeaderApiVersionResolver("API-Version")), new SemanticApiVersionParser(),
|
||||
true, null, true, null, null);
|
||||
this.handlerMapping.setApiVersionStrategy(versionStrategy);
|
||||
this.handlerMapping.afterPropertiesSet();
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/foo").header("API-Version", version).build();
|
||||
return MockServerWebExchange.from(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveRequestMappingViaComposedAnnotation() {
|
||||
RequestMappingInfo info = assertComposedAnnotationMapping("postJson", "/postJson", RequestMethod.POST);
|
||||
@@ -367,6 +405,7 @@ class RequestMappingHandlerMappingTests {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post(path)
|
||||
.contentType(mediaType).build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.EMPTY);
|
||||
RequestMappingInfo matchingInfo = info.getMatchingCondition(exchange);
|
||||
// Since the request has no body AND the required flag is false, the
|
||||
// ConsumesCondition in the matching condition in an EMPTY_CONDITION.
|
||||
@@ -409,6 +448,7 @@ class RequestMappingHandlerMappingTests {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post(path)
|
||||
.contentType(mediaType).build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.EMPTY);
|
||||
RequestMappingInfo matchingInfo = info.getMatchingCondition(exchange);
|
||||
assertThat(matchingInfo).isEqualTo(paths(path).methods(POST).consumes(mediaType.toString()).build());
|
||||
}
|
||||
@@ -568,6 +608,22 @@ class RequestMappingHandlerMappingTests {
|
||||
}
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/foo")
|
||||
static class VersionController {
|
||||
|
||||
@GetMapping
|
||||
public String foo() {
|
||||
return "foo";
|
||||
}
|
||||
|
||||
@GetMapping(version = "1.1")
|
||||
public String foo1_1() {
|
||||
return "foo1_1";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RestController
|
||||
@HttpExchange("/exchange")
|
||||
static class HttpExchangeController {
|
||||
|
||||
@@ -20,6 +20,8 @@ import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
|
||||
/**
|
||||
* Interface to be implemented by objects that define a mapping between
|
||||
* requests and handler objects.
|
||||
@@ -136,8 +138,9 @@ public interface HandlerMapping {
|
||||
String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes";
|
||||
|
||||
/**
|
||||
* Name of the {@link HttpServletRequest} attribute that contains the
|
||||
* resolved and parsed API version.
|
||||
* Name of the {@link HttpServletRequest} attribute that contains an
|
||||
* {@link ApiVersionHolder} with the
|
||||
* result of obtaining and parsing the API version of the request.
|
||||
* @since 7.0
|
||||
*/
|
||||
String API_VERSION_ATTRIBUTE = HandlerMapping.class.getName() + ".apiVersion";
|
||||
|
||||
+7
-3
@@ -54,6 +54,7 @@ import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.ApiVersionStrategy;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.cors.CorsUtils;
|
||||
@@ -890,14 +891,17 @@ public abstract class RequestPredicates {
|
||||
this.parsedVersion = strategy.parseVersion(this.version);
|
||||
}
|
||||
|
||||
Comparable<?> requestVersion =
|
||||
(Comparable<?>) request.attribute(HandlerMapping.API_VERSION_ATTRIBUTE).orElse(null);
|
||||
ApiVersionHolder requestVersionHolder =
|
||||
(ApiVersionHolder) request.attribute(HandlerMapping.API_VERSION_ATTRIBUTE)
|
||||
.orElseThrow(() -> new IllegalStateException("Expect API version attribute"));
|
||||
|
||||
if (requestVersion == null) {
|
||||
if (!requestVersionHolder.hasVersion()) {
|
||||
traceMatch("Version", this.version, null, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
Comparable<?> requestVersion = requestVersionHolder.getVersion();
|
||||
|
||||
int result = compareVersions(this.parsedVersion, requestVersion);
|
||||
boolean match = (this.baselineVersion ? result <= 0 : result == 0);
|
||||
traceMatch("Version", this.version, requestVersion, match);
|
||||
|
||||
+26
-10
@@ -39,6 +39,7 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.HttpRequestHandler;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.ApiVersionStrategy;
|
||||
import org.springframework.web.context.request.WebRequestInterceptor;
|
||||
import org.springframework.web.context.request.async.WebAsyncManager;
|
||||
@@ -539,7 +540,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
|
||||
*/
|
||||
@Override
|
||||
public final @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
|
||||
initApiVersion(request);
|
||||
ApiVersionHolder versionHolder = initApiVersion(request);
|
||||
Object handler = getHandlerInternal(request);
|
||||
if (handler == null) {
|
||||
handler = getDefaultHandler();
|
||||
@@ -547,6 +548,11 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
|
||||
if (handler == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (versionHolder.hasError()) {
|
||||
throw versionHolder.getError();
|
||||
}
|
||||
|
||||
// Bean name or resolved handler?
|
||||
if (handler instanceof String handlerName) {
|
||||
handler = obtainApplicationContext().getBean(handlerName);
|
||||
@@ -584,16 +590,25 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
|
||||
return executionChain;
|
||||
}
|
||||
|
||||
private void initApiVersion(HttpServletRequest request) {
|
||||
if (this.versionStrategy != null) {
|
||||
Comparable<?> version = (Comparable<?>) request.getAttribute(API_VERSION_ATTRIBUTE);
|
||||
if (version == null) {
|
||||
version = this.versionStrategy.resolveParseAndValidateVersion(request);
|
||||
if (version != null) {
|
||||
request.setAttribute(API_VERSION_ATTRIBUTE, version);
|
||||
private ApiVersionHolder initApiVersion(HttpServletRequest request) {
|
||||
ApiVersionHolder versionHolder = (ApiVersionHolder) request.getAttribute(API_VERSION_ATTRIBUTE);
|
||||
if (versionHolder == null) {
|
||||
if (this.versionStrategy == null) {
|
||||
versionHolder = ApiVersionHolder.EMPTY;
|
||||
}
|
||||
else {
|
||||
Comparable<?> version;
|
||||
try {
|
||||
version = this.versionStrategy.resolveParseAndValidateVersion(request);
|
||||
versionHolder = ApiVersionHolder.fromVersion(version);
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
versionHolder = ApiVersionHolder.fromError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
request.setAttribute(API_VERSION_ATTRIBUTE, versionHolder);
|
||||
return versionHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -683,8 +698,9 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
|
||||
}
|
||||
|
||||
if (this.versionStrategy != null) {
|
||||
Comparable<?> version = (Comparable<?>) request.getAttribute(API_VERSION_ATTRIBUTE);
|
||||
if (version != null) {
|
||||
ApiVersionHolder versionHolder = (ApiVersionHolder) request.getAttribute(API_VERSION_ATTRIBUTE);
|
||||
if (versionHolder.hasVersion()) {
|
||||
Comparable<?> version = versionHolder.getVersion();
|
||||
chain.addInterceptor(new ApiVersionDeprecationHandlerInterceptor(this.versionStrategy, version));
|
||||
}
|
||||
}
|
||||
|
||||
+9
-5
@@ -25,6 +25,7 @@ import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.ApiVersionStrategy;
|
||||
import org.springframework.web.accept.NotAcceptableApiVersionException;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -98,12 +99,14 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||
|
||||
@Override
|
||||
public @Nullable VersionRequestCondition getMatchingCondition(HttpServletRequest request) {
|
||||
Comparable<?> requestVersion = (Comparable<?>) request.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
ApiVersionHolder versionHolder = (ApiVersionHolder) request.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
|
||||
if (this.version == null || requestVersion == null) {
|
||||
if (this.version == null || !versionHolder.hasVersion()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
Comparable<?> requestVersion = versionHolder.getVersion();
|
||||
|
||||
// Always use a baseline match here in order to select the highest version (baseline or fixed)
|
||||
// The fixed version match is enforced at the end in handleMatch()
|
||||
|
||||
@@ -129,8 +132,8 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||
else {
|
||||
// Prefer mappings with a version unless the request is without a version
|
||||
int result = this.version != null ? -1 : 1;
|
||||
Comparable<?> version = (Comparable<?>) request.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
return (version == null ? -1 * result : result);
|
||||
ApiVersionHolder holder = (ApiVersionHolder) request.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
return (!holder.hasVersion() ? -1 * result : result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +153,8 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||
*/
|
||||
public void handleMatch(HttpServletRequest request) {
|
||||
if (this.version != null && !this.baselineVersion) {
|
||||
Comparable<?> version = (Comparable<?>) request.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
ApiVersionHolder holder = (ApiVersionHolder) request.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
Comparable<?> version = holder.getVersionIfPresent();
|
||||
if (version != null && !this.version.equals(version)) {
|
||||
throw new NotAcceptableApiVersionException(version.toString());
|
||||
}
|
||||
|
||||
+3
-1
@@ -23,6 +23,7 @@ import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.MissingApiVersionException;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
@@ -51,7 +52,8 @@ public class ApiVersionMethodArgumentResolver implements HandlerMethodArgumentRe
|
||||
|
||||
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
|
||||
Assert.state(request != null, "No HttpServletRequest");
|
||||
Object version = request.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
ApiVersionHolder versionHolder = (ApiVersionHolder) request.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||
Object version = versionHolder.getVersionIfPresent();
|
||||
|
||||
if (parameter.getParameterType() == Optional.class) {
|
||||
return Optional.ofNullable(version);
|
||||
|
||||
+3
-1
@@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.ApiVersionStrategy;
|
||||
import org.springframework.web.accept.DefaultApiVersionStrategy;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||
@@ -284,7 +285,8 @@ class RequestPredicatesTests {
|
||||
|
||||
MockHttpServletRequest servletRequest =
|
||||
PathPatternsTestUtils.initRequest("GET", null, "/path", true,
|
||||
req -> req.setAttribute(API_VERSION_ATTRIBUTE, strategy.parseVersion(version)));
|
||||
req -> req.setAttribute(API_VERSION_ATTRIBUTE,
|
||||
ApiVersionHolder.fromVersion(strategy.parseVersion(version))));
|
||||
|
||||
return new DefaultServerRequest(servletRequest, Collections.emptyList(), strategy);
|
||||
}
|
||||
|
||||
+8
-5
@@ -24,6 +24,7 @@ import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.DefaultApiVersionStrategy;
|
||||
import org.springframework.web.accept.NotAcceptableApiVersionException;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||
@@ -125,7 +126,8 @@ public class VersionRequestConditionTests {
|
||||
String version = "1.2";
|
||||
this.strategy = initVersionStrategy(version);
|
||||
VersionRequestCondition condition = condition(version);
|
||||
VersionRequestCondition match = condition.getMatchingCondition(new MockHttpServletRequest("GET", "/path"));
|
||||
|
||||
VersionRequestCondition match = condition.getMatchingCondition(requestWithVersion(null));
|
||||
|
||||
assertThat(match).isSameAs(condition);
|
||||
}
|
||||
@@ -141,7 +143,7 @@ public class VersionRequestConditionTests {
|
||||
private void testCompare(String expected, String... versions) {
|
||||
List<VersionRequestCondition> list = Arrays.stream(versions)
|
||||
.map(this::condition)
|
||||
.sorted((c1, c2) -> c1.compareTo(c2, new MockHttpServletRequest()))
|
||||
.sorted((c1, c2) -> c1.compareTo(c2, requestWithVersion(null)))
|
||||
.toList();
|
||||
|
||||
assertThat(list.get(0)).isEqualTo(condition(expected));
|
||||
@@ -150,7 +152,7 @@ public class VersionRequestConditionTests {
|
||||
@Test
|
||||
void compareWithoutRequestVersion() {
|
||||
VersionRequestCondition condition = Stream.of(condition("1.1"), condition("1.2"), emptyCondition())
|
||||
.min((c1, c2) -> c1.compareTo(c2, new MockHttpServletRequest()))
|
||||
.min((c1, c2) -> c1.compareTo(c2, requestWithVersion(null)))
|
||||
.get();
|
||||
|
||||
assertThat(condition).isEqualTo(emptyCondition());
|
||||
@@ -158,7 +160,7 @@ public class VersionRequestConditionTests {
|
||||
|
||||
@Test // gh-35236
|
||||
void noRequestVersion() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path");
|
||||
MockHttpServletRequest request = requestWithVersion(null);
|
||||
VersionRequestCondition condition = condition("1.1");
|
||||
|
||||
VersionRequestCondition match = condition.getMatchingCondition(request);
|
||||
@@ -178,7 +180,8 @@ public class VersionRequestConditionTests {
|
||||
|
||||
private MockHttpServletRequest requestWithVersion(String v) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path");
|
||||
request.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, this.strategy.parseVersion(v));
|
||||
Comparable<?> version = (v != null ? strategy.parseVersion(v) : null);
|
||||
request.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.fromVersion(version));
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser.Version;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
@@ -82,7 +83,7 @@ class ApiVersionMethodArgumentResolverTests {
|
||||
@Test
|
||||
void resolveArgument() throws Exception {
|
||||
Version version = new SemanticApiVersionParser().parseVersion("1.2");
|
||||
this.servletRequest.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, version);
|
||||
this.servletRequest.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.fromVersion(version));
|
||||
|
||||
Object actual = this.resolver.resolveArgument(this.param, this.mav, this.webRequest, null);
|
||||
|
||||
@@ -93,6 +94,7 @@ class ApiVersionMethodArgumentResolverTests {
|
||||
|
||||
@Test
|
||||
void resolveNullableArgument() throws Exception {
|
||||
this.servletRequest.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.EMPTY);
|
||||
Object actual = this.resolver.resolveArgument(this.nullableParam, this.mav, this.webRequest, null);
|
||||
assertThat(actual).isNull();
|
||||
}
|
||||
@@ -100,7 +102,7 @@ class ApiVersionMethodArgumentResolverTests {
|
||||
@Test
|
||||
void resolveOptionalArgument() throws Exception {
|
||||
Version version = new SemanticApiVersionParser().parseVersion("1.2");
|
||||
this.servletRequest.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, version);
|
||||
this.servletRequest.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.fromVersion(version));
|
||||
|
||||
Object actual = this.resolver.resolveArgument(this.optionalParam, this.mav, this.webRequest, null);
|
||||
assertThat(actual).asInstanceOf(OPTIONAL).hasValue(version);
|
||||
@@ -108,6 +110,7 @@ class ApiVersionMethodArgumentResolverTests {
|
||||
|
||||
@Test
|
||||
void resolveOptionalArgumentWhenEmpty() throws Exception {
|
||||
this.servletRequest.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.EMPTY);
|
||||
Object actual = this.resolver.resolveArgument(this.optionalParam, this.mav, this.webRequest, null);
|
||||
assertThat(actual).asInstanceOf(OPTIONAL).isEmpty();
|
||||
}
|
||||
|
||||
+2
-1
@@ -57,6 +57,7 @@ import org.springframework.ui.ModelMap;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.accept.ApiVersionHolder;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.CookieValue;
|
||||
@@ -176,7 +177,7 @@ class RequestMappingHandlerAdapterIntegrationTests {
|
||||
request.getSession().setAttribute("sessionAttribute", sessionAttribute);
|
||||
request.setAttribute("requestAttribute", requestAttribute);
|
||||
SemanticApiVersionParser.Version version = new SemanticApiVersionParser().parseVersion("1.2");
|
||||
request.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, version);
|
||||
request.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, ApiVersionHolder.fromVersion(version));
|
||||
|
||||
HandlerMethod handlerMethod = handlerMethod("handle", parameterTypes);
|
||||
ModelAndView mav = handlerAdapter.handle(request, response, handlerMethod);
|
||||
|
||||
+53
@@ -23,6 +23,7 @@ import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Method;
|
||||
import java.security.Principal;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -35,6 +36,10 @@ import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.accept.DefaultApiVersionStrategy;
|
||||
import org.springframework.web.accept.HeaderApiVersionResolver;
|
||||
import org.springframework.web.accept.InvalidApiVersionException;
|
||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
@@ -44,6 +49,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.method.HandlerTypePredicate;
|
||||
import org.springframework.web.service.annotation.HttpExchange;
|
||||
import org.springframework.web.service.annotation.PostExchange;
|
||||
@@ -60,6 +66,7 @@ import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.Named.named;
|
||||
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -160,6 +167,36 @@ class RequestMappingHandlerMappingTests {
|
||||
}
|
||||
}
|
||||
|
||||
@PathPatternsParameterizedTest
|
||||
void version(RequestMappingHandlerMapping mapping) throws Exception {
|
||||
MockHttpServletRequest request = initRequestForVersionTest(mapping, "1.1");
|
||||
HandlerMethod handlerMethod = (HandlerMethod) mapping.getHandler(request).getHandler();
|
||||
assertThat(handlerMethod.getMethod().getName()).isEqualTo("foo1_1");
|
||||
}
|
||||
|
||||
@PathPatternsParameterizedTest
|
||||
void versionInvalid(RequestMappingHandlerMapping mapping) throws Exception {
|
||||
MockHttpServletRequest request = initRequestForVersionTest(mapping, "99");
|
||||
assertThatThrownBy(() -> mapping.getHandler(request)).isInstanceOf(InvalidApiVersionException.class);
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest initRequestForVersionTest(
|
||||
RequestMappingHandlerMapping mapping, String version) {
|
||||
|
||||
((StaticWebApplicationContext) mapping.getApplicationContext())
|
||||
.registerSingleton("controller", VersionController.class);
|
||||
|
||||
DefaultApiVersionStrategy versionStrategy = new DefaultApiVersionStrategy(
|
||||
List.of(new HeaderApiVersionResolver("API-Version")), new SemanticApiVersionParser(),
|
||||
true, null, true, null, null);
|
||||
mapping.setApiVersionStrategy(versionStrategy);
|
||||
mapping.afterPropertiesSet();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
|
||||
request.addHeader("API-Version", version);
|
||||
return request;
|
||||
}
|
||||
|
||||
@PathPatternsParameterizedTest
|
||||
void resolveRequestMappingViaComposedAnnotation(RequestMappingHandlerMapping mapping) {
|
||||
RequestMappingInfo info = assertComposedAnnotationMapping(
|
||||
@@ -626,6 +663,22 @@ class RequestMappingHandlerMappingTests {
|
||||
}
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/foo")
|
||||
static class VersionController {
|
||||
|
||||
@GetMapping
|
||||
public String foo() {
|
||||
return "foo";
|
||||
}
|
||||
|
||||
@GetMapping(version = "1.1")
|
||||
public String foo1_1() {
|
||||
return "foo1_1";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RestController
|
||||
@HttpExchange("/exchange")
|
||||
static class HttpExchangeController {
|
||||
|
||||
Reference in New Issue
Block a user