diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java index 3e28bf585a2..084198518b9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionResolver.java @@ -34,20 +34,21 @@ interface ApiVersionResolver { /** * Resolve the version for the given exchange. - * @param exchange the current exchange - * @return the version value, or {@code null} if not found - */ - @Nullable String resolveVersion(ServerWebExchange exchange); - - /** - * Asynchronously resolve the version for the given request exchange. * This method wraps the synchronous {@code resolveVersion} method * and provides a reactive alternative. - * @param exchange the current request exchange - * @return a {@code Mono} emitting the version value, or an empty {@code Mono} if no version is found + * @param exchange the current exchange + * @return {@code Mono} emitting the version value, or an empty {@code Mono} + * @since 7.0.3 */ default Mono resolveVersionAsync(ServerWebExchange exchange){ return Mono.justOrEmpty(this.resolveVersion(exchange)); } + /** + * Resolve the version for the given exchange. + * @param exchange the current exchange + * @return the version value, or {@code null} if not found + */ + @Nullable String resolveVersion(ServerWebExchange exchange); + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java index a9f66846660..38ce07b6def 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java @@ -34,29 +34,29 @@ import org.springframework.web.server.ServerWebExchange; */ public interface ApiVersionStrategy { + /** + * Resolve the version value from a request. + * @param exchange the current exchange + * @return a {@code Mono} emitting the raw version as a {@code String}, + * or an empty {@code Mono} if no version is found + * @since 7.0.3 + * @see ApiVersionResolver + */ + default Mono resolveVersionAsync(ServerWebExchange exchange) { + return Mono.justOrEmpty(resolveVersion(exchange)); + } + /** * Resolve the version value from a request, e.g. from a request header. * @param exchange the current exchange * @return the version, if present or {@code null} * @see ApiVersionResolver - * @deprecated as of 7.0.3, in favor of - * {@link #resolveVersionAsync(ServerWebExchange)} + * @deprecated as of 7.0.3, in favor of {@link #resolveVersionAsync(ServerWebExchange)} */ - @Deprecated(forRemoval = true, since = "7.0.3") + @Deprecated(since = "7.0.3", forRemoval = true) @Nullable String resolveVersion(ServerWebExchange exchange); - - /** - * Resolve the version value from a request asynchronously. - * @param exchange the current server exchange containing the request details - * @return a {@code Mono} emitting the resolved version as a {@code String}, - * or an empty {@code Mono} if no version is resolved - */ - default Mono resolveVersionAsync(ServerWebExchange exchange) { - return Mono.justOrEmpty(this.resolveVersion(exchange)); - } - /** * Parse the version of a request into an Object. * @param version the value to parse @@ -75,6 +75,36 @@ public interface ApiVersionStrategy { void validateVersion(@Nullable Comparable requestVersion, ServerWebExchange exchange) throws MissingApiVersionException, InvalidApiVersionException; + /** + * Return a default version to use for requests that don't specify one. + */ + @Nullable Comparable getDefaultVersion(); + + /** + * Convenience method to resolve, parse, and validate the request version, + * or return the default version if configured. + * @param exchange the current exchange + * @return a {@code Mono} emitting the request version, a validation error, + * or an empty {@code Mono} if there is no version + * @since 7.0.3 + */ + @SuppressWarnings("Convert2MethodRef") + default Mono> resolveParseAndValidate(ServerWebExchange exchange) { + return this.resolveVersionAsync(exchange) + .switchIfEmpty(Mono.justOrEmpty(this.getDefaultVersion()) + .mapNotNull(comparable -> comparable.toString())) + .>handle((version, sink) -> { + try { + sink.next(this.parseVersion(version)); + } + catch (Exception ex) { + sink.error(new InvalidApiVersionException(version, null, ex)); + } + }) + .flatMap(version -> this.validateVersionAsync(version, exchange)) + .switchIfEmpty(this.validateVersionAsync(null, exchange)); + } + /** * Validates the provided request version against the requirements and constraints * of the API. If the validation succeeds, the version is returned, otherwise an @@ -94,19 +124,14 @@ public interface ApiVersionStrategy { } } - /** - * Return a default version to use for requests that don't specify one. - */ - @Nullable Comparable getDefaultVersion(); - /** * Convenience method to return the parsed and validated request version, * or the default version if configured. * @param exchange the current exchange * @return the parsed request version, or the default version + * @deprecated in favor of {@link #resolveParseAndValidate(ServerWebExchange)} */ - @SuppressWarnings({"DeprecatedIsStillUsed", "DuplicatedCode"}) - @Deprecated(forRemoval = true, since = "7.0.3") + @Deprecated(since = "7.0.3", forRemoval = true) default @Nullable Comparable resolveParseAndValidateVersion(ServerWebExchange exchange) { String value = resolveVersion(exchange); Comparable version; @@ -125,32 +150,6 @@ public interface ApiVersionStrategy { return version; } - /** - * Convenience method to return the API version from the given request exchange, parse and validate - * the version, and return the result as a reactive {@code Mono} stream. If no version - * is resolved, the default version is used. - * @param exchange the current server exchange containing the request details - * @return a {@code Mono} emitting the resolved, parsed, and validated version as a {@code Comparable}, - * or an error in case parsing or validation fails - */ - @SuppressWarnings("Convert2MethodRef") - default Mono> resolveParseAndValidateVersionAsync(ServerWebExchange exchange) { - return this.resolveVersionAsync(exchange) - .switchIfEmpty(Mono.justOrEmpty(this.getDefaultVersion()) - .mapNotNull(comparable -> comparable.toString())) - .>handle((version, sink) -> { - try { - sink.next(this.parseVersion(version)); - } - catch (Exception ex) { - sink.error(new InvalidApiVersionException(version, null, ex)); - } - }) - .flatMap(version -> this.validateVersionAsync(version, exchange)) - .switchIfEmpty(this.validateVersionAsync(null, exchange)); - - } - /** * Check if the requested API version is deprecated, and if so handle it * accordingly, e.g. by setting response headers to signal the deprecation, 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 7d6f6d8e3a4..ba8fcd7570a 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,7 +20,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeSet; -import java.util.function.Function; import java.util.function.Predicate; import org.jspecify.annotations.Nullable; @@ -170,8 +169,7 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy { @Override public Mono resolveVersionAsync(ServerWebExchange exchange) { return Flux.fromIterable(this.versionResolvers) - .mapNotNull(resolver -> resolver.resolveVersionAsync(exchange)) - .flatMap(Function.identity()) + .flatMap(resolver -> resolver.resolveVersionAsync(exchange)) .next(); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java index 903ec701b40..4cd924aa15b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java @@ -232,7 +232,7 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport if (this.apiVersionStrategy != null) { if (exchange.getAttribute(API_VERSION_ATTRIBUTE) == null) { return this.apiVersionStrategy - .resolveParseAndValidateVersionAsync(exchange) + .resolveParseAndValidate(exchange) .doOnNext(version -> exchange.getAttributes() .put(API_VERSION_ATTRIBUTE, version)); } 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 f6886795712..ad6fb739218 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 @@ -21,6 +21,7 @@ import java.util.function.Predicate; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import org.springframework.web.accept.InvalidApiVersionException; @@ -31,7 +32,6 @@ import org.springframework.web.testfixture.server.MockServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link org.springframework.web.accept.DefaultApiVersionStrategy}. @@ -51,91 +51,80 @@ public class DefaultApiVersionStrategiesTests { } @Test - void missingRequiredVersionReactively() { - - testValidateReactively(null, apiVersionStrategy()) - .expectErrorSatisfies(throwable -> - assertThat(throwable).isInstanceOf(MissingApiVersionException.class) - .hasMessage(("400 BAD_REQUEST \"API version is " + - "required.\"") - )) + void missingRequiredVersion() { + StepVerifier.create(testValidate(null, apiVersionStrategy())) + .expectErrorSatisfies(ex -> assertThat(ex) + .isInstanceOf(MissingApiVersionException.class) + .hasMessage(("400 BAD_REQUEST \"API version is required.\""))) .verify(); } @Test - void validateSupportedVersionReactively() { + void validateSupportedVersion() { String version = "1.2"; DefaultApiVersionStrategy strategy = apiVersionStrategy(); strategy.addSupportedVersion(version); - testValidateReactively(version, strategy) - .expectNextMatches(next -> next.toString().equals("1.2.0")) - .verifyComplete(); + Mono result = testValidate(version, strategy); + StepVerifier.create(result).expectNext("1.2.0").verifyComplete(); } @Test - void validateSupportedVersionForDefaultVersionReactively() { + void validateSupportedVersionForDefaultVersion() { String defaultVersion = "1.2"; DefaultApiVersionStrategy strategy = apiVersionStrategy(defaultVersion, false, null); - - testValidateReactively(defaultVersion, strategy) - .expectNextMatches(next -> next.toString().equals("1.2.0")) - .verifyComplete(); + Mono result = testValidate(defaultVersion, strategy); + StepVerifier.create(result).expectNext("1.2.0").verifyComplete(); } @Test - void validateUnsupportedVersionReactively() { - testValidateReactively("1.2", apiVersionStrategy()) - .expectErrorSatisfies(throwable -> - assertThat(throwable).isInstanceOf(InvalidApiVersionException.class) - .hasMessage(("400 BAD_REQUEST \"Invalid API " + - "version: '1.2.0'.\"") - )) + void validateUnsupportedVersion() { + StepVerifier.create(testValidate("1.2", apiVersionStrategy())) + .expectErrorSatisfies(ex -> assertThat(ex) + .isInstanceOf(InvalidApiVersionException.class) + .hasMessage(("400 BAD_REQUEST \"Invalid API version: '1.2.0'.\""))) .verify(); - } @Test - void validateDetectedVersionReactively() { + void validateDetectedVersion() { String version = "1.2"; DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true, null); strategy.addMappedVersion(version); - testValidateReactively(version, strategy) - .expectNextMatches(next -> next.toString().equals("1.2.0")) - .verifyComplete(); + Mono result = testValidate(version, strategy); + StepVerifier.create(result).expectNext("1.2.0").verifyComplete(); } @Test - void validateWhenDetectedVersionOffReactively() { + void validateWhenDetectedVersionOff() { String version = "1.2"; DefaultApiVersionStrategy strategy = apiVersionStrategy(); strategy.addMappedVersion(version); - testValidateReactively(version, strategy) - .expectError(InvalidApiVersionException.class) - .verify(); + Mono result = testValidate(version, strategy); + StepVerifier.create(result).expectError(InvalidApiVersionException.class).verify(); } @Test - void validateSupportedWithPredicateReactively() { + void validateSupportedWithPredicate() { SemanticApiVersionParser.Version parsedVersion = parser.parseVersion("1.2"); - testValidateReactively("1.2", apiVersionStrategy(null, false, version -> version.equals(parsedVersion))) - .expectNextMatches(next -> next.toString().equals("1.2.0")) - .verifyComplete(); + ApiVersionStrategy strategy = apiVersionStrategy(null, false, version -> version.equals(parsedVersion)); + Mono result = testValidate("1.2", strategy); + StepVerifier.create(result).expectNext("1.2.0").verifyComplete(); } @Test - void validateUnsupportedWithPredicateReactively() { - DefaultApiVersionStrategy strategy = apiVersionStrategy(null, false, version -> version.equals("1.2")); - testValidateReactively("1.2", strategy) - .verifyError(InvalidApiVersionException.class); + void validateUnsupportedWithPredicate() { + ApiVersionStrategy strategy = apiVersionStrategy(null, false, version -> version.equals("1.2")); + Mono result = testValidate("1.2", strategy); + StepVerifier.create(result).verifyError(InvalidApiVersionException.class); } @Test - void versionRequiredAndDefaultVersionSetReactively() { + void versionRequiredAndDefaultVersionSet() { assertThatIllegalArgumentException() - .isThrownBy(() -> - new org.springframework.web.accept.DefaultApiVersionStrategy( - List.of(request -> request.getParameter("api-version")), new SemanticApiVersionParser(), - true, "1.2", true, version -> true, null)) + .isThrownBy(() -> { + ApiVersionResolver resolver = new QueryApiVersionResolver("api-version"); + new DefaultApiVersionStrategy(List.of(resolver), parser, true, "1.2", true, v -> true, null); + }) .withMessage("versionRequired cannot be set to true if a defaultVersion is also configured"); } @@ -152,88 +141,13 @@ public class DefaultApiVersionStrategiesTests { parser, null, defaultVersion, detectSupportedVersions, supportedVersionPredicate, null); } - private StepVerifier.FirstStep> testValidateReactively(@Nullable String version, DefaultApiVersionStrategy strategy) { - MockServerHttpRequest.BaseBuilder requestBuilder = MockServerHttpRequest.get("/"); - if (version != null) { - requestBuilder.queryParam("api-version", version); - } - return StepVerifier.create(strategy.resolveParseAndValidateVersionAsync(MockServerWebExchange.builder(requestBuilder).build())); - } - - @Test - void missingRequiredVersion() { - assertThatThrownBy(() -> testValidate(null, apiVersionStrategy())) - .isInstanceOf(MissingApiVersionException.class) - .hasMessage("400 BAD_REQUEST \"API version is required.\""); - } - - @Test - void validateSupportedVersion() { - String version = "1.2"; - DefaultApiVersionStrategy strategy = apiVersionStrategy(); - strategy.addSupportedVersion(version); - testValidate(version, strategy); - } - - @Test - void validateSupportedVersionForDefaultVersion() { - String defaultVersion = "1.2"; - DefaultApiVersionStrategy strategy = apiVersionStrategy(defaultVersion, false, null); - testValidate(defaultVersion, strategy); - } - - @Test - void validateUnsupportedVersion() { - assertThatThrownBy(() -> testValidate("1.2", apiVersionStrategy())) - .isInstanceOf(InvalidApiVersionException.class) - .hasMessage("400 BAD_REQUEST \"Invalid API version: '1.2.0'.\""); - } - - @Test - void validateDetectedVersion() { - String version = "1.2"; - DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true, null); - strategy.addMappedVersion(version); - testValidate(version, strategy); - } - - @Test - void validateWhenDetectedVersionOff() { - String version = "1.2"; - DefaultApiVersionStrategy strategy = apiVersionStrategy(); - strategy.addMappedVersion(version); - assertThatThrownBy(() -> testValidate(version, strategy)).isInstanceOf(InvalidApiVersionException.class); - } - - @Test - void validateSupportedWithPredicate() { - SemanticApiVersionParser.Version parsedVersion = parser.parseVersion("1.2"); - testValidate("1.2", apiVersionStrategy(null, false, version -> version.equals(parsedVersion))); - } - - @Test - void validateUnsupportedWithPredicate() { - DefaultApiVersionStrategy strategy = apiVersionStrategy(null, false, version -> version.equals("1.2")); - assertThatThrownBy(() -> testValidate("1.2", strategy)).isInstanceOf(InvalidApiVersionException.class); - } - - @Test - void versionRequiredAndDefaultVersionSet() { - assertThatIllegalArgumentException() - .isThrownBy(() -> - new org.springframework.web.accept.DefaultApiVersionStrategy( - List.of(request -> request.getParameter("api-version")), new SemanticApiVersionParser(), - true, "1.2", true, version -> true, null)) - .withMessage("versionRequired cannot be set to true if a defaultVersion is also configured"); - } - - @SuppressWarnings("removal") - private void testValidate(@Nullable String version, DefaultApiVersionStrategy strategy) { - MockServerHttpRequest.BaseBuilder requestBuilder = MockServerHttpRequest.get("/"); + private Mono testValidate(@Nullable String version, ApiVersionStrategy strategy) { + MockServerHttpRequest.BaseBuilder builder = MockServerHttpRequest.get("/"); if (version != null) { - requestBuilder.queryParam("api-version", version); + builder.queryParam("api-version", version); } - strategy.resolveParseAndValidateVersion(MockServerWebExchange.builder(requestBuilder).build()); + MockServerWebExchange exchange = MockServerWebExchange.builder(builder).build(); + return strategy.resolveParseAndValidate(exchange).map(Object::toString); } }