From 224f1af08efab1a228dbd39f3979d926c9cafe53 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 27 Jun 2025 12:13:00 +0100 Subject: [PATCH] Prepare to support API versioning for fn Add default method to resolve, parse, and validate version Simplify tests --- .../web/accept/ApiVersionStrategy.java | 21 +++++++++++++++++++ .../reactive/accept/ApiVersionStrategy.java | 21 +++++++++++++++++++ .../accept/DefaultApiVersionStrategy.java | 10 +++++++++ .../RequestMappingHandlerMapping.java | 18 +--------------- ...RequestMappingVersionIntegrationTests.java | 19 +++++++---------- .../RequestMappingHandlerMapping.java | 18 +--------------- ...questMappingVersionHandlerMethodTests.java | 14 ++++--------- 7 files changed, 65 insertions(+), 56 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/accept/ApiVersionStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/ApiVersionStrategy.java index 1dc1496197..9418ab7be0 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/ApiVersionStrategy.java +++ b/spring-web/src/main/java/org/springframework/web/accept/ApiVersionStrategy.java @@ -62,6 +62,27 @@ public interface ApiVersionStrategy { */ @Nullable Comparable getDefaultVersion(); + /** + * Convenience method to return the parsed and validated request version, + * or the default version if configured. + * @param request the current request + * @return the parsed request version, or the default version + */ + default @Nullable Comparable resolveParseAndValidateVersion(HttpServletRequest request) { + String value = resolveVersion(request); + if (value == null) { + return getDefaultVersion(); + } + try { + Comparable version = parseVersion(value); + validateVersion(version, request); + return version; + } + catch (Exception ex) { + throw new InvalidApiVersionException(value, null, ex); + } + } + /** * 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/ApiVersionStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/ApiVersionStrategy.java index 994f4837bb..fc7cca7af5 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 @@ -64,6 +64,27 @@ public interface ApiVersionStrategy { */ @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 + */ + default @Nullable Comparable resolveParseAndValidateVersion(ServerWebExchange exchange) { + String value = resolveVersion(exchange); + if (value == null) { + return getDefaultVersion(); + } + try { + Comparable version = parseVersion(value); + validateVersion(version, exchange); + return version; + } + catch (Exception ex) { + throw new InvalidApiVersionException(value, null, ex); + } + } + /** * 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 ff8ba4563d..08816c0032 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 @@ -93,6 +93,16 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy { return this.defaultVersion; } + /** + * Whether the strategy is configured to detect supported versions. + * If this is set to {@code false} then {@link #addMappedVersion} is ignored + * and the list of supported versions can be built explicitly through calls + * to {@link #addSupportedVersion}. + */ + public boolean detectSupportedVersions() { + return this.detectSupportedVersions; + } + /** * Add to the list of supported versions to check against in * {@link ApiVersionStrategy#validateVersion} before raising diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index 4ce5262f76..3422315bfd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -42,7 +42,6 @@ import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; -import org.springframework.web.accept.InvalidApiVersionException; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -180,7 +179,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi if (this.apiVersionStrategy != null) { Comparable version = exchange.getAttribute(API_VERSION_ATTRIBUTE); if (version == null) { - version = getApiVersion(exchange, this.apiVersionStrategy); + version = this.apiVersionStrategy.resolveParseAndValidateVersion(exchange); if (version != null) { exchange.getAttributes().put(API_VERSION_ATTRIBUTE, version); } @@ -189,21 +188,6 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi return super.getHandlerInternal(exchange); } - private static @Nullable Comparable getApiVersion(ServerWebExchange exchange, ApiVersionStrategy strategy) { - String value = strategy.resolveVersion(exchange); - if (value == null) { - return strategy.getDefaultVersion(); - } - try { - Comparable version = strategy.parseVersion(value); - strategy.validateVersion(version, exchange); - return version; - } - catch (Exception ex) { - throw new InvalidApiVersionException(value, null, ex); - } - } - /** * Uses type-level and method-level {@link RequestMapping @RequestMapping} * and {@link HttpExchange @HttpExchange} annotations to create the diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingVersionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingVersionIntegrationTests.java index bfc090b667..c80065d55c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingVersionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingVersionIntegrationTests.java @@ -50,29 +50,24 @@ public class RequestMappingVersionIntegrationTests extends AbstractRequestMappin @ParameterizedHttpServerTest - void initialVersion(HttpServer httpServer) throws Exception { + void mapVersion(HttpServer httpServer) throws Exception { startServer(httpServer); + assertThat(exchangeWithVersion("1.0").getBody()).isEqualTo("none"); assertThat(exchangeWithVersion("1.1").getBody()).isEqualTo("none"); - } - - @ParameterizedHttpServerTest - void baselineVersion(HttpServer httpServer) throws Exception { - startServer(httpServer); assertThat(exchangeWithVersion("1.2").getBody()).isEqualTo("1.2"); assertThat(exchangeWithVersion("1.3").getBody()).isEqualTo("1.2"); - } - - @ParameterizedHttpServerTest - void fixedVersion(HttpServer httpServer) throws Exception { - startServer(httpServer); assertThat(exchangeWithVersion("1.5").getBody()).isEqualTo("1.5"); - assertThatThrownBy(() -> exchangeWithVersion("1.6")).isInstanceOf(HttpClientErrorException.BadRequest.class); + + assertThatThrownBy(() -> exchangeWithVersion("1.6")) + .as("Should reject if highest supported below request version is fixed") + .isInstanceOf(HttpClientErrorException.BadRequest.class); } @ParameterizedHttpServerTest void deprecation(HttpServer httpServer) throws Exception { startServer(httpServer); + assertThat(exchangeWithVersion("1").getHeaders().getFirst("Link")) .isEqualTo("; rel=\"deprecation\"; type=\"text/html\""); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 0c2e9d3c34..a21c10cbeb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -46,7 +46,6 @@ import org.springframework.util.StringValueResolver; import org.springframework.web.accept.ApiVersionStrategy; import org.springframework.web.accept.ContentNegotiationManager; 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.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -207,7 +206,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi if (this.apiVersionStrategy != null) { Comparable version = (Comparable) request.getAttribute(API_VERSION_ATTRIBUTE); if (version == null) { - version = getApiVersion(request, this.apiVersionStrategy); + version = this.apiVersionStrategy.resolveParseAndValidateVersion(request); if (version != null) { request.setAttribute(API_VERSION_ATTRIBUTE, version); } @@ -216,21 +215,6 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi return super.getHandlerInternal(request); } - private static @Nullable Comparable getApiVersion(HttpServletRequest request, ApiVersionStrategy strategy) { - String value = strategy.resolveVersion(request); - if (value == null) { - return strategy.getDefaultVersion(); - } - try { - Comparable version = strategy.parseVersion(value); - strategy.validateVersion(version, request); - return version; - } - catch (Exception ex) { - throw new InvalidApiVersionException(value, null, ex); - } - } - /** * Uses type-level and method-level {@link RequestMapping @RequestMapping} * and {@link HttpExchange @HttpExchange} annotations to create the diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingVersionHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingVersionHandlerMethodTests.java index c926ed9a1d..72cfa67e94 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingVersionHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingVersionHandlerMethodTests.java @@ -59,23 +59,17 @@ public class RequestMappingVersionHandlerMethodTests { @Test - void initialVersion() throws Exception { + void mapVersion() throws Exception { assertThat(requestWithVersion("1.0").getContentAsString()).isEqualTo("none"); assertThat(requestWithVersion("1.1").getContentAsString()).isEqualTo("none"); - } - - @Test - void baselineVersion() throws Exception { assertThat(requestWithVersion("1.2").getContentAsString()).isEqualTo("1.2"); assertThat(requestWithVersion("1.3").getContentAsString()).isEqualTo("1.2"); - } - - @Test - void fixedVersion() throws Exception { assertThat(requestWithVersion("1.5").getContentAsString()).isEqualTo("1.5"); MockHttpServletResponse response = requestWithVersion("1.6"); - assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getStatus()) + .as("Should reject if highest supported below request version is fixed") + .isEqualTo(400); } @Test