Browse Source

Add usePathSegment with Predicate<RequestPath>

See gh-36398

Signed-off-by: Martin Mois <martin.mois@gmail.com>
pull/36429/head
Martin Mois 3 weeks ago committed by rstoyanchev
parent
commit
348482b5a0
  1. 19
      spring-web/src/main/java/org/springframework/web/accept/PathApiVersionResolver.java
  2. 56
      spring-web/src/test/java/org/springframework/web/accept/PathApiVersionResolverTests.java
  3. 25
      spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java
  4. 15
      spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java
  5. 45
      spring-webflux/src/test/java/org/springframework/web/reactive/accept/PathApiVersionResolverTests.java
  6. 15
      spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java

19
spring-web/src/main/java/org/springframework/web/accept/PathApiVersionResolver.java

@ -16,13 +16,17 @@ @@ -16,13 +16,17 @@
package org.springframework.web.accept;
import java.util.function.Predicate;
import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.http.server.PathContainer;
import org.springframework.http.server.RequestPath;
import org.springframework.util.Assert;
import org.springframework.web.util.ServletRequestPathUtils;
/**
* {@link ApiVersionResolver} that extract the version from a path segment.
*
@ -37,6 +41,7 @@ import org.springframework.web.util.ServletRequestPathUtils; @@ -37,6 +41,7 @@ import org.springframework.web.util.ServletRequestPathUtils;
public class PathApiVersionResolver implements ApiVersionResolver {
private final int pathSegmentIndex;
private @Nullable Predicate<RequestPath> includePath;
/**
@ -49,13 +54,25 @@ public class PathApiVersionResolver implements ApiVersionResolver { @@ -49,13 +54,25 @@ public class PathApiVersionResolver implements ApiVersionResolver {
this.pathSegmentIndex = pathSegmentIndex;
}
/**
* 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
*/
public PathApiVersionResolver(int pathSegmentIndex, Predicate<RequestPath> includePath) {
this(pathSegmentIndex);
this.includePath = includePath;
}
@Override
public String resolveVersion(HttpServletRequest request) {
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)) {
return null;
}
int i = 0;
for (PathContainer.Element element : path.pathWithinApplication().elements()) {
if (element instanceof PathContainer.PathSegment && i++ == this.pathSegmentIndex) {

56
spring-web/src/test/java/org/springframework/web/accept/PathApiVersionResolverTests.java

@ -16,8 +16,11 @@ @@ -16,8 +16,11 @@
package org.springframework.web.accept;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.http.server.PathContainer;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.util.ServletRequestPathUtils;
@ -41,6 +44,59 @@ public class PathApiVersionResolverTests { @@ -41,6 +44,59 @@ public class PathApiVersionResolverTests {
assertThatThrownBy(() -> testResolve(0, "/", "1.0")).isInstanceOf(InvalidApiVersionException.class);
}
@Test
void includePathFalse() {
String requestUri = "/v3/api-docs";
testResolveWithIncludePath(requestUri, 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) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
try {
ServletRequestPathUtils.parseAndCache(request);
String actual = new PathApiVersionResolver(1, requestPath -> {
List<PathContainer.Element> 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);
}
finally {
ServletRequestPathUtils.clearParsedRequestPath(request);
}
}
private static void testResolve(int index, String requestUri, String expected) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
try {

25
spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java

@ -16,7 +16,12 @@ @@ -16,7 +16,12 @@
package org.springframework.web.reactive.accept;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.springframework.http.server.PathContainer;
import org.springframework.http.server.RequestPath;
import org.springframework.util.Assert;
import org.springframework.web.accept.InvalidApiVersionException;
import org.springframework.web.server.ServerWebExchange;
@ -30,11 +35,13 @@ import org.springframework.web.server.ServerWebExchange; @@ -30,11 +35,13 @@ import org.springframework.web.server.ServerWebExchange;
* cannot yield to other resolvers.
*
* @author Rossen Stoyanchev
* @author Martin Mois
* @since 7.0
*/
public class PathApiVersionResolver implements ApiVersionResolver {
private final int pathSegmentIndex;
private @Nullable Predicate<RequestPath> includePath = null;
/**
@ -47,11 +54,25 @@ public class PathApiVersionResolver implements ApiVersionResolver { @@ -47,11 +54,25 @@ public class PathApiVersionResolver implements ApiVersionResolver {
this.pathSegmentIndex = pathSegmentIndex;
}
/**
* 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
*/
public PathApiVersionResolver(int pathSegmentIndex, Predicate<RequestPath> includePath) {
this(pathSegmentIndex);
this.includePath = includePath;
}
@Override
public String resolveVersion(ServerWebExchange exchange) {
public @Nullable String resolveVersion(ServerWebExchange exchange) {
int i = 0;
for (PathContainer.Element e : exchange.getRequest().getPath().pathWithinApplication().elements()) {
RequestPath path = exchange.getRequest().getPath();
if (this.includePath != null && !this.includePath.test(path)) {
return null;
}
for (PathContainer.Element e : path.pathWithinApplication().elements()) {
if (e instanceof PathContainer.PathSegment && i++ == this.pathSegmentIndex) {
return e.value();
}

15
spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java

@ -27,6 +27,7 @@ import java.util.function.Predicate; @@ -27,6 +27,7 @@ import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.http.server.RequestPath;
import org.springframework.util.Assert;
import org.springframework.web.accept.ApiVersionParser;
import org.springframework.web.accept.InvalidApiVersionException;
@ -108,6 +109,20 @@ public class ApiVersionConfigurer { @@ -108,6 +109,20 @@ public class ApiVersionConfigurer {
return this;
}
/**
* 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}.
* <p>Note that this resolver never returns {@code null}, and therefore
* cannot yield to other resolvers, see {@link org.springframework.web.accept.PathApiVersionResolver}.
* @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
*/
public ApiVersionConfigurer usePathSegment(int index, Predicate<RequestPath> includePath) {
this.versionResolvers.add(new PathApiVersionResolver(index, includePath));
return this;
}
/**
* Add custom resolvers to resolve the API version.
* @param resolvers the resolvers to use

45
spring-webflux/src/test/java/org/springframework/web/reactive/accept/PathApiVersionResolverTests.java

@ -16,8 +16,11 @@ @@ -16,8 +16,11 @@
package org.springframework.web.reactive.accept;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.http.server.PathContainer;
import org.springframework.web.accept.InvalidApiVersionException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
@ -29,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -29,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link org.springframework.web.accept.PathApiVersionResolver}.
* @author Rossen Stoyanchev
* @author Martin Mois
*/
public class PathApiVersionResolverTests {
@ -43,10 +47,49 @@ public class PathApiVersionResolverTests { @@ -43,10 +47,49 @@ public class PathApiVersionResolverTests {
assertThatThrownBy(() -> testResolve(0, "/", "1.0")).isInstanceOf(InvalidApiVersionException.class);
}
@Test
void includePathFalse() {
String requestUri = "/v3/api-docs";
testResolveWithIncludePath(requestUri, 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<PathContainer.Element> 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(exchange);
assertThat(actual).isEqualTo(expected);
}
private static void testResolve(int index, String requestUri, String expected) {
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(requestUri));
String actual = new PathApiVersionResolver(index).resolveVersion(exchange);
assertThat(actual).isEqualTo(expected);
}
}

15
spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java

@ -27,6 +27,7 @@ import java.util.function.Predicate; @@ -27,6 +27,7 @@ import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.http.server.RequestPath;
import org.springframework.util.Assert;
import org.springframework.web.accept.ApiVersionDeprecationHandler;
import org.springframework.web.accept.ApiVersionParser;
@ -108,6 +109,20 @@ public class ApiVersionConfigurer { @@ -108,6 +109,20 @@ public class ApiVersionConfigurer {
return this;
}
/**
* 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}.
* <p>Note that this resolver never returns {@code null}, and therefore
* cannot yield to other resolvers, see {@link org.springframework.web.accept.PathApiVersionResolver}.
* @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
*/
public ApiVersionConfigurer usePathSegment(int index, Predicate<RequestPath> includePath) {
this.versionResolvers.add(new PathApiVersionResolver(index, includePath));
return this;
}
/**
* Add custom resolvers to resolve the API version.
* @param resolvers the resolvers to use

Loading…
Cancel
Save