diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/client/samples/ApiVersionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/client/samples/ApiVersionTests.java index c23d60b8c8b..70af17d22e0 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/client/samples/ApiVersionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/client/samples/ApiVersionTests.java @@ -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)) diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/ApiVersionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/ApiVersionTests.java index 25637ed8b00..be52b6634df 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/ApiVersionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/ApiVersionTests.java @@ -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) diff --git a/spring-web/src/main/java/org/springframework/web/accept/DefaultApiVersionStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/DefaultApiVersionStrategy.java index a85dff6ed3c..b9c41fb31bb 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/DefaultApiVersionStrategy.java +++ b/spring-web/src/main/java/org/springframework/web/accept/DefaultApiVersionStrategy.java @@ -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> detectedVersions = new TreeSet<>(); + private final Predicate> supportedVersionPredicate; + private final @Nullable ApiVersionDeprecationHandler deprecationHandler; @@ -71,7 +74,8 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy { */ public DefaultApiVersionStrategy( List versionResolvers, ApiVersionParser versionParser, - boolean versionRequired, @Nullable String defaultVersion, boolean detectSupportedVersions, + boolean versionRequired, @Nullable String defaultVersion, + boolean detectSupportedVersions, @Nullable Predicate> 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> initSupportedVersionPredicate(@Nullable Predicate> 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) { diff --git a/spring-web/src/test/java/org/springframework/web/accept/DefaultApiVersionStrategiesTests.java b/spring-web/src/test/java/org/springframework/web/accept/DefaultApiVersionStrategiesTests.java index b35282bee92..222798a3bc8 100644 --- a/spring-web/src/test/java/org/springframework/web/accept/DefaultApiVersionStrategiesTests.java +++ b/spring-web/src/test/java/org/springframework/web/accept/DefaultApiVersionStrategiesTests.java @@ -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> 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); } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java index 08816c00329..114f0c95dd5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java @@ -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> detectedVersions = new TreeSet<>(); + private final Predicate> supportedVersionPredicate; + private final @Nullable ApiVersionDeprecationHandler deprecationHandler; @@ -73,7 +76,8 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy { */ public DefaultApiVersionStrategy( List versionResolvers, ApiVersionParser versionParser, - boolean versionRequired, @Nullable String defaultVersion, boolean detectSupportedVersions, + boolean versionRequired, @Nullable String defaultVersion, + boolean detectSupportedVersions, @Nullable Predicate> 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> initSupportedVersionPredicate(@Nullable Predicate> 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) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java index 0392f4dd878..2fe21fc5393 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java @@ -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> 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> 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); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java index b9e21028c9d..367587dd50c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java @@ -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> 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); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java index 7f73b5e121a..ec779c637f9 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java @@ -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); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/VersionRequestConditionTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/VersionRequestConditionTests.java index 06940692aa1..b96de727c5a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/VersionRequestConditionTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/VersionRequestConditionTests.java @@ -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 diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java index e3e3b1c60a1..e08e1708163 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java @@ -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> 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> 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); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicatesTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicatesTests.java index 508ca3b46ab..55f9eb0d22d 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicatesTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicatesTests.java @@ -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, diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/VersionRequestConditionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/VersionRequestConditionTests.java index 5ee4ddff5c3..47b41ba4563 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/VersionRequestConditionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/VersionRequestConditionTests.java @@ -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