Resolve API version in RequestMappingHandlerMapping
API version resolution and parsing is already applied as long as an ApiVersionStrategy is configured and irrespective of whether a given RequestMapping has a version or not. RequestMappingHandlerMapping also needs to be aware of the API version in order to apply deprecated version handling. So it is better to resolve, parse, and validate the version in the beginning of handler mapping rather than in the first call to any VersionRequestCondition. Closes gh-35049
This commit is contained in:
parent
492e51f3ba
commit
ffdf941219
|
@ -36,7 +36,7 @@ public class DefaultApiVersionStrategiesTests {
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void defaultVersion() {
|
void defaultVersionIsParsed() {
|
||||||
SemanticApiVersionParser.Version version = this.parser.parseVersion("1.2.3");
|
SemanticApiVersionParser.Version version = this.parser.parseVersion("1.2.3");
|
||||||
ApiVersionStrategy strategy = initVersionStrategy(version.toString());
|
ApiVersionStrategy strategy = initVersionStrategy(version.toString());
|
||||||
|
|
||||||
|
@ -44,21 +44,29 @@ public class DefaultApiVersionStrategiesTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void supportedVersions() {
|
void validateSupportedVersion() {
|
||||||
SemanticApiVersionParser.Version v1 = this.parser.parseVersion("1");
|
SemanticApiVersionParser.Version v12 = this.parser.parseVersion("1.2");
|
||||||
SemanticApiVersionParser.Version v2 = this.parser.parseVersion("2");
|
|
||||||
SemanticApiVersionParser.Version v9 = this.parser.parseVersion("9");
|
|
||||||
|
|
||||||
DefaultApiVersionStrategy strategy = initVersionStrategy(null);
|
DefaultApiVersionStrategy strategy = initVersionStrategy(null);
|
||||||
strategy.addSupportedVersion(v1.toString());
|
strategy.addSupportedVersion(v12.toString());
|
||||||
strategy.addSupportedVersion(v2.toString());
|
|
||||||
|
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||||
strategy.validateVersion(v1, request);
|
strategy.validateVersion(v12, request);
|
||||||
strategy.validateVersion(v2, request);
|
}
|
||||||
|
|
||||||
assertThatThrownBy(() -> strategy.validateVersion(v9, request))
|
@Test
|
||||||
.isInstanceOf(InvalidApiVersionException.class);
|
void validateUnsupportedVersion() {
|
||||||
|
assertThatThrownBy(() -> initVersionStrategy(null).validateVersion("1.2", new MockHttpServletRequest()))
|
||||||
|
.isInstanceOf(InvalidApiVersionException.class)
|
||||||
|
.hasMessage("400 BAD_REQUEST \"Invalid API version: '1.2'.\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void missingRequiredVersion() {
|
||||||
|
DefaultApiVersionStrategy strategy = initVersionStrategy(null);
|
||||||
|
assertThatThrownBy(() -> strategy.validateVersion(null, new MockHttpServletRequest()))
|
||||||
|
.isInstanceOf(MissingApiVersionException.class)
|
||||||
|
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultValue) {
|
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultValue) {
|
||||||
|
|
|
@ -24,7 +24,6 @@ import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.accept.InvalidApiVersionException;
|
|
||||||
import org.springframework.web.accept.NotAcceptableApiVersionException;
|
import org.springframework.web.accept.NotAcceptableApiVersionException;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.reactive.HandlerMapping;
|
import org.springframework.web.reactive.HandlerMapping;
|
||||||
|
@ -42,21 +41,12 @@ import org.springframework.web.server.ServerWebExchange;
|
||||||
*/
|
*/
|
||||||
public final class VersionRequestCondition extends AbstractRequestCondition<VersionRequestCondition> {
|
public final class VersionRequestCondition extends AbstractRequestCondition<VersionRequestCondition> {
|
||||||
|
|
||||||
private static final String VERSION_ATTRIBUTE_NAME = VersionRequestCondition.class.getName() + ".VERSION";
|
|
||||||
|
|
||||||
private static final String NO_VERSION_ATTRIBUTE = "NO_VERSION";
|
|
||||||
|
|
||||||
private static final ApiVersionStrategy NO_OP_VERSION_STRATEGY = new NoOpApiVersionStrategy();
|
|
||||||
|
|
||||||
|
|
||||||
private final @Nullable String versionValue;
|
private final @Nullable String versionValue;
|
||||||
|
|
||||||
private final @Nullable Object version;
|
private final @Nullable Object version;
|
||||||
|
|
||||||
private final boolean baselineVersion;
|
private final boolean baselineVersion;
|
||||||
|
|
||||||
private final ApiVersionStrategy versionStrategy;
|
|
||||||
|
|
||||||
private final Set<String> content;
|
private final Set<String> content;
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,14 +57,12 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||||
public VersionRequestCondition(@Nullable String version, @Nullable ApiVersionStrategy strategy) {
|
public VersionRequestCondition(@Nullable String version, @Nullable ApiVersionStrategy strategy) {
|
||||||
if (StringUtils.hasText(version)) {
|
if (StringUtils.hasText(version)) {
|
||||||
Assert.isTrue(strategy != null, "ApiVersionStrategy is required for mapping by version");
|
Assert.isTrue(strategy != null, "ApiVersionStrategy is required for mapping by version");
|
||||||
this.versionStrategy = strategy;
|
|
||||||
this.baselineVersion = version.endsWith("+");
|
this.baselineVersion = version.endsWith("+");
|
||||||
this.versionValue = updateVersion(version, this.baselineVersion);
|
this.versionValue = updateVersion(version, this.baselineVersion);
|
||||||
this.version = this.versionStrategy.parseVersion(this.versionValue);
|
this.version = strategy.parseVersion(this.versionValue);
|
||||||
this.content = Set.of(version);
|
this.content = Set.of(version);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.versionStrategy = (strategy != null ? strategy : NO_OP_VERSION_STRATEGY);
|
|
||||||
this.versionValue = null;
|
this.versionValue = null;
|
||||||
this.version = null;
|
this.version = null;
|
||||||
this.baselineVersion = false;
|
this.baselineVersion = false;
|
||||||
|
@ -111,37 +99,19 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable VersionRequestCondition getMatchingCondition(ServerWebExchange exchange) {
|
public @Nullable VersionRequestCondition getMatchingCondition(ServerWebExchange exchange) {
|
||||||
Comparable<?> requestVersion = exchange.getAttribute(VERSION_ATTRIBUTE_NAME);
|
Comparable<?> requestVersion = exchange.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||||
if (requestVersion == null) {
|
|
||||||
String value = this.versionStrategy.resolveVersion(exchange);
|
|
||||||
requestVersion = (value != null ? parseVersion(value) : this.versionStrategy.getDefaultVersion());
|
|
||||||
this.versionStrategy.validateVersion(requestVersion, exchange);
|
|
||||||
requestVersion = (requestVersion != null ? requestVersion : NO_VERSION_ATTRIBUTE);
|
|
||||||
exchange.getAttributes().put(VERSION_ATTRIBUTE_NAME, requestVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.version == null || requestVersion == NO_VERSION_ATTRIBUTE) {
|
if (this.version == null || requestVersion == null) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, requestVersion);
|
// 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()
|
||||||
// At this stage, match all versions as baseline versions.
|
|
||||||
// Strict matching for fixed versions is enforced at the end in handleMatch.
|
|
||||||
|
|
||||||
int result = compareVersions(this.version, requestVersion);
|
int result = compareVersions(this.version, requestVersion);
|
||||||
return (result <= 0 ? this : null);
|
return (result <= 0 ? this : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Comparable<?> parseVersion(String value) {
|
|
||||||
try {
|
|
||||||
return this.versionStrategy.parseVersion(value);
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
throw new InvalidApiVersionException(value, null, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private <V extends Comparable<V>> int compareVersions(Object v1, Object v2) {
|
private <V extends Comparable<V>> int compareVersions(Object v1, Object v2) {
|
||||||
return ((V) v1).compareTo((V) v2);
|
return ((V) v1).compareTo((V) v2);
|
||||||
|
@ -178,7 +148,7 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||||
*/
|
*/
|
||||||
public void handleMatch(ServerWebExchange exchange) {
|
public void handleMatch(ServerWebExchange exchange) {
|
||||||
if (this.version != null && !this.baselineVersion) {
|
if (this.version != null && !this.baselineVersion) {
|
||||||
Comparable<?> version = exchange.getAttribute(VERSION_ATTRIBUTE_NAME);
|
Comparable<?> version = exchange.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||||
Assert.state(version != null, "No API version attribute");
|
Assert.state(version != null, "No API version attribute");
|
||||||
if (!this.version.equals(version)) {
|
if (!this.version.equals(version)) {
|
||||||
throw new NotAcceptableApiVersionException(version.toString());
|
throw new NotAcceptableApiVersionException(version.toString());
|
||||||
|
@ -186,31 +156,4 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static final class NoOpApiVersionStrategy implements ApiVersionStrategy {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable String resolveVersion(ServerWebExchange exchange) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String parseVersion(String version) {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void validateVersion(@Nullable Comparable<?> requestVersion, ServerWebExchange exchange) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable Comparable<?> getDefaultVersion() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleDeprecations(Comparable<?> version, ServerWebExchange exchange) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import java.util.function.Predicate;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.jspecify.annotations.Nullable;
|
import org.jspecify.annotations.Nullable;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.context.EmbeddedValueResolverAware;
|
import org.springframework.context.EmbeddedValueResolverAware;
|
||||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||||
|
@ -41,6 +42,7 @@ import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.util.StringValueResolver;
|
import org.springframework.util.StringValueResolver;
|
||||||
|
import org.springframework.web.accept.InvalidApiVersionException;
|
||||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
@ -173,6 +175,37 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
|
||||||
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
|
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<HandlerMethod> getHandlerInternal(ServerWebExchange exchange) {
|
||||||
|
if (this.apiVersionStrategy != null) {
|
||||||
|
Comparable<?> requestVersion = exchange.getAttribute(API_VERSION_ATTRIBUTE);
|
||||||
|
if (requestVersion == null) {
|
||||||
|
requestVersion = getApiVersion(exchange, this.apiVersionStrategy);
|
||||||
|
if (requestVersion != null) {
|
||||||
|
exchange.getAttributes().put(API_VERSION_ATTRIBUTE, requestVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.getHandlerInternal(exchange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable Comparable<?> getApiVersion(
|
||||||
|
ServerWebExchange exchange, ApiVersionStrategy versionStrategy) {
|
||||||
|
|
||||||
|
String value = versionStrategy.resolveVersion(exchange);
|
||||||
|
if (value == null) {
|
||||||
|
return versionStrategy.getDefaultVersion();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Comparable<?> version = versionStrategy.parseVersion(value);
|
||||||
|
versionStrategy.validateVersion(version, exchange);
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new InvalidApiVersionException(value, null, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses type-level and method-level {@link RequestMapping @RequestMapping}
|
* Uses type-level and method-level {@link RequestMapping @RequestMapping}
|
||||||
* and {@link HttpExchange @HttpExchange} annotations to create the
|
* and {@link HttpExchange @HttpExchange} annotations to create the
|
||||||
|
|
|
@ -22,6 +22,7 @@ import org.jspecify.annotations.Nullable;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.web.accept.InvalidApiVersionException;
|
import org.springframework.web.accept.InvalidApiVersionException;
|
||||||
|
import org.springframework.web.accept.MissingApiVersionException;
|
||||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||||
|
@ -59,7 +60,8 @@ public class DefaultApiVersionStrategiesTests {
|
||||||
@Test
|
@Test
|
||||||
void validateUnsupportedVersion() {
|
void validateUnsupportedVersion() {
|
||||||
assertThatThrownBy(() -> validateVersion("1.2", apiVersionStrategy()))
|
assertThatThrownBy(() -> validateVersion("1.2", apiVersionStrategy()))
|
||||||
.isInstanceOf(InvalidApiVersionException.class);
|
.isInstanceOf(InvalidApiVersionException.class)
|
||||||
|
.hasMessage("400 BAD_REQUEST \"Invalid API version: '1.2.0'.\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -80,6 +82,14 @@ public class DefaultApiVersionStrategiesTests {
|
||||||
.isInstanceOf(InvalidApiVersionException.class);
|
.isInstanceOf(InvalidApiVersionException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void missingRequiredVersion() {
|
||||||
|
DefaultApiVersionStrategy strategy = apiVersionStrategy();
|
||||||
|
assertThatThrownBy(() -> strategy.validateVersion(null, exchange))
|
||||||
|
.isInstanceOf(MissingApiVersionException.class)
|
||||||
|
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
|
||||||
|
}
|
||||||
|
|
||||||
private static DefaultApiVersionStrategy apiVersionStrategy() {
|
private static DefaultApiVersionStrategy apiVersionStrategy() {
|
||||||
return apiVersionStrategy(null, false);
|
return apiVersionStrategy(null, false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,9 @@ import org.jspecify.annotations.Nullable;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.web.accept.InvalidApiVersionException;
|
|
||||||
import org.springframework.web.accept.MissingApiVersionException;
|
|
||||||
import org.springframework.web.accept.NotAcceptableApiVersionException;
|
import org.springframework.web.accept.NotAcceptableApiVersionException;
|
||||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||||
|
import org.springframework.web.reactive.HandlerMapping;
|
||||||
import org.springframework.web.reactive.accept.DefaultApiVersionStrategy;
|
import org.springframework.web.reactive.accept.DefaultApiVersionStrategy;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||||
|
@ -99,12 +98,6 @@ public class VersionRequestConditionTests {
|
||||||
|
|
||||||
testMatch("v1.1", condition, true, false);
|
testMatch("v1.1", condition, true, false);
|
||||||
testMatch("v1.3", condition, true, false);
|
testMatch("v1.3", condition, true, false);
|
||||||
|
|
||||||
assertThatThrownBy(() -> condition.getMatchingCondition(exchangeWithVersion("1.2")))
|
|
||||||
.isInstanceOf(InvalidApiVersionException.class);
|
|
||||||
|
|
||||||
assertThatThrownBy(() -> condition.getMatchingCondition(MockServerWebExchange.from(MockServerHttpRequest.get("/"))))
|
|
||||||
.isInstanceOf(MissingApiVersionException.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testMatch(
|
private void testMatch(
|
||||||
|
@ -128,12 +121,6 @@ public class VersionRequestConditionTests {
|
||||||
condition.handleMatch(exchange);
|
condition.handleMatch(exchange);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void missingRequiredVersion() {
|
|
||||||
assertThatThrownBy(() -> condition("1.2").getMatchingCondition(exchange()))
|
|
||||||
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void defaultVersion() {
|
void defaultVersion() {
|
||||||
String version = "1.2";
|
String version = "1.2";
|
||||||
|
@ -144,12 +131,6 @@ public class VersionRequestConditionTests {
|
||||||
assertThat(match).isSameAs(condition);
|
assertThat(match).isSameAs(condition);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void unsupportedVersion() {
|
|
||||||
assertThatThrownBy(() -> condition("1.2").getMatchingCondition(exchangeWithVersion("1.3")))
|
|
||||||
.hasMessage("400 BAD_REQUEST \"Invalid API version: '1.3.0'.\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void compare() {
|
void compare() {
|
||||||
testCompare("1.1", "1", "1.1");
|
testCompare("1.1", "1", "1.1");
|
||||||
|
@ -181,8 +162,10 @@ public class VersionRequestConditionTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServerWebExchange exchangeWithVersion(String v) {
|
private ServerWebExchange exchangeWithVersion(String v) {
|
||||||
return MockServerWebExchange.from(
|
Comparable<?> version = this.strategy.parseVersion(v);
|
||||||
MockServerHttpRequest.get("/path").queryParam("api-version", v));
|
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path"));
|
||||||
|
exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, version);
|
||||||
|
return exchange;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,13 +21,11 @@ import java.util.Collections;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.jspecify.annotations.Nullable;
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.accept.ApiVersionStrategy;
|
import org.springframework.web.accept.ApiVersionStrategy;
|
||||||
import org.springframework.web.accept.InvalidApiVersionException;
|
|
||||||
import org.springframework.web.accept.NotAcceptableApiVersionException;
|
import org.springframework.web.accept.NotAcceptableApiVersionException;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.servlet.HandlerMapping;
|
import org.springframework.web.servlet.HandlerMapping;
|
||||||
|
@ -42,21 +40,12 @@ import org.springframework.web.servlet.HandlerMapping;
|
||||||
*/
|
*/
|
||||||
public final class VersionRequestCondition extends AbstractRequestCondition<VersionRequestCondition> {
|
public final class VersionRequestCondition extends AbstractRequestCondition<VersionRequestCondition> {
|
||||||
|
|
||||||
private static final String VERSION_ATTRIBUTE_NAME = VersionRequestCondition.class.getName() + ".VERSION";
|
|
||||||
|
|
||||||
private static final String NO_VERSION_ATTRIBUTE = "NO_VERSION";
|
|
||||||
|
|
||||||
private static final ApiVersionStrategy NO_OP_VERSION_STRATEGY = new NoOpApiVersionStrategy();
|
|
||||||
|
|
||||||
|
|
||||||
private final @Nullable String versionValue;
|
private final @Nullable String versionValue;
|
||||||
|
|
||||||
private final @Nullable Comparable<?> version;
|
private final @Nullable Comparable<?> version;
|
||||||
|
|
||||||
private final boolean baselineVersion;
|
private final boolean baselineVersion;
|
||||||
|
|
||||||
private final ApiVersionStrategy versionStrategy;
|
|
||||||
|
|
||||||
private final Set<String> content;
|
private final Set<String> content;
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,14 +56,12 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||||
public VersionRequestCondition(@Nullable String version, @Nullable ApiVersionStrategy strategy) {
|
public VersionRequestCondition(@Nullable String version, @Nullable ApiVersionStrategy strategy) {
|
||||||
if (StringUtils.hasText(version)) {
|
if (StringUtils.hasText(version)) {
|
||||||
Assert.isTrue(strategy != null, "ApiVersionStrategy is required for mapping by version");
|
Assert.isTrue(strategy != null, "ApiVersionStrategy is required for mapping by version");
|
||||||
this.versionStrategy = strategy;
|
|
||||||
this.baselineVersion = version.endsWith("+");
|
this.baselineVersion = version.endsWith("+");
|
||||||
this.versionValue = updateVersion(version, this.baselineVersion);
|
this.versionValue = updateVersion(version, this.baselineVersion);
|
||||||
this.version = this.versionStrategy.parseVersion(this.versionValue);
|
this.version = strategy.parseVersion(this.versionValue);
|
||||||
this.content = Set.of(version);
|
this.content = Set.of(version);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.versionStrategy = (strategy != null ? strategy : NO_OP_VERSION_STRATEGY);
|
|
||||||
this.versionValue = null;
|
this.versionValue = null;
|
||||||
this.version = null;
|
this.version = null;
|
||||||
this.baselineVersion = false;
|
this.baselineVersion = false;
|
||||||
|
@ -111,37 +98,19 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable VersionRequestCondition getMatchingCondition(HttpServletRequest request) {
|
public @Nullable VersionRequestCondition getMatchingCondition(HttpServletRequest request) {
|
||||||
Comparable<?> requestVersion = (Comparable<?>) request.getAttribute(VERSION_ATTRIBUTE_NAME);
|
Comparable<?> requestVersion = (Comparable<?>) request.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||||
if (requestVersion == null) {
|
|
||||||
String value = this.versionStrategy.resolveVersion(request);
|
|
||||||
requestVersion = (value != null ? parseVersion(value) : this.versionStrategy.getDefaultVersion());
|
|
||||||
this.versionStrategy.validateVersion(requestVersion, request);
|
|
||||||
requestVersion = (requestVersion != null ? requestVersion : NO_VERSION_ATTRIBUTE);
|
|
||||||
request.setAttribute(VERSION_ATTRIBUTE_NAME, requestVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.version == null || requestVersion == NO_VERSION_ATTRIBUTE) {
|
if (this.version == null || requestVersion == null) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
request.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, requestVersion);
|
// 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()
|
||||||
// At this stage, match all versions as baseline versions.
|
|
||||||
// Strict matching for fixed versions is enforced at the end in handleMatch.
|
|
||||||
|
|
||||||
int result = compareVersions(this.version, requestVersion);
|
int result = compareVersions(this.version, requestVersion);
|
||||||
return (result <= 0 ? this : null);
|
return (result <= 0 ? this : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Comparable<?> parseVersion(String value) {
|
|
||||||
try {
|
|
||||||
return this.versionStrategy.parseVersion(value);
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
throw new InvalidApiVersionException(value, null, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private <V extends Comparable<V>> int compareVersions(Object v1, Object v2) {
|
private <V extends Comparable<V>> int compareVersions(Object v1, Object v2) {
|
||||||
return ((V) v1).compareTo((V) v2);
|
return ((V) v1).compareTo((V) v2);
|
||||||
|
@ -178,7 +147,7 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||||
*/
|
*/
|
||||||
public void handleMatch(HttpServletRequest request) {
|
public void handleMatch(HttpServletRequest request) {
|
||||||
if (this.version != null && !this.baselineVersion) {
|
if (this.version != null && !this.baselineVersion) {
|
||||||
Comparable<?> version = (Comparable<?>) request.getAttribute(VERSION_ATTRIBUTE_NAME);
|
Comparable<?> version = (Comparable<?>) request.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE);
|
||||||
Assert.state(version != null, "No API version attribute");
|
Assert.state(version != null, "No API version attribute");
|
||||||
if (!this.version.equals(version)) {
|
if (!this.version.equals(version)) {
|
||||||
throw new NotAcceptableApiVersionException(version.toString());
|
throw new NotAcceptableApiVersionException(version.toString());
|
||||||
|
@ -186,31 +155,4 @@ public final class VersionRequestCondition extends AbstractRequestCondition<Vers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static final class NoOpApiVersionStrategy implements ApiVersionStrategy {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable String resolveVersion(HttpServletRequest request) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String parseVersion(String version) {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void validateVersion(@Nullable Comparable<?> requestVersion, HttpServletRequest request) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable Comparable<?> getDefaultVersion() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleDeprecations(Comparable<?> version, HttpServletRequest request, HttpServletResponse response) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ import org.springframework.util.StringValueResolver;
|
||||||
import org.springframework.web.accept.ApiVersionStrategy;
|
import org.springframework.web.accept.ApiVersionStrategy;
|
||||||
import org.springframework.web.accept.ContentNegotiationManager;
|
import org.springframework.web.accept.ContentNegotiationManager;
|
||||||
import org.springframework.web.accept.DefaultApiVersionStrategy;
|
import org.springframework.web.accept.DefaultApiVersionStrategy;
|
||||||
|
import org.springframework.web.accept.InvalidApiVersionException;
|
||||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
@ -200,6 +201,38 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
|
||||||
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
|
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected @Nullable HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
|
||||||
|
if (this.apiVersionStrategy != null) {
|
||||||
|
Comparable<?> requestVersion = (Comparable<?>) request.getAttribute(API_VERSION_ATTRIBUTE);
|
||||||
|
if (requestVersion == null) {
|
||||||
|
requestVersion = getApiVersion(request, this.apiVersionStrategy);
|
||||||
|
if (requestVersion != null) {
|
||||||
|
request.setAttribute(API_VERSION_ATTRIBUTE, requestVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.getHandlerInternal(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable Comparable<?> getApiVersion(
|
||||||
|
HttpServletRequest request, ApiVersionStrategy versionStrategy) {
|
||||||
|
|
||||||
|
String value = versionStrategy.resolveVersion(request);
|
||||||
|
if (value == null) {
|
||||||
|
return versionStrategy.getDefaultVersion();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Comparable<?> version = versionStrategy.parseVersion(value);
|
||||||
|
versionStrategy.validateVersion(version, request);
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new InvalidApiVersionException(value, null, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses type-level and method-level {@link RequestMapping @RequestMapping}
|
* Uses type-level and method-level {@link RequestMapping @RequestMapping}
|
||||||
* and {@link HttpExchange @HttpExchange} annotations to create the
|
* and {@link HttpExchange @HttpExchange} annotations to create the
|
||||||
|
|
|
@ -24,10 +24,9 @@ import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.web.accept.DefaultApiVersionStrategy;
|
import org.springframework.web.accept.DefaultApiVersionStrategy;
|
||||||
import org.springframework.web.accept.InvalidApiVersionException;
|
|
||||||
import org.springframework.web.accept.MissingApiVersionException;
|
|
||||||
import org.springframework.web.accept.NotAcceptableApiVersionException;
|
import org.springframework.web.accept.NotAcceptableApiVersionException;
|
||||||
import org.springframework.web.accept.SemanticApiVersionParser;
|
import org.springframework.web.accept.SemanticApiVersionParser;
|
||||||
|
import org.springframework.web.servlet.HandlerMapping;
|
||||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
@ -97,12 +96,6 @@ public class VersionRequestConditionTests {
|
||||||
|
|
||||||
testMatch("v1.1", condition, true, false);
|
testMatch("v1.1", condition, true, false);
|
||||||
testMatch("v1.3", condition, true, false);
|
testMatch("v1.3", condition, true, false);
|
||||||
|
|
||||||
assertThatThrownBy(() -> condition.getMatchingCondition(requestWithVersion("1.2")))
|
|
||||||
.isInstanceOf(InvalidApiVersionException.class);
|
|
||||||
|
|
||||||
assertThatThrownBy(() -> condition.getMatchingCondition(new MockHttpServletRequest("GET", "/path")))
|
|
||||||
.isInstanceOf(MissingApiVersionException.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testMatch(
|
private void testMatch(
|
||||||
|
@ -126,12 +119,6 @@ public class VersionRequestConditionTests {
|
||||||
condition.handleMatch(request);
|
condition.handleMatch(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void missingRequiredVersion() {
|
|
||||||
assertThatThrownBy(() -> condition("1.2").getMatchingCondition(new MockHttpServletRequest("GET", "/path")))
|
|
||||||
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void defaultVersion() {
|
void defaultVersion() {
|
||||||
String version = "1.2";
|
String version = "1.2";
|
||||||
|
@ -142,12 +129,6 @@ public class VersionRequestConditionTests {
|
||||||
assertThat(match).isSameAs(condition);
|
assertThat(match).isSameAs(condition);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void unsupportedVersion() {
|
|
||||||
assertThatThrownBy(() -> condition("1.2").getMatchingCondition(requestWithVersion("1.3")))
|
|
||||||
.hasMessage("400 BAD_REQUEST \"Invalid API version: '1.3.0'.\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void compare() {
|
void compare() {
|
||||||
testCompare("1.1", "1", "1.1");
|
testCompare("1.1", "1", "1.1");
|
||||||
|
@ -176,7 +157,7 @@ public class VersionRequestConditionTests {
|
||||||
|
|
||||||
private MockHttpServletRequest requestWithVersion(String v) {
|
private MockHttpServletRequest requestWithVersion(String v) {
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path");
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path");
|
||||||
request.addParameter("api-version", v);
|
request.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, this.strategy.parseVersion(v));
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue