Skip to content

Commit cf1bc81

Browse files
committed
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
1 parent 0557404 commit cf1bc81

File tree

18 files changed

+335
-183
lines changed

18 files changed

+335
-183
lines changed

spring-web/src/main/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSource.java

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
import org.springframework.util.PathMatcher;
2525
import org.springframework.web.cors.CorsConfiguration;
2626
import org.springframework.web.server.ServerWebExchange;
27-
import org.springframework.web.server.support.HttpRequestPathHelper;
2827
import org.springframework.web.util.pattern.ParsingPathMatcher;
28+
import org.springframework.web.server.support.LookupPath;
2929

3030
/**
3131
* Provide a per reactive request {@link CorsConfiguration} instance based on a
@@ -43,8 +43,6 @@ public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource
4343

4444
private PathMatcher pathMatcher = new ParsingPathMatcher();
4545

46-
private HttpRequestPathHelper pathHelper = new HttpRequestPathHelper();
47-
4846

4947
/**
5048
* Set the PathMatcher implementation to use for matching URL paths
@@ -56,26 +54,6 @@ public void setPathMatcher(PathMatcher pathMatcher) {
5654
this.pathMatcher = pathMatcher;
5755
}
5856

59-
/**
60-
* Set if context path and request URI should be URL-decoded. Both are returned
61-
* <i>undecoded</i> by the Servlet API, in contrast to the servlet path.
62-
* <p>Uses either the request encoding or the default encoding according
63-
* to the Servlet spec (ISO-8859-1).
64-
* @see HttpRequestPathHelper#setUrlDecode
65-
*/
66-
public void setUrlDecode(boolean urlDecode) {
67-
this.pathHelper.setUrlDecode(urlDecode);
68-
}
69-
70-
/**
71-
* Set the UrlPathHelper to use for resolution of lookup paths.
72-
* <p>Use this to override the default UrlPathHelper with a custom subclass.
73-
*/
74-
public void setHttpRequestPathHelper(HttpRequestPathHelper pathHelper) {
75-
Assert.notNull(pathHelper, "HttpRequestPathHelper must not be null");
76-
this.pathHelper = pathHelper;
77-
}
78-
7957
/**
8058
* Set CORS configuration based on URL patterns.
8159
*/
@@ -102,7 +80,7 @@ public void registerCorsConfiguration(String path, CorsConfiguration config) {
10280

10381
@Override
10482
public CorsConfiguration getCorsConfiguration(ServerWebExchange exchange) {
105-
String lookupPath = this.pathHelper.getLookupPathForRequest(exchange);
83+
String lookupPath = exchange.<LookupPath>getAttribute(LookupPath.LOOKUP_PATH_ATTRIBUTE).get().getPath();
10684
for (Map.Entry<String, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
10785
if (this.pathMatcher.match(entry.getKey(), lookupPath)) {
10886
return entry.getValue();

spring-web/src/main/java/org/springframework/web/server/support/HttpRequestPathHelper.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,14 @@ public boolean shouldUrlDecode() {
5757
}
5858

5959

60-
public String getLookupPathForRequest(ServerWebExchange exchange) {
60+
public LookupPath getLookupPathForRequest(ServerWebExchange exchange) {
6161
String path = getPathWithinApplication(exchange.getRequest());
62-
return (shouldUrlDecode() ? decode(exchange, path) : path);
62+
path = (shouldUrlDecode() ? decode(exchange, path) : path);
63+
int begin = path.lastIndexOf('/') + 1;
64+
int end = path.length();
65+
int paramIndex = path.indexOf(';', begin);
66+
int extIndex = path.lastIndexOf('.', paramIndex != -1 ? paramIndex : end);
67+
return new LookupPath(path, extIndex, paramIndex);
6368
}
6469

6570
private String getPathWithinApplication(ServerHttpRequest request) {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.server.support;
18+
19+
import org.springframework.lang.Nullable;
20+
import org.springframework.web.server.ServerWebExchange;
21+
22+
/**
23+
* Lookup path information of an incoming HTTP request.
24+
*
25+
* @author Brian Clozel
26+
* @since 5.0
27+
* @see HttpRequestPathHelper
28+
*/
29+
public final class LookupPath {
30+
31+
public static final String LOOKUP_PATH_ATTRIBUTE = LookupPath.class.getName();
32+
33+
private final String path;
34+
35+
private final int fileExtensionIndex;
36+
37+
private final int pathParametersIndex;
38+
39+
public LookupPath(String path, int fileExtensionIndex, int pathParametersIndex) {
40+
this.path = path;
41+
this.fileExtensionIndex = fileExtensionIndex;
42+
this.pathParametersIndex = pathParametersIndex;
43+
}
44+
45+
public String getPath() {
46+
if (this.pathParametersIndex != -1) {
47+
// TODO: variant without the path parameter information?
48+
//return this.path.substring(0, this.pathParametersIndex);
49+
return this.path;
50+
}
51+
else {
52+
return this.path;
53+
}
54+
}
55+
56+
public String getPathWithoutExtension() {
57+
if (this.fileExtensionIndex != -1) {
58+
return this.path.substring(0, this.fileExtensionIndex);
59+
}
60+
else {
61+
return this.path;
62+
}
63+
}
64+
65+
@Nullable
66+
public String getFileExtension() {
67+
if (this.fileExtensionIndex == -1) {
68+
return null;
69+
}
70+
else if (this.pathParametersIndex == -1) {
71+
return this.path.substring(this.fileExtensionIndex);
72+
}
73+
else {
74+
return this.path.substring(this.fileExtensionIndex, this.pathParametersIndex);
75+
}
76+
}
77+
78+
@Nullable
79+
public String getPathParameters() {
80+
return this.pathParametersIndex == -1 ?
81+
null : this.path.substring(this.pathParametersIndex + 1);
82+
}
83+
84+
}

spring-web/src/test/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSourceTests.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
2222
import org.springframework.web.cors.CorsConfiguration;
2323
import org.springframework.web.server.ServerWebExchange;
24+
import org.springframework.web.server.support.HttpRequestPathHelper;
25+
import org.springframework.web.server.support.LookupPath;
2426

2527
import static org.junit.Assert.assertEquals;
2628
import static org.junit.Assert.assertNull;
@@ -39,6 +41,7 @@ public class UrlBasedCorsConfigurationSourceTests {
3941
@Test
4042
public void empty() {
4143
ServerWebExchange exchange = MockServerHttpRequest.get("/bar/test.html").toExchange();
44+
setLookupPathAttribute(exchange);
4245
assertNull(this.configSource.getCorsConfiguration(exchange));
4346
}
4447

@@ -48,9 +51,11 @@ public void registerAndMatch() {
4851
this.configSource.registerCorsConfiguration("/bar/**", config);
4952

5053
ServerWebExchange exchange = MockServerHttpRequest.get("/foo/test.html").toExchange();
54+
setLookupPathAttribute(exchange);
5155
assertNull(this.configSource.getCorsConfiguration(exchange));
5256

5357
exchange = MockServerHttpRequest.get("/bar/test.html").toExchange();
58+
setLookupPathAttribute(exchange);
5459
assertEquals(config, this.configSource.getCorsConfiguration(exchange));
5560
}
5661

@@ -59,4 +64,10 @@ public void unmodifiableConfigurationsMap() {
5964
this.configSource.getCorsConfigurations().put("/**", new CorsConfiguration());
6065
}
6166

67+
public void setLookupPathAttribute(ServerWebExchange exchange) {
68+
HttpRequestPathHelper helper = new HttpRequestPathHelper();
69+
exchange.getAttributes().put(LookupPath.LOOKUP_PATH_ATTRIBUTE,
70+
helper.getLookupPathForRequest(exchange));
71+
}
72+
6273
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.server.support;
18+
19+
import org.junit.Test;
20+
21+
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
22+
import org.springframework.web.server.ServerWebExchange;
23+
24+
import static org.junit.Assert.assertEquals;
25+
26+
/**
27+
* Unit tests for {@link LookupPath}
28+
* @author Brian Clozel
29+
*/
30+
public class LookupPathTests {
31+
32+
@Test
33+
public void parsePath() {
34+
LookupPath path = create("/foo");
35+
assertEquals("/foo", path.getPath());
36+
assertEquals("/foo", path.getPathWithoutExtension());
37+
}
38+
39+
@Test
40+
public void parsePathWithExtension() {
41+
LookupPath path = create("/foo.txt");
42+
assertEquals("/foo.txt", path.getPath());
43+
assertEquals("/foo", path.getPathWithoutExtension());
44+
assertEquals(".txt", path.getFileExtension());
45+
}
46+
47+
@Test
48+
public void parsePathWithParams() {
49+
LookupPath path = create("/test/foo.txt;foo=bar?framework=spring");
50+
assertEquals("/test/foo.txt;foo=bar", path.getPath());
51+
assertEquals("/test/foo", path.getPathWithoutExtension());
52+
assertEquals(".txt", path.getFileExtension());
53+
assertEquals("foo=bar", path.getPathParameters());
54+
}
55+
56+
private LookupPath create(String path) {
57+
HttpRequestPathHelper helper = new HttpRequestPathHelper();
58+
ServerWebExchange exchange = MockServerHttpRequest.get(path).build().toExchange();
59+
return helper.getLookupPathForRequest(exchange);
60+
}
61+
}

spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.web.reactive.handler;
1818

1919
import java.util.Map;
20+
import java.util.Optional;
2021

2122
import reactor.core.publisher.Mono;
2223

@@ -32,6 +33,7 @@
3233
import org.springframework.web.cors.reactive.DefaultCorsProcessor;
3334
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
3435
import org.springframework.web.reactive.HandlerMapping;
36+
import org.springframework.web.server.support.LookupPath;
3537
import org.springframework.web.server.ServerWebExchange;
3638
import org.springframework.web.server.WebHandler;
3739
import org.springframework.web.server.support.HttpRequestPathHelper;
@@ -43,6 +45,7 @@
4345
*
4446
* @author Rossen Stoyanchev
4547
* @author Juergen Hoeller
48+
* @author Brian Clozel
4649
* @since 5.0
4750
*/
4851
public abstract class AbstractHandlerMapping extends ApplicationObjectSupport implements HandlerMapping, Ordered {
@@ -171,6 +174,19 @@ public Mono<Object> getHandler(ServerWebExchange exchange) {
171174
});
172175
}
173176

177+
protected LookupPath getLookupPath(ServerWebExchange exchange) {
178+
Optional<LookupPath> attribute = exchange.getAttribute(LookupPath.LOOKUP_PATH_ATTRIBUTE);
179+
return attribute.orElseGet(() -> {
180+
LookupPath lookupPath = createLookupPath(exchange);
181+
exchange.getAttributes().put(LookupPath.LOOKUP_PATH_ATTRIBUTE, lookupPath);
182+
return lookupPath;
183+
});
184+
}
185+
186+
protected LookupPath createLookupPath(ServerWebExchange exchange) {
187+
return getPathHelper().getLookupPathForRequest(exchange);
188+
}
189+
174190
/**
175191
* Look up a handler for the given request, returning an empty {@code Mono}
176192
* if no specific one is found. This method is called by {@link #getHandler}.

spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.beans.BeansException;
2929
import org.springframework.lang.Nullable;
3030
import org.springframework.util.Assert;
31+
import org.springframework.web.server.support.LookupPath;
3132
import org.springframework.web.server.ServerWebExchange;
3233

3334
/**
@@ -46,6 +47,7 @@
4647
*
4748
* @author Rossen Stoyanchev
4849
* @author Juergen Hoeller
50+
* @author Brian Clozel
4951
* @since 5.0
5052
*/
5153
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
@@ -99,7 +101,7 @@ public final Map<String, Object> getHandlerMap() {
99101

100102
@Override
101103
public Mono<Object> getHandlerInternal(ServerWebExchange exchange) {
102-
String lookupPath = getPathHelper().getLookupPathForRequest(exchange);
104+
LookupPath lookupPath = getLookupPath(exchange);
103105
Object handler;
104106
try {
105107
handler = lookupHandler(lookupPath, exchange);
@@ -109,30 +111,31 @@ public Mono<Object> getHandlerInternal(ServerWebExchange exchange) {
109111
}
110112

111113
if (handler != null && logger.isDebugEnabled()) {
112-
logger.debug("Mapping [" + lookupPath + "] to " + handler);
114+
logger.debug("Mapping [" + lookupPath.getPath() + "] to " + handler);
113115
}
114116
else if (handler == null && logger.isTraceEnabled()) {
115-
logger.trace("No handler mapping found for [" + lookupPath + "]");
117+
logger.trace("No handler mapping found for [" + lookupPath.getPath() + "]");
116118
}
117119

118120
return Mono.justOrEmpty(handler);
119121
}
120122

121123
/**
122-
* Look up a handler instance for the given URL path.
124+
* Look up a handler instance for the given URL lookup path.
125+
*
123126
* <p>Supports direct matches, e.g. a registered "/test" matches "/test",
124-
* and various Ant-style pattern matches, e.g. a registered "/t*" matches
125-
* both "/test" and "/team". For details, see the AntPathMatcher class.
126-
* <p>Looks for the most exact pattern, where most exact is defined as
127-
* the longest path pattern.
128-
* @param urlPath URL the bean is mapped to
127+
* and various path pattern matches, e.g. a registered "/t*" matches
128+
* both "/test" and "/team". For details, see the PathPattern class.
129+
*
130+
* @param lookupPath URL the handler is mapped to
129131
* @param exchange the current exchange
130132
* @return the associated handler instance, or {@code null} if not found
131133
* @see org.springframework.web.util.pattern.ParsingPathMatcher
132134
*/
133135
@Nullable
134-
protected Object lookupHandler(String urlPath, ServerWebExchange exchange) throws Exception {
136+
protected Object lookupHandler(LookupPath lookupPath, ServerWebExchange exchange) throws Exception {
135137
// Direct match?
138+
String urlPath = lookupPath.getPath();
136139
Object handler = this.handlerMap.get(urlPath);
137140
if (handler != null) {
138141
return handleMatch(handler, urlPath, urlPath, exchange);
@@ -156,7 +159,7 @@ else if (useTrailingSlashMatch()) {
156159
if (!matches.isEmpty()) {
157160
Collections.sort(matches, comparator);
158161
if (logger.isDebugEnabled()) {
159-
logger.debug("Matching patterns for request [" + urlPath + "] are " + matches);
162+
logger.debug("Matching patterns for request [" + lookupPath + "] are " + matches);
160163
}
161164
bestMatch = matches.get(0);
162165
}

spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceUrlProvider.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
3838
import org.springframework.web.server.ServerWebExchange;
3939
import org.springframework.web.server.support.HttpRequestPathHelper;
40+
import org.springframework.web.server.support.LookupPath;
4041
import org.springframework.web.util.pattern.ParsingPathMatcher;
4142

4243
/**
@@ -184,8 +185,8 @@ public final Mono<String> getForRequestUrl(ServerWebExchange exchange, String re
184185
private int getLookupPathIndex(ServerWebExchange exchange) {
185186
ServerHttpRequest request = exchange.getRequest();
186187
String requestPath = request.getURI().getPath();
187-
String lookupPath = getPathHelper().getLookupPathForRequest(exchange);
188-
return requestPath.indexOf(lookupPath);
188+
LookupPath lookupPath = getPathHelper().getLookupPathForRequest(exchange);
189+
return requestPath.indexOf(lookupPath.getPath());
189190
}
190191

191192
private int getEndPathIndex(String lookupPath) {

0 commit comments

Comments
 (0)