Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -37,6 +41,7 @@
public class PathApiVersionResolver implements ApiVersionResolver {

private final int pathSegmentIndex;
private @Nullable Predicate<RequestPath> includePath;


/**
Expand All @@ -49,13 +54,25 @@ public PathApiVersionResolver(int pathSegmentIndex) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -41,6 +44,59 @@ void insufficientPathSegments() {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,11 +35,13 @@
* 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;


/**
Expand All @@ -47,11 +54,25 @@ public PathApiVersionResolver(int pathSegmentIndex) {
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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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;
Expand Down Expand Up @@ -108,6 +109,20 @@ public ApiVersionConfigurer usePathSegment(int index) {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,6 +32,7 @@
/**
* Unit tests for {@link org.springframework.web.accept.PathApiVersionResolver}.
* @author Rossen Stoyanchev
* @author Martin Mois
*/
public class PathApiVersionResolverTests {

Expand All @@ -43,10 +47,49 @@ void insufficientPathSegments() {
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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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;
Expand Down Expand Up @@ -108,6 +109,20 @@ public ApiVersionConfigurer usePathSegment(int index) {
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
Expand Down