From f73302a66ee692697d0d99dad1f02582fb6d5e96 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 27 Feb 2026 15:10:32 +0000 Subject: [PATCH] Polishing contribution Closes gh-36398 --- .../web/accept/PathApiVersionResolver.java | 33 ++++++----- .../accept/PathApiVersionResolverTests.java | 57 ++++++------------- .../accept/PathApiVersionResolver.java | 30 ++++++---- .../reactive/config/ApiVersionConfigurer.java | 15 ++--- .../accept/PathApiVersionResolverTests.java | 48 +++++----------- .../annotation/ApiVersionConfigurer.java | 15 ++--- 6 files changed, 85 insertions(+), 113 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/accept/PathApiVersionResolver.java b/spring-web/src/main/java/org/springframework/web/accept/PathApiVersionResolver.java index dd21646b656..9c0f8820fab 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/PathApiVersionResolver.java +++ b/spring-web/src/main/java/org/springframework/web/accept/PathApiVersionResolver.java @@ -30,10 +30,13 @@ import org.springframework.web.util.ServletRequestPathUtils; /** * {@link ApiVersionResolver} that extract the version from a path segment. * - *

Note that this resolver will either resolve the version from the specified - * path segment, or raise an {@link InvalidApiVersionException}, e.g. if there - * are not enough path segments. It never returns {@code null}, and therefore - * cannot yield to other resolvers. + *

If the resolver is created with a path index only, it will always return + * a version, or raise an {@link InvalidApiVersionException}, but never + * return {@code null}. + * + *

The resolver can also be created with an additional + * {@code Predicate} that provides more flexibility in deciding + * whether a given path is versioned or not, possibly resolving to {@code null}. * * @author Rossen Stoyanchev * @since 7.0 @@ -41,7 +44,8 @@ import org.springframework.web.util.ServletRequestPathUtils; public class PathApiVersionResolver implements ApiVersionResolver { private final int pathSegmentIndex; - private @Nullable Predicate includePath; + + private @Nullable Predicate versionPathPredicate; /** @@ -55,22 +59,19 @@ public class PathApiVersionResolver implements ApiVersionResolver { } /** - * Create a resolver instance. - * @param pathSegmentIndex the index of the path segment that contains the API version - * @param includePath a {@link Predicate} that tests if the given path should be included + * Constructor variant of {@link #PathApiVersionResolver(int)} with an + * additional {@code Predicate} to help determine whether + * a given path is versioned (true) or not (false). */ - public PathApiVersionResolver(int pathSegmentIndex, Predicate includePath) { + public PathApiVersionResolver(int pathSegmentIndex, Predicate versionPathPredicate) { this(pathSegmentIndex); - this.includePath = includePath; + this.versionPathPredicate = versionPathPredicate; } @Override public @Nullable String resolveVersion(HttpServletRequest request) { - if (!ServletRequestPathUtils.hasParsedRequestPath(request)) { - throw new IllegalStateException("Expected parsed request path"); - } RequestPath path = ServletRequestPathUtils.getParsedRequestPath(request); - if (this.includePath != null && !this.includePath.test(path)) { + if (!isVersionedPath(path)) { return null; } int i = 0; @@ -82,4 +83,8 @@ public class PathApiVersionResolver implements ApiVersionResolver { throw new InvalidApiVersionException("No path segment at index " + this.pathSegmentIndex); } + private boolean isVersionedPath(RequestPath path) { + return (this.versionPathPredicate == null || this.versionPathPredicate.test(path)); + } + } diff --git a/spring-web/src/test/java/org/springframework/web/accept/PathApiVersionResolverTests.java b/spring-web/src/test/java/org/springframework/web/accept/PathApiVersionResolverTests.java index bd8ffa67aa2..ac82703d073 100644 --- a/spring-web/src/test/java/org/springframework/web/accept/PathApiVersionResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/accept/PathApiVersionResolverTests.java @@ -17,10 +17,12 @@ package org.springframework.web.accept; import java.util.List; +import java.util.function.Predicate; import org.junit.jupiter.api.Test; import org.springframework.http.server.PathContainer; +import org.springframework.http.server.RequestPath; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -45,52 +47,25 @@ public class PathApiVersionResolverTests { } @Test - void includePathFalse() { - String requestUri = "/v3/api-docs"; - testResolveWithIncludePath(requestUri, null); + void resolveWithVersionPathPredicate() { + testVersionPathPredicate("/app/1.0/path", "1.0"); + testVersionPathPredicate("/app", null); + testVersionPathPredicate("/v3/api-docs", null); } - @Test - void includePathTrue() { - String requestUri = "/app/1.0/path"; - testResolveWithIncludePath(requestUri, "1.0"); - } - - @Test - void includePathFalseShortPath() { - String requestUri = "/app"; - testResolveWithIncludePath(requestUri, null); - } - - @Test - void includePathInsufficientPathSegments() { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/app"); - try { - ServletRequestPathUtils.parseAndCache(request); - assertThatThrownBy(() -> new PathApiVersionResolver(1, requestPath -> true) - .resolveVersion(request)) - .isInstanceOf(InvalidApiVersionException.class); - } - finally { - ServletRequestPathUtils.clearParsedRequestPath(request); - } - } - - private static void testResolveWithIncludePath(String requestUri, String expected) { + private static void testVersionPathPredicate(String requestUri, String expectedVersion) { + Predicate versionPathPredicate = path -> { + List elements = path.elements(); + return (elements.size() > 3 && + elements.get(1).value().equals("app") && + elements.get(3).value().matches("\\d+\\.\\d+")); + }; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); try { ServletRequestPathUtils.parseAndCache(request); - String actual = new PathApiVersionResolver(1, requestPath -> { - List elements = requestPath.elements(); - if (elements.size() < 4) { - return false; - } - return elements.get(0).value().equals("/") && - elements.get(1).value().equals("app") && - elements.get(2).value().equals("/") && - elements.get(3).value().equals("1.0"); - }).resolveVersion(request); - assertThat(actual).isEqualTo(expected); + PathApiVersionResolver resolver = new PathApiVersionResolver(1, versionPathPredicate); + String actual = resolver.resolveVersion(request); + assertThat(actual).isEqualTo(expectedVersion); } finally { ServletRequestPathUtils.clearParsedRequestPath(request); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java index 2aa0284118f..d24760d81a4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java @@ -29,10 +29,13 @@ import org.springframework.web.server.ServerWebExchange; /** * {@link ApiVersionResolver} that extract the version from a path segment. * - *

Note that this resolver will either resolve the version from the specified - * path segment, or raise an {@link InvalidApiVersionException}, e.g. if there - * are not enough path segments. It never returns {@code null}, and therefore - * cannot yield to other resolvers. + *

If the resolver is created with a path index only, it will always return + * a version, or raise an {@link InvalidApiVersionException}, but never + * return {@code null}. + * + *

The resolver can also be created with an additional + * {@code Predicate} that provides more flexibility in deciding + * whether a given path is versioned or not, possibly resolving to {@code null}. * * @author Rossen Stoyanchev * @author Martin Mois @@ -41,7 +44,8 @@ import org.springframework.web.server.ServerWebExchange; public class PathApiVersionResolver implements ApiVersionResolver { private final int pathSegmentIndex; - private @Nullable Predicate includePath = null; + + private @Nullable Predicate versionPathPredicate; /** @@ -55,13 +59,13 @@ public class PathApiVersionResolver implements ApiVersionResolver { } /** - * Create a resolver instance. - * @param pathSegmentIndex the index of the path segment that contains the API version - * @param includePath a {@link Predicate} that tests if the given path should be included + * Constructor variant of {@link #PathApiVersionResolver(int)} with an + * additional {@code Predicate} to help determine whether + * a given path is versioned (true) or not (false). */ - public PathApiVersionResolver(int pathSegmentIndex, Predicate includePath) { + public PathApiVersionResolver(int pathSegmentIndex, Predicate versionPathPredicate) { this(pathSegmentIndex); - this.includePath = includePath; + this.versionPathPredicate = versionPathPredicate; } @@ -69,7 +73,7 @@ public class PathApiVersionResolver implements ApiVersionResolver { public @Nullable String resolveVersion(ServerWebExchange exchange) { int i = 0; RequestPath path = exchange.getRequest().getPath(); - if (this.includePath != null && !this.includePath.test(path)) { + if (!isVersionedPath(path)) { return null; } for (PathContainer.Element e : path.pathWithinApplication().elements()) { @@ -80,4 +84,8 @@ public class PathApiVersionResolver implements ApiVersionResolver { throw new InvalidApiVersionException("No path segment at index " + this.pathSegmentIndex); } + private boolean isVersionedPath(RequestPath path) { + return (this.versionPathPredicate == null || this.versionPathPredicate.test(path)); + } + } 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 64b1ddbcd28..2cc00b35904 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 @@ -110,16 +110,17 @@ public class ApiVersionConfigurer { } /** - * Add a resolver that extracts the API version from a path segment - * and that allows to include only certain paths based on the provided {@link Predicate}. - *

Note that this resolver never returns {@code null}, and therefore - * cannot yield to other resolvers, see {@link org.springframework.web.accept.PathApiVersionResolver}. + * Variant of {@link #usePathSegment(int)} with a {@code Predicate} + * to determine whether a given path is versioned, providing additional + * flexibility, and the option to resolve the version to {@code null}. * @param index the index of the path segment to check; e.g. for URL's like * {@code "/{version}/..."} use index 0, for {@code "/api/{version}/..."} index 1. - * @param includePath a {@link Predicate} that allows to include a certain path + * @param versionPathPredicate used to decide if a path is versioned (true) + * or not (false). + * @since 7.0.6 */ - public ApiVersionConfigurer usePathSegment(int index, Predicate includePath) { - this.versionResolvers.add(new PathApiVersionResolver(index, includePath)); + public ApiVersionConfigurer usePathSegment(int index, Predicate versionPathPredicate) { + this.versionResolvers.add(new PathApiVersionResolver(index, versionPathPredicate)); return this; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/PathApiVersionResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/PathApiVersionResolverTests.java index d75d37940e6..bbd88535db1 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/PathApiVersionResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/PathApiVersionResolverTests.java @@ -17,10 +17,12 @@ package org.springframework.web.reactive.accept; import java.util.List; +import java.util.function.Predicate; import org.junit.jupiter.api.Test; import org.springframework.http.server.PathContainer; +import org.springframework.http.server.RequestPath; import org.springframework.web.accept.InvalidApiVersionException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; @@ -48,42 +50,22 @@ public class PathApiVersionResolverTests { } @Test - void includePathFalse() { - String requestUri = "/v3/api-docs"; - testResolveWithIncludePath(requestUri, null); + void resolveWithVersionPathPredicate() { + testVersionPathPredicate("/app/1.0/path", "1.0"); + testVersionPathPredicate("/app", null); + testVersionPathPredicate("/v3/api-docs", null); } - @Test - void includePathTrue() { - String requestUri = "/app/1.0/path"; - testResolveWithIncludePath(requestUri, "1.0"); - } - - @Test - void includePathFalseShortPath() { - String requestUri = "/app"; - testResolveWithIncludePath(requestUri, null); - } - - @Test - void includePathInsufficientPathSegments() { - ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/too-short")); - assertThatThrownBy(() -> new PathApiVersionResolver(1, requestPath -> true).resolveVersion(exchange)) - .isInstanceOf(InvalidApiVersionException.class); - } - - private static void testResolveWithIncludePath(String requestUri, String expected) { - ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(requestUri)); - String actual = new PathApiVersionResolver(1, requestPath -> { - List elements = requestPath.elements(); - if (elements.size() < 4) { - return false; - } - return elements.get(0).value().equals("/") && + private static void testVersionPathPredicate(String requestUri, String expected) { + Predicate versionPathPredicate = path -> { + List elements = path.elements(); + return (elements.size() > 3 && elements.get(1).value().equals("app") && - elements.get(2).value().equals("/") && - elements.get(3).value().equals("1.0"); - }).resolveVersion(exchange); + elements.get(3).value().matches("\\d+\\.\\d+")); + }; + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(requestUri)); + PathApiVersionResolver resolver = new PathApiVersionResolver(1, versionPathPredicate); + String actual = resolver.resolveVersion(exchange); assertThat(actual).isEqualTo(expected); } 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 c2a562ef1fc..b7bff5573c5 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 @@ -110,16 +110,17 @@ public class ApiVersionConfigurer { } /** - * Add a resolver that extracts the API version from a path segment - * and that allows to include only certain paths based on the provided {@link Predicate}. - *

Note that this resolver never returns {@code null}, and therefore - * cannot yield to other resolvers, see {@link org.springframework.web.accept.PathApiVersionResolver}. + * Variant of {@link #usePathSegment(int)} with a {@code Predicate} + * to determine whether a given path is versioned, providing additional + * flexibility, and the option to resolve the version to {@code null}. * @param index the index of the path segment to check; e.g. for URL's like * {@code "/{version}/..."} use index 0, for {@code "/api/{version}/..."} index 1. - * @param includePath a {@link Predicate} that allows to include a certain path + * @param versionPathPredicate used to decide if a path is versioned (true) + * or not (false). + * @since 7.0.6 */ - public ApiVersionConfigurer usePathSegment(int index, Predicate includePath) { - this.versionResolvers.add(new PathApiVersionResolver(index, includePath)); + public ApiVersionConfigurer usePathSegment(int index, Predicate versionPathPredicate) { + this.versionResolvers.add(new PathApiVersionResolver(index, versionPathPredicate)); return this; }