Add supportedVersionPredicate to ApiVersionConfigurer

Closes gh-35267
This commit is contained in:
rstoyanchev
2025-08-01 12:35:18 +01:00
parent 87838aa4c5
commit 2b1a815167
12 changed files with 117 additions and 48 deletions
@@ -76,7 +76,7 @@ public class ApiVersionTests {
DefaultApiVersionStrategy versionStrategy = new DefaultApiVersionStrategy(
List.of(versionResolver), new SemanticApiVersionParser(),
true, null, true, null);
true, null, true, null, null);
RestTestClient client = RestTestClient.bindToController(new TestController())
.configureServer(mockMvcBuilder -> mockMvcBuilder.setApiVersionStrategy(versionStrategy))
@@ -50,7 +50,7 @@ public class ApiVersionTests {
DefaultApiVersionStrategy versionStrategy = new DefaultApiVersionStrategy(
List.of(request -> request.getHeader(header)), new SemanticApiVersionParser(),
true, null, true, null);
true, null, true, null, null);
MockMvc mockMvc = standaloneSetup(new PersonController())
.setApiVersionStrategy(versionStrategy)
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -50,6 +51,8 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
private final Set<Comparable<?>> detectedVersions = new TreeSet<>();
private final Predicate<Comparable<?>> supportedVersionPredicate;
private final @Nullable ApiVersionDeprecationHandler deprecationHandler;
@@ -71,7 +74,8 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
*/
public DefaultApiVersionStrategy(
List<ApiVersionResolver> versionResolvers, ApiVersionParser<?> versionParser,
boolean versionRequired, @Nullable String defaultVersion, boolean detectSupportedVersions,
boolean versionRequired, @Nullable String defaultVersion,
boolean detectSupportedVersions, @Nullable Predicate<Comparable<?>> supportedVersionPredicate,
@Nullable ApiVersionDeprecationHandler deprecationHandler) {
Assert.notEmpty(versionResolvers, "At least one ApiVersionResolver is required");
@@ -82,9 +86,16 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
this.versionRequired = (versionRequired && defaultVersion == null);
this.defaultVersion = (defaultVersion != null ? versionParser.parseVersion(defaultVersion) : null);
this.detectSupportedVersions = detectSupportedVersions;
this.supportedVersionPredicate = initSupportedVersionPredicate(supportedVersionPredicate);
this.deprecationHandler = deprecationHandler;
}
private Predicate<Comparable<?>> initSupportedVersionPredicate(@Nullable Predicate<Comparable<?>> predicate) {
return (predicate != null ? predicate :
(version -> (this.supportedVersions.contains(version) ||
this.detectSupportedVersions && this.detectedVersions.contains(version))));
}
@Override
public @Nullable Comparable<?> getDefaultVersion() {
@@ -160,16 +171,11 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
return;
}
if (!isSupportedVersion(requestVersion)) {
if (!this.supportedVersionPredicate.test(requestVersion)) {
throw new InvalidApiVersionException(requestVersion.toString());
}
}
private boolean isSupportedVersion(Comparable<?> requestVersion) {
return (this.supportedVersions.contains(requestVersion) ||
this.detectSupportedVersions && this.detectedVersions.contains(requestVersion));
}
@Override
public void handleDeprecations(Comparable<?> version, HttpServletRequest request, HttpServletResponse response) {
if (this.deprecationHandler != null) {
@@ -17,6 +17,7 @@
package org.springframework.web.accept;
import java.util.List;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
@@ -44,6 +45,13 @@ public class DefaultApiVersionStrategiesTests {
assertThat(strategy.getDefaultVersion()).isEqualTo(parser.parseVersion(version));
}
@Test
void missingRequiredVersion() {
assertThatThrownBy(() -> validateVersion(null, apiVersionStrategy()))
.isInstanceOf(MissingApiVersionException.class)
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
}
@Test
void validateSupportedVersion() {
String version = "1.2";
@@ -53,7 +61,7 @@ public class DefaultApiVersionStrategiesTests {
}
@Test
void rejectUnsupportedVersion() {
void validateUnsupportedVersion() {
assertThatThrownBy(() -> validateVersion("1.2", apiVersionStrategy()))
.isInstanceOf(InvalidApiVersionException.class)
.hasMessage("400 BAD_REQUEST \"Invalid API version: '1.2.0'.\"");
@@ -62,7 +70,7 @@ public class DefaultApiVersionStrategiesTests {
@Test
void validateDetectedVersion() {
String version = "1.2";
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true);
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true, null);
strategy.addMappedVersion(version);
validateVersion(version, strategy);
}
@@ -76,30 +84,37 @@ public class DefaultApiVersionStrategiesTests {
}
@Test
void missingRequiredVersion() {
assertThatThrownBy(() -> validateVersion(null, apiVersionStrategy()))
.isInstanceOf(MissingApiVersionException.class)
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
void validateSupportedWithPredicate() {
SemanticApiVersionParser.Version parsedVersion = parser.parseVersion("1.2");
validateVersion("1.2", apiVersionStrategy(null, false, version -> version.equals(parsedVersion)));
}
@Test
void validateUnsupportedWithPredicate() {
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, false, version -> version.equals("1.2"));
assertThatThrownBy(() -> validateVersion("1.2", strategy)).isInstanceOf(InvalidApiVersionException.class);
}
private static DefaultApiVersionStrategy apiVersionStrategy() {
return apiVersionStrategy(null, false);
return apiVersionStrategy(null, false, null);
}
private static DefaultApiVersionStrategy apiVersionStrategy(@Nullable String defaultVersion) {
return apiVersionStrategy(defaultVersion, false);
return apiVersionStrategy(defaultVersion, false, null);
}
private static DefaultApiVersionStrategy apiVersionStrategy(
@Nullable String defaultVersion, boolean detectSupportedVersions) {
@Nullable String defaultVersion, boolean detectSupportedVersions,
@Nullable Predicate<Comparable<?>> supportedVersionPredicate) {
return new DefaultApiVersionStrategy(
List.of(request -> request.getParameter("api-version")),
new SemanticApiVersionParser(), true, defaultVersion, detectSupportedVersions, null);
List.of(request -> request.getParameter("api-version")), new SemanticApiVersionParser(),
true, defaultVersion, detectSupportedVersions, supportedVersionPredicate, null);
}
private void validateVersion(@Nullable String version, DefaultApiVersionStrategy strategy) {
strategy.validateVersion(version != null ? parser.parseVersion(version) : null, request);
Comparable<?> parsedVersion = (version != null ? parser.parseVersion(version) : null);
strategy.validateVersion(parsedVersion, request);
}
}
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
@@ -52,6 +53,8 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
private final Set<Comparable<?>> detectedVersions = new TreeSet<>();
private final Predicate<Comparable<?>> supportedVersionPredicate;
private final @Nullable ApiVersionDeprecationHandler deprecationHandler;
@@ -73,7 +76,8 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
*/
public DefaultApiVersionStrategy(
List<ApiVersionResolver> versionResolvers, ApiVersionParser<?> versionParser,
boolean versionRequired, @Nullable String defaultVersion, boolean detectSupportedVersions,
boolean versionRequired, @Nullable String defaultVersion,
boolean detectSupportedVersions, @Nullable Predicate<Comparable<?>> supportedVersionPredicate,
@Nullable ApiVersionDeprecationHandler deprecationHandler) {
Assert.notEmpty(versionResolvers, "At least one ApiVersionResolver is required");
@@ -84,9 +88,16 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
this.versionRequired = (versionRequired && defaultVersion == null);
this.defaultVersion = (defaultVersion != null ? versionParser.parseVersion(defaultVersion) : null);
this.detectSupportedVersions = detectSupportedVersions;
this.supportedVersionPredicate = initSupportedVersionPredicate(supportedVersionPredicate);
this.deprecationHandler = deprecationHandler;
}
private Predicate<Comparable<?>> initSupportedVersionPredicate(@Nullable Predicate<Comparable<?>> predicate) {
return (predicate != null ? predicate :
(version -> (this.supportedVersions.contains(version) ||
this.detectSupportedVersions && this.detectedVersions.contains(version))));
}
@Override
public @Nullable Comparable<?> getDefaultVersion() {
@@ -111,7 +122,7 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
* considered supported, and use of this method is optional. However, if you
* prefer to use only explicitly configured, supported versions, then set
* {@code detectSupportedVersions} flag to {@code false}.
* @param versions the supported versions to add
* @param versions the supported versions to add
* @see #addMappedVersion(String...)
*/
public void addSupportedVersion(String... versions) {
@@ -161,16 +172,11 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
return;
}
if (!isSupportedVersion(requestVersion)) {
if (!this.supportedVersionPredicate.test(requestVersion)) {
throw new InvalidApiVersionException(requestVersion.toString());
}
}
private boolean isSupportedVersion(Comparable<?> requestVersion) {
return (this.supportedVersions.contains(requestVersion) ||
this.detectSupportedVersions && this.detectedVersions.contains(requestVersion));
}
@Override
public void handleDeprecations(Comparable<?> version, ServerWebExchange exchange) {
if (this.deprecationHandler != null) {
@@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
@@ -58,6 +59,8 @@ public class ApiVersionConfigurer {
private boolean detectSupportedVersions = true;
private @Nullable Predicate<Comparable<?>> supportedVersionPredicate;
private @Nullable ApiVersionDeprecationHandler deprecationHandler;
@@ -178,6 +181,16 @@ public class ApiVersionConfigurer {
return this;
}
/**
* Provide a {@link Predicate} to perform supported version checks with, in
* effect taking over the supported version check and superseding the
* {@link #addSupportedVersions} and {@link #detectSupportedVersions}.
* @param predicate the predicate to use
*/
public void setSupportedVersionPredicate(@Nullable Predicate<Comparable<?>> predicate) {
this.supportedVersionPredicate = predicate;
}
/**
* Configure a handler to add handling for requests with a deprecated API
* version. Typically, this involves sending hints and information about
@@ -199,8 +212,9 @@ public class ApiVersionConfigurer {
DefaultApiVersionStrategy strategy = new DefaultApiVersionStrategy(this.versionResolvers,
(this.versionParser != null ? this.versionParser : new SemanticApiVersionParser()),
(this.versionRequired != null ? this.versionRequired : true),
this.defaultVersion, this.detectSupportedVersions, this.deprecationHandler);
(this.versionRequired != null ? this.versionRequired : true), this.defaultVersion,
this.detectSupportedVersions, this.supportedVersionPredicate,
this.deprecationHandler);
this.supportedVersions.forEach(strategy::addSupportedVersion);
@@ -17,6 +17,7 @@
package org.springframework.web.reactive.accept;
import java.util.List;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
@@ -45,10 +46,17 @@ public class DefaultApiVersionStrategiesTests {
@Test
void defaultVersionIsParsed() {
String version = "1.2.3";
ApiVersionStrategy strategy = apiVersionStrategy(version, false);
ApiVersionStrategy strategy = apiVersionStrategy(version, false, null);
assertThat(strategy.getDefaultVersion()).isEqualTo(parser.parseVersion(version));
}
@Test
void missingRequiredVersion() {
assertThatThrownBy(() -> validateVersion(null, apiVersionStrategy()))
.isInstanceOf(MissingApiVersionException.class)
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
}
@Test
void validateSupportedVersion() {
String version = "1.2";
@@ -58,7 +66,7 @@ public class DefaultApiVersionStrategiesTests {
}
@Test
void rejectUnsupportedVersion() {
void validateUnsupportedVersion() {
assertThatThrownBy(() -> validateVersion("1.2", apiVersionStrategy()))
.isInstanceOf(InvalidApiVersionException.class)
.hasMessage("400 BAD_REQUEST \"Invalid API version: '1.2.0'.\"");
@@ -67,7 +75,7 @@ public class DefaultApiVersionStrategiesTests {
@Test
void validateDetectedVersion() {
String version = "1.2";
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true);
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true, null);
strategy.addMappedVersion(version);
validateVersion(version, strategy);
}
@@ -81,26 +89,33 @@ public class DefaultApiVersionStrategiesTests {
}
@Test
void missingRequiredVersion() {
assertThatThrownBy(() -> validateVersion(null, apiVersionStrategy()))
.isInstanceOf(MissingApiVersionException.class)
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
void validateSupportedWithPredicate() {
SemanticApiVersionParser.Version parsedVersion = parser.parseVersion("1.2");
validateVersion("1.2", apiVersionStrategy(null, false, version -> version.equals(parsedVersion)));
}
@Test
void validateUnsupportedWithPredicate() {
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, false, version -> version.equals("1.2"));
assertThatThrownBy(() -> validateVersion("1.2", strategy)).isInstanceOf(InvalidApiVersionException.class);
}
private static DefaultApiVersionStrategy apiVersionStrategy() {
return apiVersionStrategy(null, false);
return apiVersionStrategy(null, false, null);
}
private static DefaultApiVersionStrategy apiVersionStrategy(
@Nullable String defaultVersion, boolean detectSupportedVersions) {
@Nullable String defaultVersion, boolean detectSupportedVersions,
@Nullable Predicate<Comparable<?>> supportedVersionPredicate) {
return new DefaultApiVersionStrategy(
List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")),
parser, true, defaultVersion, detectSupportedVersions, null);
parser, true, defaultVersion, detectSupportedVersions, supportedVersionPredicate, null);
}
private void validateVersion(@Nullable String version, DefaultApiVersionStrategy strategy) {
strategy.validateVersion(version != null ? parser.parseVersion(version) : null, exchange);
Comparable<?> parsedVersion = (version != null ? parser.parseVersion(version) : null);
strategy.validateVersion(parsedVersion, exchange);
}
}
@@ -380,7 +380,7 @@ class RequestPredicatesTests {
private static DefaultApiVersionStrategy apiVersionStrategy() {
return new DefaultApiVersionStrategy(
List.of(exchange -> null), new SemanticApiVersionParser(), true, null, false, null);
List.of(exchange -> null), new SemanticApiVersionParser(), true, null, false, null, null);
}
}
@@ -52,7 +52,7 @@ public class VersionRequestConditionTests {
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultVersion) {
return new DefaultApiVersionStrategy(
List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")),
new SemanticApiVersionParser(), true, defaultVersion, false, null);
new SemanticApiVersionParser(), true, defaultVersion, false, null, null);
}
@Test
@@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
@@ -59,6 +60,8 @@ public class ApiVersionConfigurer {
private boolean detectSupportedVersions = true;
private @Nullable Predicate<Comparable<?>> supportedVersionPredicate;
private @Nullable ApiVersionDeprecationHandler deprecationHandler;
@@ -179,6 +182,16 @@ public class ApiVersionConfigurer {
return this;
}
/**
* Provide a {@link Predicate} to perform supported version checks with, in
* effect taking over the supported version check and superseding the
* {@link #addSupportedVersions} and {@link #detectSupportedVersions}.
* @param predicate the predicate to use
*/
public void setSupportedVersionPredicate(@Nullable Predicate<Comparable<?>> predicate) {
this.supportedVersionPredicate = predicate;
}
/**
* Configure a handler to add handling for requests with a deprecated API
* version. Typically, this involves sending hints and information about
@@ -200,8 +213,8 @@ public class ApiVersionConfigurer {
DefaultApiVersionStrategy strategy = new DefaultApiVersionStrategy(this.versionResolvers,
(this.versionParser != null ? this.versionParser : new SemanticApiVersionParser()),
(this.versionRequired != null ? this.versionRequired : true),
this.defaultVersion, this.detectSupportedVersions,
(this.versionRequired != null ? this.versionRequired : true), this.defaultVersion,
this.detectSupportedVersions, this.supportedVersionPredicate,
this.deprecationHandler);
this.supportedVersions.forEach(strategy::addSupportedVersion);
@@ -280,7 +280,7 @@ class RequestPredicatesTests {
private static ServerRequest serverRequest(String version) {
ApiVersionStrategy strategy = new DefaultApiVersionStrategy(
List.of(exchange -> null), new SemanticApiVersionParser(), true, null, false, null);
List.of(exchange -> null), new SemanticApiVersionParser(), true, null, false, null, null);
MockHttpServletRequest servletRequest =
PathPatternsTestUtils.initRequest("GET", null, "/path", true,
@@ -50,7 +50,7 @@ public class VersionRequestConditionTests {
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultVersion) {
return new DefaultApiVersionStrategy(
List.of(request -> request.getParameter("api-version")),
new SemanticApiVersionParser(), true, defaultVersion, false, null);
new SemanticApiVersionParser(), true, defaultVersion, false, null, null);
}
@Test