Skip to content

Commit

Permalink
Introduce LookupPath in WebFlux request routing
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bclozel committed Jun 1, 2017
1 parent 0557404 commit cf1bc81
Show file tree
Hide file tree
Showing 18 changed files with 335 additions and 183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
import org.springframework.util.PathMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.support.HttpRequestPathHelper;
import org.springframework.web.util.pattern.ParsingPathMatcher;
import org.springframework.web.server.support.LookupPath;

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

private PathMatcher pathMatcher = new ParsingPathMatcher();

private HttpRequestPathHelper pathHelper = new HttpRequestPathHelper();


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

/**
* Set if context path and request URI should be URL-decoded. Both are returned
* <i>undecoded</i> by the Servlet API, in contrast to the servlet path.
* <p>Uses either the request encoding or the default encoding according
* to the Servlet spec (ISO-8859-1).
* @see HttpRequestPathHelper#setUrlDecode
*/
public void setUrlDecode(boolean urlDecode) {
this.pathHelper.setUrlDecode(urlDecode);
}

/**
* Set the UrlPathHelper to use for resolution of lookup paths.
* <p>Use this to override the default UrlPathHelper with a custom subclass.
*/
public void setHttpRequestPathHelper(HttpRequestPathHelper pathHelper) {
Assert.notNull(pathHelper, "HttpRequestPathHelper must not be null");
this.pathHelper = pathHelper;
}

/**
* Set CORS configuration based on URL patterns.
*/
Expand All @@ -102,7 +80,7 @@ public void registerCorsConfiguration(String path, CorsConfiguration config) {

@Override
public CorsConfiguration getCorsConfiguration(ServerWebExchange exchange) {
String lookupPath = this.pathHelper.getLookupPathForRequest(exchange);
String lookupPath = exchange.<LookupPath>getAttribute(LookupPath.LOOKUP_PATH_ATTRIBUTE).get().getPath();
for (Map.Entry<String, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
if (this.pathMatcher.match(entry.getKey(), lookupPath)) {
return entry.getValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,14 @@ public boolean shouldUrlDecode() {
}


public String getLookupPathForRequest(ServerWebExchange exchange) {
public LookupPath getLookupPathForRequest(ServerWebExchange exchange) {
String path = getPathWithinApplication(exchange.getRequest());
return (shouldUrlDecode() ? decode(exchange, path) : path);
path = (shouldUrlDecode() ? decode(exchange, path) : path);
int begin = path.lastIndexOf('/') + 1;
int end = path.length();
int paramIndex = path.indexOf(';', begin);
int extIndex = path.lastIndexOf('.', paramIndex != -1 ? paramIndex : end);
return new LookupPath(path, extIndex, paramIndex);
}

private String getPathWithinApplication(ServerHttpRequest request) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.server.support;

import org.springframework.lang.Nullable;
import org.springframework.web.server.ServerWebExchange;

/**
* Lookup path information of an incoming HTTP request.
*
* @author Brian Clozel
* @since 5.0
* @see HttpRequestPathHelper
*/
public final class LookupPath {

public static final String LOOKUP_PATH_ATTRIBUTE = LookupPath.class.getName();

private final String path;

private final int fileExtensionIndex;

private final int pathParametersIndex;

public LookupPath(String path, int fileExtensionIndex, int pathParametersIndex) {
this.path = path;
this.fileExtensionIndex = fileExtensionIndex;
this.pathParametersIndex = pathParametersIndex;
}

public String getPath() {
if (this.pathParametersIndex != -1) {
// TODO: variant without the path parameter information?
//return this.path.substring(0, this.pathParametersIndex);
return this.path;
}
else {
return this.path;
}
}

public String getPathWithoutExtension() {
if (this.fileExtensionIndex != -1) {
return this.path.substring(0, this.fileExtensionIndex);
}
else {
return this.path;
}
}

@Nullable
public String getFileExtension() {
if (this.fileExtensionIndex == -1) {
return null;
}
else if (this.pathParametersIndex == -1) {
return this.path.substring(this.fileExtensionIndex);
}
else {
return this.path.substring(this.fileExtensionIndex, this.pathParametersIndex);
}
}

@Nullable
public String getPathParameters() {
return this.pathParametersIndex == -1 ?
null : this.path.substring(this.pathParametersIndex + 1);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.support.HttpRequestPathHelper;
import org.springframework.web.server.support.LookupPath;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
Expand All @@ -39,6 +41,7 @@ public class UrlBasedCorsConfigurationSourceTests {
@Test
public void empty() {
ServerWebExchange exchange = MockServerHttpRequest.get("/bar/test.html").toExchange();
setLookupPathAttribute(exchange);
assertNull(this.configSource.getCorsConfiguration(exchange));
}

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

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

exchange = MockServerHttpRequest.get("/bar/test.html").toExchange();
setLookupPathAttribute(exchange);
assertEquals(config, this.configSource.getCorsConfiguration(exchange));
}

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

public void setLookupPathAttribute(ServerWebExchange exchange) {
HttpRequestPathHelper helper = new HttpRequestPathHelper();
exchange.getAttributes().put(LookupPath.LOOKUP_PATH_ATTRIBUTE,
helper.getLookupPathForRequest(exchange));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.server.support;

import org.junit.Test;

import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;

import static org.junit.Assert.assertEquals;

/**
* Unit tests for {@link LookupPath}
* @author Brian Clozel
*/
public class LookupPathTests {

@Test
public void parsePath() {
LookupPath path = create("/foo");
assertEquals("/foo", path.getPath());
assertEquals("/foo", path.getPathWithoutExtension());
}

@Test
public void parsePathWithExtension() {
LookupPath path = create("/foo.txt");
assertEquals("/foo.txt", path.getPath());
assertEquals("/foo", path.getPathWithoutExtension());
assertEquals(".txt", path.getFileExtension());
}

@Test
public void parsePathWithParams() {
LookupPath path = create("/test/foo.txt;foo=bar?framework=spring");
assertEquals("/test/foo.txt;foo=bar", path.getPath());
assertEquals("/test/foo", path.getPathWithoutExtension());
assertEquals(".txt", path.getFileExtension());
assertEquals("foo=bar", path.getPathParameters());
}

private LookupPath create(String path) {
HttpRequestPathHelper helper = new HttpRequestPathHelper();
ServerWebExchange exchange = MockServerHttpRequest.get(path).build().toExchange();
return helper.getLookupPathForRequest(exchange);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.springframework.web.reactive.handler;

import java.util.Map;
import java.util.Optional;

import reactor.core.publisher.Mono;

Expand All @@ -32,6 +33,7 @@
import org.springframework.web.cors.reactive.DefaultCorsProcessor;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.server.support.LookupPath;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.support.HttpRequestPathHelper;
Expand All @@ -43,6 +45,7 @@
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Brian Clozel
* @since 5.0
*/
public abstract class AbstractHandlerMapping extends ApplicationObjectSupport implements HandlerMapping, Ordered {
Expand Down Expand Up @@ -171,6 +174,19 @@ public Mono<Object> getHandler(ServerWebExchange exchange) {
});
}

protected LookupPath getLookupPath(ServerWebExchange exchange) {
Optional<LookupPath> attribute = exchange.getAttribute(LookupPath.LOOKUP_PATH_ATTRIBUTE);
return attribute.orElseGet(() -> {
LookupPath lookupPath = createLookupPath(exchange);
exchange.getAttributes().put(LookupPath.LOOKUP_PATH_ATTRIBUTE, lookupPath);
return lookupPath;
});
}

protected LookupPath createLookupPath(ServerWebExchange exchange) {
return getPathHelper().getLookupPathForRequest(exchange);
}

/**
* Look up a handler for the given request, returning an empty {@code Mono}
* if no specific one is found. This method is called by {@link #getHandler}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.springframework.beans.BeansException;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.server.support.LookupPath;
import org.springframework.web.server.ServerWebExchange;

/**
Expand All @@ -46,6 +47,7 @@
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Brian Clozel
* @since 5.0
*/
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
Expand Down Expand Up @@ -99,7 +101,7 @@ public final Map<String, Object> getHandlerMap() {

@Override
public Mono<Object> getHandlerInternal(ServerWebExchange exchange) {
String lookupPath = getPathHelper().getLookupPathForRequest(exchange);
LookupPath lookupPath = getLookupPath(exchange);
Object handler;
try {
handler = lookupHandler(lookupPath, exchange);
Expand All @@ -109,30 +111,31 @@ public Mono<Object> getHandlerInternal(ServerWebExchange exchange) {
}

if (handler != null && logger.isDebugEnabled()) {
logger.debug("Mapping [" + lookupPath + "] to " + handler);
logger.debug("Mapping [" + lookupPath.getPath() + "] to " + handler);
}
else if (handler == null && logger.isTraceEnabled()) {
logger.trace("No handler mapping found for [" + lookupPath + "]");
logger.trace("No handler mapping found for [" + lookupPath.getPath() + "]");
}

return Mono.justOrEmpty(handler);
}

/**
* Look up a handler instance for the given URL path.
* Look up a handler instance for the given URL lookup path.
*
* <p>Supports direct matches, e.g. a registered "/test" matches "/test",
* and various Ant-style pattern matches, e.g. a registered "/t*" matches
* both "/test" and "/team". For details, see the AntPathMatcher class.
* <p>Looks for the most exact pattern, where most exact is defined as
* the longest path pattern.
* @param urlPath URL the bean is mapped to
* and various path pattern matches, e.g. a registered "/t*" matches
* both "/test" and "/team". For details, see the PathPattern class.
*
* @param lookupPath URL the handler is mapped to
* @param exchange the current exchange
* @return the associated handler instance, or {@code null} if not found
* @see org.springframework.web.util.pattern.ParsingPathMatcher
*/
@Nullable
protected Object lookupHandler(String urlPath, ServerWebExchange exchange) throws Exception {
protected Object lookupHandler(LookupPath lookupPath, ServerWebExchange exchange) throws Exception {
// Direct match?
String urlPath = lookupPath.getPath();
Object handler = this.handlerMap.get(urlPath);
if (handler != null) {
return handleMatch(handler, urlPath, urlPath, exchange);
Expand All @@ -156,7 +159,7 @@ else if (useTrailingSlashMatch()) {
if (!matches.isEmpty()) {
Collections.sort(matches, comparator);
if (logger.isDebugEnabled()) {
logger.debug("Matching patterns for request [" + urlPath + "] are " + matches);
logger.debug("Matching patterns for request [" + lookupPath + "] are " + matches);
}
bestMatch = matches.get(0);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.support.HttpRequestPathHelper;
import org.springframework.web.server.support.LookupPath;
import org.springframework.web.util.pattern.ParsingPathMatcher;

/**
Expand Down Expand Up @@ -184,8 +185,8 @@ public final Mono<String> getForRequestUrl(ServerWebExchange exchange, String re
private int getLookupPathIndex(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
String requestPath = request.getURI().getPath();
String lookupPath = getPathHelper().getLookupPathForRequest(exchange);
return requestPath.indexOf(lookupPath);
LookupPath lookupPath = getPathHelper().getLookupPathForRequest(exchange);
return requestPath.indexOf(lookupPath.getPath());
}

private int getEndPathIndex(String lookupPath) {
Expand Down
Loading

0 comments on commit cf1bc81

Please sign in to comment.