Introduce LookupPath in WebFlux request routing

This commit adds the `LookupPath` class that contains the full
request path relative to the web context; the application can
get from it various information, including the file extension
and path parameters (if any).

Since that operation is done multiple times for each request, this
object is stored as an attribute at the `ServerWebExchange` level.

Issue: SPR-15397
This commit is contained in:
Brian Clozel
2017-05-16 22:36:48 +02:00
parent 0557404715
commit cf1bc81199
18 changed files with 335 additions and 183 deletions

View File

@@ -22,6 +22,8 @@ import java.util.Set;
import org.junit.Test;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.web.server.support.HttpRequestPathHelper;
import org.springframework.web.server.support.LookupPath;
import org.springframework.web.server.ServerWebExchange;
import static org.junit.Assert.assertEquals;
@@ -80,7 +82,7 @@ public class PatternsRequestConditionTests {
@Test
public void matchDirectPath() throws Exception {
PatternsRequestCondition condition = new PatternsRequestCondition("/foo");
PatternsRequestCondition match = condition.getMatchingCondition(get("/foo").toExchange());
PatternsRequestCondition match = condition.getMatchingCondition(createExchange("/foo"));
assertNotNull(match);
}
@@ -88,7 +90,7 @@ public class PatternsRequestConditionTests {
@Test
public void matchPattern() throws Exception {
PatternsRequestCondition condition = new PatternsRequestCondition("/foo/*");
PatternsRequestCondition match = condition.getMatchingCondition(get("/foo/bar").toExchange());
PatternsRequestCondition match = condition.getMatchingCondition(createExchange("/foo/bar"));
assertNotNull(match);
}
@@ -96,7 +98,7 @@ public class PatternsRequestConditionTests {
@Test
public void matchSortPatterns() throws Exception {
PatternsRequestCondition condition = new PatternsRequestCondition("/*/*", "/foo/bar", "/foo/*");
PatternsRequestCondition match = condition.getMatchingCondition(get("/foo/bar").toExchange());
PatternsRequestCondition match = condition.getMatchingCondition(createExchange("/foo/bar"));
PatternsRequestCondition expected = new PatternsRequestCondition("/foo/bar", "/foo/*", "/*/*");
assertEquals(expected, match);
@@ -104,7 +106,7 @@ public class PatternsRequestConditionTests {
@Test
public void matchSuffixPattern() throws Exception {
ServerWebExchange exchange = get("/foo.html").toExchange();
ServerWebExchange exchange = createExchange("/foo.html");
PatternsRequestCondition condition = new PatternsRequestCondition("/{foo}");
PatternsRequestCondition match = condition.getMatchingCondition(exchange);
@@ -112,7 +114,7 @@ public class PatternsRequestConditionTests {
assertNotNull(match);
assertEquals("/{foo}.*", match.getPatterns().iterator().next());
condition = new PatternsRequestCondition(new String[] {"/{foo}"}, null, null, false, false, null);
condition = new PatternsRequestCondition(new String[] {"/{foo}"}, null,false, false, null);
match = condition.getMatchingCondition(exchange);
assertNotNull(match);
@@ -125,15 +127,15 @@ public class PatternsRequestConditionTests {
public void matchSuffixPatternUsingFileExtensions() throws Exception {
String[] patterns = new String[] {"/jobs/{jobName}"};
Set<String> extensions = Collections.singleton("json");
PatternsRequestCondition condition = new PatternsRequestCondition(patterns, null, null, true, false, extensions);
PatternsRequestCondition condition = new PatternsRequestCondition(patterns, null, true, false, extensions);
MockServerWebExchange exchange = get("/jobs/my.job").toExchange();
MockServerWebExchange exchange = createExchange("/jobs/my.job");
PatternsRequestCondition match = condition.getMatchingCondition(exchange);
assertNotNull(match);
assertEquals("/jobs/{jobName}", match.getPatterns().iterator().next());
exchange = get("/jobs/my.job.json").toExchange();
exchange = createExchange("/jobs/my.job.json");
match = condition.getMatchingCondition(exchange);
assertNotNull(match);
@@ -143,14 +145,14 @@ public class PatternsRequestConditionTests {
@Test
public void matchSuffixPatternUsingFileExtensions2() throws Exception {
PatternsRequestCondition condition1 = new PatternsRequestCondition(
new String[] {"/prefix"}, null, null, true, false, Collections.singleton("json"));
new String[] {"/prefix"}, null, true, false, Collections.singleton("json"));
PatternsRequestCondition condition2 = new PatternsRequestCondition(
new String[] {"/suffix"}, null, null, true, false, null);
new String[] {"/suffix"}, null, true, false, null);
PatternsRequestCondition combined = condition1.combine(condition2);
MockServerWebExchange exchange = get("/prefix/suffix.json").toExchange();
MockServerWebExchange exchange = createExchange("/prefix/suffix.json");
PatternsRequestCondition match = combined.getMatchingCondition(exchange);
assertNotNull(match);
@@ -158,7 +160,7 @@ public class PatternsRequestConditionTests {
@Test
public void matchTrailingSlash() throws Exception {
MockServerWebExchange exchange = get("/foo/").toExchange();
MockServerWebExchange exchange = createExchange("/foo/");
PatternsRequestCondition condition = new PatternsRequestCondition("/foo");
PatternsRequestCondition match = condition.getMatchingCondition(exchange);
@@ -166,14 +168,15 @@ public class PatternsRequestConditionTests {
assertNotNull(match);
assertEquals("Should match by default", "/foo/", match.getPatterns().iterator().next());
condition = new PatternsRequestCondition(new String[] {"/foo"}, null, null, false, true, null);
condition = new PatternsRequestCondition(new String[] {"/foo"}, null, false, true, null);
match = condition.getMatchingCondition(exchange);
assertNotNull(match);
assertEquals("Trailing slash should be insensitive to useSuffixPatternMatch settings (SPR-6164, SPR-5636)",
"/foo/", match.getPatterns().iterator().next());
condition = new PatternsRequestCondition(new String[] {"/foo"}, null, null, false, false, null);
exchange = createExchange("/foo/");
condition = new PatternsRequestCondition(new String[] {"/foo"}, null, false, false, null);
match = condition.getMatchingCondition(exchange);
assertNull(match);
@@ -182,7 +185,7 @@ public class PatternsRequestConditionTests {
@Test
public void matchPatternContainsExtension() throws Exception {
PatternsRequestCondition condition = new PatternsRequestCondition("/foo.jpg");
PatternsRequestCondition match = condition.getMatchingCondition(get("/foo.html").toExchange());
PatternsRequestCondition match = condition.getMatchingCondition(createExchange("/foo.html"));
assertNull(match);
}
@@ -192,7 +195,7 @@ public class PatternsRequestConditionTests {
PatternsRequestCondition c1 = new PatternsRequestCondition("/foo*");
PatternsRequestCondition c2 = new PatternsRequestCondition("/foo*");
assertEquals(0, c1.compareTo(c2, get("/foo").toExchange()));
assertEquals(0, c1.compareTo(c2, createExchange("/foo")));
}
@Test
@@ -200,12 +203,12 @@ public class PatternsRequestConditionTests {
PatternsRequestCondition c1 = new PatternsRequestCondition("/fo*");
PatternsRequestCondition c2 = new PatternsRequestCondition("/foo");
assertEquals(1, c1.compareTo(c2, get("/foo").toExchange()));
assertEquals(1, c1.compareTo(c2, createExchange("/foo")));
}
@Test
public void compareNumberOfMatchingPatterns() throws Exception {
ServerWebExchange exchange = get("/foo.html").toExchange();
ServerWebExchange exchange = createExchange("/foo.html");
PatternsRequestCondition c1 = new PatternsRequestCondition("/foo", "*.jpeg");
PatternsRequestCondition c2 = new PatternsRequestCondition("/foo", "*.html");
@@ -217,5 +220,11 @@ public class PatternsRequestConditionTests {
assertEquals(1, match1.compareTo(match2, exchange));
}
private MockServerWebExchange createExchange(String path) {
MockServerWebExchange exchange = get(path).toExchange();
HttpRequestPathHelper helper = new HttpRequestPathHelper();
exchange.getAttributes().put(LookupPath.LOOKUP_PATH_ATTRIBUTE, helper.getLookupPathForRequest(exchange));
return exchange;
}
}

View File

@@ -29,6 +29,8 @@ import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.server.support.HttpRequestPathHelper;
import org.springframework.web.server.support.LookupPath;
import org.springframework.web.server.ServerWebExchange;
import static java.util.Arrays.asList;
@@ -65,6 +67,7 @@ public class RequestMappingInfoTests {
@Test
public void matchPatternsCondition() {
MockServerWebExchange exchange = MockServerHttpRequest.get("/foo").toExchange();
setLookupPathAttribute(exchange);
RequestMappingInfo info = paths("/foo*", "/bar").build();
RequestMappingInfo expected = paths("/foo*").build();
@@ -80,6 +83,7 @@ public class RequestMappingInfoTests {
@Test
public void matchParamsCondition() {
ServerWebExchange exchange = MockServerHttpRequest.get("/foo?foo=bar").toExchange();
setLookupPathAttribute(exchange);
RequestMappingInfo info = paths("/foo").params("foo=bar").build();
RequestMappingInfo match = info.getMatchingCondition(exchange);
@@ -95,6 +99,7 @@ public class RequestMappingInfoTests {
@Test
public void matchHeadersCondition() {
ServerWebExchange exchange = MockServerHttpRequest.get("/foo").header("foo", "bar").toExchange();
setLookupPathAttribute(exchange);
RequestMappingInfo info = paths("/foo").headers("foo=bar").build();
RequestMappingInfo match = info.getMatchingCondition(exchange);
@@ -110,6 +115,7 @@ public class RequestMappingInfoTests {
@Test
public void matchConsumesCondition() {
ServerWebExchange exchange = MockServerHttpRequest.post("/foo").contentType(MediaType.TEXT_PLAIN).toExchange();
setLookupPathAttribute(exchange);
RequestMappingInfo info = paths("/foo").consumes("text/plain").build();
RequestMappingInfo match = info.getMatchingCondition(exchange);
@@ -125,6 +131,7 @@ public class RequestMappingInfoTests {
@Test
public void matchProducesCondition() {
ServerWebExchange exchange = MockServerHttpRequest.get("/foo").accept(MediaType.TEXT_PLAIN).toExchange();
setLookupPathAttribute(exchange);
RequestMappingInfo info = paths("/foo").produces("text/plain").build();
RequestMappingInfo match = info.getMatchingCondition(exchange);
@@ -140,7 +147,8 @@ public class RequestMappingInfoTests {
@Test
public void matchCustomCondition() {
ServerWebExchange exchange = MockServerHttpRequest.get("/foo?foo=bar").toExchange();
setLookupPathAttribute(exchange);
RequestMappingInfo info = paths("/foo").params("foo=bar").build();
RequestMappingInfo match = info.getMatchingCondition(exchange);
@@ -161,6 +169,7 @@ public class RequestMappingInfoTests {
RequestMappingInfo oneMethodOneParam = paths().methods(RequestMethod.GET).params("foo").build();
ServerWebExchange exchange = MockServerHttpRequest.get("/foo").toExchange();
setLookupPathAttribute(exchange);
Comparator<RequestMappingInfo> comparator = (info, otherInfo) -> info.compareTo(otherInfo, exchange);
List<RequestMappingInfo> list = asList(none, oneMethod, oneMethodOneParam);
@@ -270,4 +279,9 @@ public class RequestMappingInfoTests {
assertNull("Pre-flight should match the ACCESS_CONTROL_REQUEST_METHOD", match);
}
public void setLookupPathAttribute(ServerWebExchange exchange) {
HttpRequestPathHelper helper = new HttpRequestPathHelper();
exchange.getAttributes().put(LookupPath.LOOKUP_PATH_ATTRIBUTE, helper.getLookupPathForRequest(exchange));
}
}

View File

@@ -54,6 +54,7 @@ import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import org.springframework.web.server.support.HttpRequestPathHelper;
import org.springframework.web.server.support.LookupPath;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
@@ -72,11 +73,15 @@ public class RequestMappingInfoHandlerMappingTests {
private TestRequestMappingInfoHandlerMapping handlerMapping;
private HttpRequestPathHelper pathHelper;
@Before
public void setup() throws Exception {
this.handlerMapping = new TestRequestMappingInfoHandlerMapping();
this.handlerMapping.registerHandler(new TestController());
this.pathHelper = new HttpRequestPathHelper();
this.handlerMapping.setPathHelper(this.pathHelper);
}
@@ -208,8 +213,8 @@ public class RequestMappingInfoHandlerMappingTests {
@Test
@SuppressWarnings("unchecked")
public void handleMatchUriTemplateVariables() throws Exception {
String lookupPath = "/1/2";
ServerWebExchange exchange = get(lookupPath).toExchange();
ServerWebExchange exchange = get("/1/2").toExchange();
LookupPath lookupPath = this.pathHelper.getLookupPathForRequest(exchange);
RequestMappingInfo key = paths("/{path1}/{path2}").build();
this.handlerMapping.handleMatch(key, lookupPath, exchange);
@@ -228,11 +233,10 @@ public class RequestMappingInfoHandlerMappingTests {
URI url = URI.create("/group/a%2Fb");
ServerWebExchange exchange = MockServerHttpRequest.method(HttpMethod.GET, url).toExchange();
HttpRequestPathHelper pathHelper = new HttpRequestPathHelper();
pathHelper.setUrlDecode(false);
String lookupPath = pathHelper.getLookupPathForRequest(exchange);
this.handlerMapping.setPathHelper(pathHelper);
this.pathHelper.setUrlDecode(false);
LookupPath lookupPath = this.pathHelper.getLookupPathForRequest(exchange);
this.handlerMapping.setPathHelper(this.pathHelper);
this.handlerMapping.handleMatch(key, lookupPath, exchange);
String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
@@ -248,7 +252,8 @@ public class RequestMappingInfoHandlerMappingTests {
public void handleMatchBestMatchingPatternAttribute() throws Exception {
RequestMappingInfo key = paths("/{path1}/2", "/**").build();
ServerWebExchange exchange = get("/1/2").toExchange();
this.handlerMapping.handleMatch(key, "/1/2", exchange);
LookupPath lookupPath = this.pathHelper.getLookupPathForRequest(exchange);
this.handlerMapping.handleMatch(key, lookupPath, exchange);
assertEquals("/{path1}/2", exchange.getAttributes().get(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE));
}
@@ -257,8 +262,8 @@ public class RequestMappingInfoHandlerMappingTests {
public void handleMatchBestMatchingPatternAttributeNoPatternsDefined() throws Exception {
RequestMappingInfo key = paths().build();
ServerWebExchange exchange = get("/1/2").toExchange();
this.handlerMapping.handleMatch(key, "/1/2", exchange);
LookupPath lookupPath = this.pathHelper.getLookupPathForRequest(exchange);
this.handlerMapping.handleMatch(key, lookupPath, exchange);
assertEquals("/1/2", exchange.getAttributes().get(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE));
}
@@ -268,8 +273,8 @@ public class RequestMappingInfoHandlerMappingTests {
MultiValueMap<String, String> matrixVariables;
Map<String, String> uriVariables;
ServerWebExchange exchange = get("/").toExchange();
handleMatch(exchange, "/{cars}", "/cars;colors=red,blue,green;year=2012");
ServerWebExchange exchange = get("/cars;colors=red,blue,green;year=2012").toExchange();
handleMatch(exchange, "/{cars}");
matrixVariables = getMatrixVariables(exchange, "cars");
uriVariables = getUriTemplateVariables(exchange);
@@ -279,8 +284,8 @@ public class RequestMappingInfoHandlerMappingTests {
assertEquals("2012", matrixVariables.getFirst("year"));
assertEquals("cars", uriVariables.get("cars"));
exchange = get("/").toExchange();
handleMatch(exchange, "/{cars:[^;]+}{params}", "/cars;colors=red,blue,green;year=2012");
exchange = get("/cars;colors=red,blue,green;year=2012").toExchange();
handleMatch(exchange, "/{cars:[^;]+}{params}");
matrixVariables = getMatrixVariables(exchange, "params");
uriVariables = getUriTemplateVariables(exchange);
@@ -291,8 +296,8 @@ public class RequestMappingInfoHandlerMappingTests {
assertEquals("cars", uriVariables.get("cars"));
assertEquals(";colors=red,blue,green;year=2012", uriVariables.get("params"));
exchange = get("/").toExchange();
handleMatch(exchange, "/{cars:[^;]+}{params}", "/cars");
exchange = get("/cars").toExchange();
handleMatch(exchange, "/{cars:[^;]+}{params}");
matrixVariables = getMatrixVariables(exchange, "params");
uriVariables = getUriTemplateVariables(exchange);
@@ -308,8 +313,8 @@ public class RequestMappingInfoHandlerMappingTests {
urlPathHelper.setUrlDecode(false);
this.handlerMapping.setPathHelper(urlPathHelper);
ServerWebExchange exchange = get("/").toExchange();
handleMatch(exchange, "/path{filter}", "/path;mvar=a%2fb");
ServerWebExchange exchange = get("/path;mvar=a%2fb").toExchange();
handleMatch(exchange, "/path{filter}");
MultiValueMap<String, String> matrixVariables = getMatrixVariables(exchange, "filter");
Map<String, String> uriVariables = getUriTemplateVariables(exchange);
@@ -368,8 +373,9 @@ public class RequestMappingInfoHandlerMappingTests {
ex.getSupportedMediaTypes()));
}
private void handleMatch(ServerWebExchange exchange, String pattern, String lookupPath) {
private void handleMatch(ServerWebExchange exchange, String pattern) {
RequestMappingInfo info = paths(pattern).build();
LookupPath lookupPath = this.pathHelper.getLookupPathForRequest(exchange);
this.handlerMapping.handleMatch(info, lookupPath, exchange);
}
@@ -474,7 +480,6 @@ public class RequestMappingInfoHandlerMappingTests {
RequestMapping annot = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
if (annot != null) {
BuilderConfiguration options = new BuilderConfiguration();
options.setPathHelper(getPathHelper());
options.setPathMatcher(getPathMatcher());
options.setSuffixPatternMatch(true);
options.setTrailingSlashMatch(true);

View File

@@ -55,6 +55,8 @@ import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.support.HttpRequestPathHelper;
import org.springframework.web.server.support.LookupPath;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
@@ -217,14 +219,17 @@ public class ViewResolutionResultHandlerTests {
ViewResolutionResultHandler handler = resultHandler(new TestViewResolver("account"));
MockServerWebExchange exchange = get("/account").toExchange();
addLookupPathAttribute(exchange);
handler.handleResult(exchange, result).block(Duration.ofMillis(5000));
assertResponseBody(exchange, "account: {id=123}");
exchange = get("/account/").toExchange();
addLookupPathAttribute(exchange);
handler.handleResult(exchange, result).block(Duration.ofMillis(5000));
assertResponseBody(exchange, "account: {id=123}");
exchange = get("/account.123").toExchange();
addLookupPathAttribute(exchange);
handler.handleResult(exchange, result).block(Duration.ofMillis(5000));
assertResponseBody(exchange, "account: {id=123}");
}
@@ -251,7 +256,8 @@ public class ViewResolutionResultHandlerTests {
HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType, this.bindingContext);
MockServerWebExchange exchange = get("/account").accept(APPLICATION_JSON).toExchange();
addLookupPathAttribute(exchange);
TestView defaultView = new TestView("jsonView", APPLICATION_JSON);
resultHandler(Collections.singletonList(defaultView), new TestViewResolver("account"))
@@ -301,6 +307,11 @@ public class ViewResolutionResultHandlerTests {
assertEquals("/", response.getHeaders().getLocation().toString());
}
private void addLookupPathAttribute(ServerWebExchange exchange) {
HttpRequestPathHelper helper = new HttpRequestPathHelper();
exchange.getAttributes().put(LookupPath.LOOKUP_PATH_ATTRIBUTE, helper.getLookupPathForRequest(exchange));
}
private ViewResolutionResultHandler resultHandler(ViewResolver... resolvers) {
return resultHandler(Collections.emptyList(), resolvers);
@@ -322,6 +333,7 @@ public class ViewResolutionResultHandlerTests {
model.addAttribute("id", "123");
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.bindingContext);
MockServerWebExchange exchange = get(path).toExchange();
addLookupPathAttribute(exchange);
resultHandler(resolvers).handleResult(exchange, result).block(Duration.ofSeconds(5));
assertResponseBody(exchange, responseBody);
return exchange;