From 2e915a3934444ae351e12e0443ee29a1b88873df Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 26 Sep 2025 17:32:38 +0100 Subject: [PATCH] Provide access to API version in controller method Closes gh-35424 --- .../ApiVersionMethodArgumentResolver.java | 70 ++++++++++ .../annotation/ControllerMethodResolver.java | 1 + ...ApiVersionMethodArgumentResolverTests.java | 115 ++++++++++++++++ .../ControllerMethodResolverTests.java | 4 + ...RequestMappingVersionIntegrationTests.java | 4 +- .../ApiVersionMethodArgumentResolver.java | 76 +++++++++++ .../RequestMappingHandlerAdapter.java | 1 + ...ApiVersionMethodArgumentResolverTests.java | 123 ++++++++++++++++++ ...MappingHandlerAdapterIntegrationTests.java | 9 +- 9 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ApiVersionMethodArgumentResolver.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ApiVersionMethodArgumentResolverTests.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ApiVersionMethodArgumentResolver.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ApiVersionMethodArgumentResolverTests.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ApiVersionMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ApiVersionMethodArgumentResolver.java new file mode 100644 index 00000000000..6946db0186c --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ApiVersionMethodArgumentResolver.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.util.Optional; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.MethodParameter; +import org.springframework.web.accept.MissingApiVersionException; +import org.springframework.web.accept.SemanticApiVersionParser; +import org.springframework.web.reactive.BindingContext; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolvers argument values of type {@link SemanticApiVersionParser.Version}. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +public class ApiVersionMethodArgumentResolver implements SyncHandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return (SemanticApiVersionParser.Version.class == parameter.nestedIfOptional().getNestedParameterType()); + } + + @Override + public @Nullable Object resolveArgumentValue( + MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) { + + Object version = exchange.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE); + + if (parameter.getParameterType() == Optional.class) { + return Optional.ofNullable(version); + } + + if (version == null) { + if (!parameter.isOptional()) { + // typically this should be caught earlier in AbstractHandlerMapping + throw new MissingApiVersionException(); + } + return null; + } + + if (version instanceof SemanticApiVersionParser.Version semanticApiVersion) { + return semanticApiVersion; + } + + // Should never happen + throw new IllegalStateException("Unexpected version type: " + version.getClass()); + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java index 5df9086e867..5609683efcb 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java @@ -242,6 +242,7 @@ class ControllerMethodResolver { result.add(new SessionStatusMethodArgumentResolver()); } result.add(new WebSessionMethodArgumentResolver(adapterRegistry)); + result.add(new ApiVersionMethodArgumentResolver()); if (KotlinDetector.isKotlinPresent()) { result.add(new ContinuationHandlerMethodArgumentResolver()); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ApiVersionMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ApiVersionMethodArgumentResolverTests.java new file mode 100644 index 00000000000..354025c80d2 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ApiVersionMethodArgumentResolverTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.web.accept.SemanticApiVersionParser; +import org.springframework.web.accept.SemanticApiVersionParser.Version; +import org.springframework.web.reactive.BindingContext; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; +import org.springframework.web.testfixture.server.MockServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ApiVersionMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +class ApiVersionMethodArgumentResolverTests { + + private final ApiVersionMethodArgumentResolver resolver = new ApiVersionMethodArgumentResolver(); + + private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + + private MethodParameter param; + private MethodParameter nullableParam; + private MethodParameter optionalParam; + private MethodParameter intParam; + + + @BeforeEach + void setUp() throws Exception { + + Method method = getClass().getDeclaredMethod( + "handle", Version.class, Version.class, Optional.class, int.class); + + this.param = new MethodParameter(method, 0); + this.nullableParam = new MethodParameter(method, 1); + this.optionalParam = new MethodParameter(method, 2); + this.intParam = new MethodParameter(method, 3); + } + + @Test + void supportsParameter() { + assertThat(this.resolver.supportsParameter(this.param)).isTrue(); + assertThat(this.resolver.supportsParameter(this.nullableParam)).isTrue(); + assertThat(this.resolver.supportsParameter(this.optionalParam)).isTrue(); + assertThat(this.resolver.supportsParameter(this.intParam)).isFalse(); + } + + @Test + void resolveArgument() throws Exception { + Version version = new SemanticApiVersionParser().parseVersion("1.2"); + this.exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, version); + + Object actual = this.resolver.resolveArgumentValue(this.param, new BindingContext(), exchange); + + assertThat(actual).isNotNull(); + assertThat(actual.getClass()).isEqualTo(Version.class); + assertThat(actual).isSameAs(version); + } + + @Test + void resolveNullableArgument() { + Object actual = this.resolver.resolveArgumentValue(this.nullableParam, new BindingContext(), exchange); + assertThat(actual).isNull(); + } + + @Test + void resolveOptionalArgument() { + Version version = new SemanticApiVersionParser().parseVersion("1.2"); + this.exchange.getAttributes().put(HandlerMapping.API_VERSION_ATTRIBUTE, version); + + Object actual = this.resolver.resolveArgumentValue(this.optionalParam, new BindingContext(), exchange); + assertThat(((Optional) actual)).hasValue(version); + } + + @Test + void resolveOptionalArgumentWhenEmpty() { + Object actual = this.resolver.resolveArgumentValue(this.optionalParam, new BindingContext(), exchange); + assertThat(((Optional) actual)).isEmpty(); + } + + + @SuppressWarnings({"OptionalUsedAsFieldOrParameterType", "unused"}) + void handle( + Version version, + @Nullable Version nullableVersion, + Optional optionalVersion, + int value) { + } + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java index 2b62949dc08..e2545dd1276 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java @@ -120,6 +120,7 @@ class ControllerMethodResolverTests { assertThat(next(resolvers, index).getClass()).isEqualTo(PrincipalMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(SessionStatusMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(WebSessionMethodArgumentResolver.class); + assertThat(next(resolvers, index).getClass()).isEqualTo(ApiVersionMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(ContinuationHandlerMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(CustomArgumentResolver.class); @@ -157,6 +158,7 @@ class ControllerMethodResolverTests { assertThat(next(resolvers, index).getClass()).isEqualTo(ServerWebExchangeMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(PrincipalMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(WebSessionMethodArgumentResolver.class); + assertThat(next(resolvers, index).getClass()).isEqualTo(ApiVersionMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(ContinuationHandlerMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(CustomArgumentResolver.class); @@ -190,6 +192,7 @@ class ControllerMethodResolverTests { assertThat(next(resolvers, index).getClass()).isEqualTo(ModelMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(ServerWebExchangeMethodArgumentResolver.class); + assertThat(next(resolvers, index).getClass()).isEqualTo(ApiVersionMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(CustomSyncArgumentResolver.class); @@ -224,6 +227,7 @@ class ControllerMethodResolverTests { assertThat(next(resolvers, index).getClass()).isEqualTo(ServerWebExchangeMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(PrincipalMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(WebSessionMethodArgumentResolver.class); + assertThat(next(resolvers, index).getClass()).isEqualTo(ApiVersionMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(ContinuationHandlerMethodArgumentResolver.class); assertThat(next(resolvers, index).getClass()).isEqualTo(CustomArgumentResolver.class); 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 e4be854d953..7457a8d1d8f 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 @@ -22,6 +22,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.web.accept.SemanticApiVersionParser.Version; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.HttpClientErrorException; @@ -104,7 +105,8 @@ public class RequestMappingVersionIntegrationTests extends AbstractRequestMappin } @GetMapping(version = "1.2+") - String version1_2() { + String version1_2(Version version) { + assertThat(version).isNotNull(); return getBody("1.2"); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ApiVersionMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ApiVersionMethodArgumentResolver.java new file mode 100644 index 00000000000..d49134adc3d --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ApiVersionMethodArgumentResolver.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.util.Optional; + +import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; +import org.springframework.web.accept.MissingApiVersionException; +import org.springframework.web.accept.SemanticApiVersionParser; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Resolvers argument values of type {@link SemanticApiVersionParser.Version}. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +public class ApiVersionMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return (SemanticApiVersionParser.Version.class == parameter.nestedIfOptional().getNestedParameterType()); + } + + @Override + public @Nullable Object resolveArgument( + MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + Assert.state(request != null, "No HttpServletRequest"); + Object version = request.getAttribute(HandlerMapping.API_VERSION_ATTRIBUTE); + + if (parameter.getParameterType() == Optional.class) { + return Optional.ofNullable(version); + } + + if (version == null) { + if (!parameter.isOptional()) { + // typically this should be caught earlier in AbstractHandlerMapping + throw new MissingApiVersionException(); + } + return null; + } + + if (version instanceof SemanticApiVersionParser.Version semanticApiVersion) { + return semanticApiVersion; + } + + // Should never happen + throw new IllegalStateException("Unexpected version type: " + version.getClass()); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index dde3ef44ea6..bb1e64b54d7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -671,6 +671,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter resolvers.add(new ErrorsMethodArgumentResolver()); resolvers.add(new SessionStatusMethodArgumentResolver()); resolvers.add(new UriComponentsBuilderMethodArgumentResolver()); + resolvers.add(new ApiVersionMethodArgumentResolver()); if (KotlinDetector.isKotlinPresent()) { resolvers.add(new ContinuationHandlerMethodArgumentResolver()); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ApiVersionMethodArgumentResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ApiVersionMethodArgumentResolverTests.java new file mode 100644 index 00000000000..7eeba187740 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ApiVersionMethodArgumentResolverTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.lang.reflect.Method; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.web.accept.SemanticApiVersionParser; +import org.springframework.web.accept.SemanticApiVersionParser.Version; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test fixture with {@link ApiVersionMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +class ApiVersionMethodArgumentResolverTests { + + private ApiVersionMethodArgumentResolver resolver; + + private ServletWebRequest webRequest; + + private MockHttpServletRequest servletRequest; + + private final ModelAndViewContainer mav = new ModelAndViewContainer(); + + private MethodParameter param; + private MethodParameter nullableParam; + private MethodParameter optionalParam; + private MethodParameter intParam; + + + @BeforeEach + void setup() throws Exception { + this.resolver = new ApiVersionMethodArgumentResolver(); + this.servletRequest = new MockHttpServletRequest(); + this.webRequest = new ServletWebRequest(this.servletRequest); + + Method method = getClass().getDeclaredMethod( + "handle", Version.class, Version.class, Optional.class, int.class); + + this.param = new MethodParameter(method, 0); + this.nullableParam = new MethodParameter(method, 1); + this.optionalParam = new MethodParameter(method, 2); + this.intParam = new MethodParameter(method, 3); + } + + + @Test + void supportsParameter() throws Exception { + assertThat(this.resolver.supportsParameter(this.param)).isTrue(); + assertThat(this.resolver.supportsParameter(this.nullableParam)).isTrue(); + assertThat(this.resolver.supportsParameter(this.optionalParam)).isTrue(); + assertThat(this.resolver.supportsParameter(this.intParam)).isFalse(); + } + + @Test + void resolveArgument() throws Exception { + Version version = new SemanticApiVersionParser().parseVersion("1.2"); + this.servletRequest.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, version); + + Object actual = this.resolver.resolveArgument(this.param, this.mav, this.webRequest, null); + + assertThat(actual).isNotNull(); + assertThat(actual.getClass()).isEqualTo(Version.class); + assertThat(actual).isSameAs(version); + } + + @Test + void resolveNullableArgument() throws Exception { + Object actual = this.resolver.resolveArgument(this.nullableParam, this.mav, this.webRequest, null); + assertThat(actual).isNull(); + } + + @Test + void resolveOptionalArgument() throws Exception { + Version version = new SemanticApiVersionParser().parseVersion("1.2"); + this.servletRequest.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, version); + + Object actual = this.resolver.resolveArgument(this.optionalParam, this.mav, this.webRequest, null); + assertThat(((Optional) actual)).hasValue(version); + } + + @Test + void resolveOptionalArgumentWhenEmpty() throws Exception { + Object actual = this.resolver.resolveArgument(this.optionalParam, this.mav, this.webRequest, null); + assertThat(((Optional) actual)).isEmpty(); + } + + + @SuppressWarnings({"OptionalUsedAsFieldOrParameterType", "unused"}) + void handle( + Version version, + @Nullable Version nullableVersion, + Optional optionalVersion, + int value) { + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java index 8e2f796349e..6e63ffe1eb2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java @@ -57,6 +57,7 @@ import org.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.Validator; +import org.springframework.web.accept.SemanticApiVersionParser; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.InitBinder; @@ -147,7 +148,8 @@ class RequestMappingHandlerAdapterIntegrationTests { Class[] parameterTypes = new Class[] {int.class, String.class, String.class, String.class, Map.class, Date.class, Map.class, String.class, String.class, TestBean.class, Errors.class, TestBean.class, Color.class, HttpServletRequest.class, HttpServletResponse.class, TestBean.class, TestBean.class, - User.class, OtherUser.class, Principal.class, Model.class, UriComponentsBuilder.class}; + User.class, OtherUser.class, Principal.class, Model.class, + SemanticApiVersionParser.Version.class, UriComponentsBuilder.class}; String datePattern = "yyyy.MM.dd"; String formattedDate = "2011.03.16"; @@ -173,6 +175,8 @@ class RequestMappingHandlerAdapterIntegrationTests { request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); request.getSession().setAttribute("sessionAttribute", sessionAttribute); request.setAttribute("requestAttribute", requestAttribute); + SemanticApiVersionParser.Version version = new SemanticApiVersionParser().parseVersion("1.2"); + request.setAttribute(HandlerMapping.API_VERSION_ATTRIBUTE, version); HandlerMethod handlerMethod = handlerMethod("handle", parameterTypes); ModelAndView mav = handlerAdapter.handle(request, response, handlerMethod); @@ -221,6 +225,7 @@ class RequestMappingHandlerAdapterIntegrationTests { assertThat(model.get("sessionAttribute")).isSameAs(sessionAttribute); assertThat(model.get("requestAttribute")).isSameAs(requestAttribute); + assertThat(model.get("version")).isSameAs(version); assertThat(model.get("url")).isEqualTo(URI.create("http://localhost/contextPath/main/path")); } @@ -483,6 +488,7 @@ class RequestMappingHandlerAdapterIntegrationTests { @ModelAttribute OtherUser otherUser, @AuthenticationPrincipal Principal customUser, // gh-25780 Model model, + SemanticApiVersionParser.Version version, UriComponentsBuilder builder) { model.addAttribute("cookie", cookieV) @@ -499,6 +505,7 @@ class RequestMappingHandlerAdapterIntegrationTests { .addAttribute("customUser", customUser) .addAttribute("sessionAttribute", sessionAttribute) .addAttribute("requestAttribute", requestAttribute) + .addAttribute("version", version) .addAttribute("url", builder.path("/path").build().toUri()); assertThat(request).isNotNull();