Support for parsed PathPatterns in Spring MVC

See gh-24945
This commit is contained in:
Rossen Stoyanchev
2020-06-15 11:20:32 +01:00
parent a0f4d81db7
commit 5225a57411
82 changed files with 5324 additions and 3011 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -16,7 +16,6 @@
package org.springframework.http.server;
import java.net.URI;
import java.util.List;
import org.springframework.lang.Nullable;
@@ -37,8 +36,8 @@ class DefaultRequestPath implements RequestPath {
private final PathContainer pathWithinApplication;
DefaultRequestPath(URI uri, @Nullable String contextPath) {
this.fullPath = PathContainer.parsePath(uri.getRawPath());
DefaultRequestPath(String rawPath, @Nullable String contextPath) {
this.fullPath = PathContainer.parsePath(rawPath);
this.contextPath = initContextPath(this.fullPath, contextPath);
this.pathWithinApplication = extractPathWithinApplication(this.fullPath, this.contextPath);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2020 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.
@@ -20,7 +20,11 @@ import java.net.URI;
import org.springframework.lang.Nullable;
/**
* Represents the complete path for a request.
* Specialization of {@link PathContainer} that sub-divides the path into a
* {@link #contextPath()} and the remaining {@link #pathWithinApplication()}.
* The lattery is typically used for request mapping within the application
* while the former is useful when preparing external links that point back to
* the application.
*
* @author Rossen Stoyanchev
* @since 5.0
@@ -40,7 +44,8 @@ public interface RequestPath extends PathContainer {
PathContainer contextPath();
/**
* The portion of the request path after the context path.
* The portion of the request path after the context path which is typically
* used for request mapping within the application .
*/
PathContainer pathWithinApplication();
@@ -54,10 +59,23 @@ public interface RequestPath extends PathContainer {
/**
* Create a new {@code RequestPath} with the given parameters.
* Parse the URI for a request into a {@code RequestPath}.
* @param uri the URI of the request
* @param contextPath the contextPath portion of the URI path
*/
static RequestPath parse(URI uri, @Nullable String contextPath) {
return new DefaultRequestPath(uri, contextPath);
return parse(uri.getRawPath(), contextPath);
}
/**
* Variant of {@link #parse(URI, String)} with the encoded
* {@link URI#getRawPath() raw path}.
* @param rawPath the path
* @param contextPath the contextPath portion of the URI path
* @since 5.3
*/
static RequestPath parse(String rawPath, @Nullable String contextPath) {
return new DefaultRequestPath(rawPath, contextPath);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -17,87 +17,131 @@
package org.springframework.web.cors;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.server.PathContainer;
import org.springframework.lang.Nullable;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.util.ServletRequestPathUtils;
import org.springframework.web.util.UrlPathHelper;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* Provide a per request {@link CorsConfiguration} instance based on a
* collection of {@link CorsConfiguration} mapped on path patterns.
* {@code CorsConfigurationSource} that uses URL path patterns to select the
* {@code CorsConfiguration} for a request.
*
* <p>Exact path mapping URIs (such as {@code "/admin"}) are supported
* as well as Ant-style path patterns (such as {@code "/admin/**"}).
* <p>Pattern matching can be done with a {@link PathMatcher} or with pre-parsed
* {@link PathPattern}s. The syntax is largely the same with the latter being more
* tailored for web usage and more efficient. The choice depends on the presence of a
* {@link UrlPathHelper#resolveAndCacheLookupPath resolved} String lookupPath or a
* {@link ServletRequestPathUtils#parseAndCache parsed} {@code RequestPath}
* with a fallback on {@link PathMatcher} but the fallback can be disabled.
* For more details, please see {@link #setAllowInitLookupPath(boolean)}.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @since 4.2
* @see PathPattern
* @see AntPathMatcher
*/
public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource {
private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
private static PathMatcher defaultPathMatcher = new AntPathMatcher();
private PathMatcher pathMatcher = new AntPathMatcher();
private UrlPathHelper urlPathHelper = new UrlPathHelper();
private final PathPatternParser patternParser;
private UrlPathHelper urlPathHelper = UrlPathHelper.defaultInstance;
private PathMatcher pathMatcher = defaultPathMatcher;
@Nullable
private String lookupPathAttributeName;
private boolean allowInitLookupPath = true;
private final Map<PathPattern, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
/**
* Set the PathMatcher implementation to use for matching URL paths
* against registered URL patterns. Default is AntPathMatcher.
* @see org.springframework.util.AntPathMatcher
* Default constructor with {@link PathPatternParser#defaultInstance}.
*/
public void setPathMatcher(PathMatcher pathMatcher) {
Assert.notNull(pathMatcher, "PathMatcher must not be null");
this.pathMatcher = pathMatcher;
public UrlBasedCorsConfigurationSource() {
this(PathPatternParser.defaultInstance);
}
/**
* Shortcut to same property on underlying {@link #setUrlPathHelper UrlPathHelper}.
* @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
* Constructor with a {@link PathPatternParser} to parse patterns with.
* @param parser the parser to use
* @since 5.3
*/
public UrlBasedCorsConfigurationSource(PathPatternParser parser) {
Assert.notNull(parser, "PathPatternParser must not be null");
this.patternParser = parser;
}
/**
* Shortcut to the
* {@link org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
* same property} on the configured {@code UrlPathHelper}.
* @deprecated as of 5.3 in favor of using
* {@link #setUrlPathHelper(UrlPathHelper)}, if at all. For further details,
* please see {@link #setAllowInitLookupPath(boolean)}.
*/
@Deprecated
public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
initUrlPathHelper();
this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
}
/**
* Shortcut to same property on underlying {@link #setUrlPathHelper UrlPathHelper}.
* @see org.springframework.web.util.UrlPathHelper#setUrlDecode
* Shortcut to the
* {@link org.springframework.web.util.UrlPathHelper#setUrlDecode same property}
* on the configured {@code UrlPathHelper}.
* @deprecated as of 5.3 in favor of using
* {@link #setUrlPathHelper(UrlPathHelper)}, if at all. For further details,
* please see {@link #setAllowInitLookupPath(boolean)}.
*/
@Deprecated
public void setUrlDecode(boolean urlDecode) {
initUrlPathHelper();
this.urlPathHelper.setUrlDecode(urlDecode);
}
/**
* Optionally configure the name of the attribute that caches the lookupPath.
* This is used to make the call to
* {@link UrlPathHelper#getLookupPathForRequest(HttpServletRequest, String)}
* @param lookupPathAttributeName the request attribute to check
* @since 5.2
*/
public void setLookupPathAttributeName(@Nullable String lookupPathAttributeName) {
this.lookupPathAttributeName = lookupPathAttributeName;
}
/**
* Shortcut to same property on underlying {@link #setUrlPathHelper UrlPathHelper}.
* @see org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean)
* Shortcut to the
* {@link org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent
* same property} on the configured {@code UrlPathHelper}.
* @deprecated as of 5.3 in favor of using
* {@link #setUrlPathHelper(UrlPathHelper)}, if at all. For further details,
* please see {@link #setAllowInitLookupPath(boolean)}.
*/
@Deprecated
public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
initUrlPathHelper();
this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent);
}
private void initUrlPathHelper() {
if (this.urlPathHelper == UrlPathHelper.defaultInstance) {
this.urlPathHelper = new UrlPathHelper();
}
}
/**
* Set the UrlPathHelper to use for resolution of lookup paths.
* <p>Use this to override the default UrlPathHelper with a custom subclass.
* Configure the {@code UrlPathHelper} to resolve the lookupPath. This may
* not be necessary if the lookupPath is expected to be pre-resolved or if
* parsed {@code PathPatterns} are used instead.
* For further details on that, see {@link #setAllowInitLookupPath(boolean)}.
* <p>By default this is {@link UrlPathHelper#defaultInstance}.
*/
public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
@@ -105,40 +149,127 @@ public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource
}
/**
* Set CORS configuration based on URL patterns.
* When enabled, if there is neither a
* {@link UrlPathHelper#resolveAndCacheLookupPath esolved} String lookupPath nor a
* {@link ServletRequestPathUtils#parseAndCache parsed} {@code RequestPath}
* then use the {@link #setUrlPathHelper configured} {@code UrlPathHelper}
* to resolve a String lookupPath. This in turn determines use of URL
* pattern matching with {@link PathMatcher} or with parsed {@link PathPattern}s.
* <p>In Spring MVC, either a resolved String lookupPath or a parsed
* {@code RequestPath} is always available within {@code DispatcherServlet}
* processing. However in a Servlet {@code Filter} such as {@code CorsFilter}
* that may or may not be the case.
* <p>By default this is set to {@code true} in which case lazy lookupPath
* initialization is allowed. Set this to {@code false} when an
* application is using parsed {@code PathPatterns} in which case the
* {@code RequestPath} can be parsed earlier via
* {@link org.springframework.web.filter.ServletRequestPathFilter
* ServletRequestPathFilter}.
* @param allowInitLookupPath whether to disable lazy initialization
* and fail if not already resolved
* @since 5.3
*/
public void setAllowInitLookupPath(boolean allowInitLookupPath) {
this.allowInitLookupPath = allowInitLookupPath;
}
/**
* Configure the name of the attribute that holds the lookupPath extracted
* via {@link UrlPathHelper#getLookupPathForRequest(HttpServletRequest)}.
* <p>By default this is {@link UrlPathHelper#PATH_ATTRIBUTE}.
* @param name the request attribute to check
* @since 5.2
* @deprecated as of 5.3 in favor of {@link UrlPathHelper#PATH_ATTRIBUTE}.
*/
@Deprecated
public void setLookupPathAttributeName(String name) {
this.lookupPathAttributeName = name;
}
/**
* Configure a {@code PathMatcher} to use for pattern matching.
* <p>This is an advanced property that should be used only when a
* customized {@link AntPathMatcher} or a custom PathMatcher is required.
* <p>By default this is {@link AntPathMatcher}.
* <p><strong>Note:</strong> Setting {@code PathMatcher} enforces use of
* String pattern matching even when a
* {@link ServletRequestPathUtils#parseAndCache parsed} {@code RequestPath}
* is available.
*/
public void setPathMatcher(PathMatcher pathMatcher) {
this.pathMatcher = pathMatcher;
}
/**
* Set the CORS configuration mappings.
* <p>For pattern syntax see {@link AntPathMatcher} and {@link PathPattern}
* as well as class-level Javadoc for details on which one may in use.
* Generally the syntax is largely the same with {@link PathPattern} more
* tailored for web usage.
* @param corsConfigurations the mappings to use
* @see PathPattern
* @see AntPathMatcher
*/
public void setCorsConfigurations(@Nullable Map<String, CorsConfiguration> corsConfigurations) {
this.corsConfigurations.clear();
if (corsConfigurations != null) {
this.corsConfigurations.putAll(corsConfigurations);
corsConfigurations.forEach(this::registerCorsConfiguration);
}
}
/**
* Get the CORS configuration.
* Variant of {@link #setCorsConfigurations(Map)} to register one mapping at a time.
* @param pattern the mapping pattern
* @param config the CORS configuration to use for the pattern
* @see PathPattern
* @see AntPathMatcher
*/
public Map<String, CorsConfiguration> getCorsConfigurations() {
return Collections.unmodifiableMap(this.corsConfigurations);
public void registerCorsConfiguration(String pattern, CorsConfiguration config) {
this.corsConfigurations.put(this.patternParser.parse(pattern), config);
}
/**
* Register a {@link CorsConfiguration} for the specified path pattern.
* Return all configured CORS mappings.
*/
public void registerCorsConfiguration(String path, CorsConfiguration config) {
this.corsConfigurations.put(path, config);
public Map<String, CorsConfiguration> getCorsConfigurations() {
Map<String, CorsConfiguration> result = new HashMap<>(this.corsConfigurations.size());
this.corsConfigurations.forEach((pattern, config) -> result.put(pattern.getPatternString(), config));
return Collections.unmodifiableMap(result);
}
@Override
@Nullable
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, this.lookupPathAttributeName);
for (Map.Entry<String, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
if (this.pathMatcher.match(entry.getKey(), lookupPath)) {
Object path = resolvePath(request);
boolean isPathContainer = (path instanceof PathContainer);
for (Map.Entry<PathPattern, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
if (match(path, isPathContainer, entry.getKey())) {
return entry.getValue();
}
}
return null;
}
@SuppressWarnings("deprecation")
private Object resolvePath(HttpServletRequest request) {
if (this.allowInitLookupPath && !ServletRequestPathUtils.hasCachedPath(request)) {
return (this.lookupPathAttributeName != null ?
this.urlPathHelper.getLookupPathForRequest(request, this.lookupPathAttributeName) :
this.urlPathHelper.getLookupPathForRequest(request));
}
Object lookupPath = ServletRequestPathUtils.getCachedPath(request);
if (this.pathMatcher != defaultPathMatcher) {
lookupPath = lookupPath.toString();
}
return lookupPath;
}
private boolean match(Object path, boolean isPathContainer, PathPattern pattern) {
return (isPathContainer ?
pattern.matches((PathContainer) path) :
this.pathMatcher.match(pattern.getPatternString(), (String) path));
}
}

View File

@@ -27,22 +27,20 @@ import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* Provide a per reactive request {@link CorsConfiguration} instance based on a
* collection of {@link CorsConfiguration} mapped on path patterns.
*
* <p>Exact path mapping URIs (such as {@code "/admin"}) are supported
* as well as Ant-style path patterns (such as {@code "/admin/**"}).
* {@code CorsConfigurationSource} that uses URL patterns to select the
* {@code CorsConfiguration} for a request.
*
* @author Sebastien Deleuze
* @author Brian Clozel
* @since 5.0
* @see PathPattern
*/
public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource {
private final Map<PathPattern, CorsConfiguration> corsConfigurations;
private final PathPatternParser patternParser;
private final Map<PathPattern, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
/**
* Construct a new {@code UrlBasedCorsConfigurationSource} instance with default
@@ -58,7 +56,6 @@ public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource
* {@code PathPatternParser}.
*/
public UrlBasedCorsConfigurationSource(PathPatternParser patternParser) {
this.corsConfigurations = new LinkedHashMap<>();
this.patternParser = patternParser;
}
@@ -66,10 +63,10 @@ public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource
/**
* Set CORS configuration based on URL patterns.
*/
public void setCorsConfigurations(@Nullable Map<String, CorsConfiguration> corsConfigurations) {
public void setCorsConfigurations(@Nullable Map<String, CorsConfiguration> configMap) {
this.corsConfigurations.clear();
if (corsConfigurations != null) {
corsConfigurations.forEach(this::registerCorsConfiguration);
if (configMap != null) {
configMap.forEach(this::registerCorsConfiguration);
}
}
@@ -83,12 +80,13 @@ public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource
@Override
@Nullable
public CorsConfiguration getCorsConfiguration(ServerWebExchange exchange) {
PathContainer lookupPath = exchange.getRequest().getPath().pathWithinApplication();
return this.corsConfigurations.entrySet().stream()
.filter(entry -> entry.getKey().matches(lookupPath))
.map(Map.Entry::getValue)
.findFirst()
.orElse(null);
PathContainer path = exchange.getRequest().getPath().pathWithinApplication();
for (Map.Entry<PathPattern, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
if (entry.getKey().matches(path)) {
return entry.getValue();
}
}
return null;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -32,24 +32,23 @@ import org.springframework.web.cors.DefaultCorsProcessor;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
/**
* {@link javax.servlet.Filter} that handles CORS preflight requests and intercepts
* CORS simple and actual requests thanks to a {@link CorsProcessor} implementation
* ({@link DefaultCorsProcessor} by default) in order to add the relevant CORS
* response headers (like {@code Access-Control-Allow-Origin}) using the provided
* {@link CorsConfigurationSource} (for example an {@link UrlBasedCorsConfigurationSource}
* instance.
* {@link javax.servlet.Filter} to handle CORS pre-flight requests and intercept
* CORS simple and actual requests with a {@link CorsProcessor}, and to update
* the response, e.g. with CORS response headers, based on the policy matched
* through the provided {@link CorsConfigurationSource}.
*
* <p>This is an alternative to Spring MVC Java config and XML namespace CORS configuration,
* useful for applications depending only on spring-web (not on spring-webmvc) or for
* security constraints requiring CORS checks to be performed at {@link javax.servlet.Filter}
* level.
* <p>This is an alternative to configuring CORS in the Spring MVC Java config
* and the Spring MVC XML namespace. It is useful for applications depending
* only on spring-web (not on spring-webmvc) or for security constraints that
* require CORS checks to be performed at {@link javax.servlet.Filter} level.
*
* <p>This filter could be used in conjunction with {@link DelegatingFilterProxy} in order
* to help with its initialization.
* <p>This filter could be used in conjunction with {@link DelegatingFilterProxy}
* in order to help with its initialization.
*
* @author Sebastien Deleuze
* @since 4.2
* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>
* @see UrlBasedCorsConfigurationSource
*/
public class CorsFilter extends OncePerRequestFilter {

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2002-2020 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
*
* https://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.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.util.ServletRequestPathUtils;
/**
* A {@code Filter} to {@link ServletRequestPathUtils#parseAndCache parse}
* and cache a {@link org.springframework.http.server.RequestPath} for further
* {@link ServletRequestPathUtils#getParsedRequestPath access} throughout the
* filter chain. This is useful when parsed
* {@link org.springframework.web.util.pattern.PathPattern}s are in use anywhere
* in an application instead of String pattern matching with
* {@link org.springframework.util.PathMatcher}.
* <p>Note that in Spring MVC, the {@code DispatcherServlet} will also parse and
* cache the {@code RequestPath} if it detects that parsed {@code PathPatterns}
* are enabled for any {@code HandlerMapping} but it will skip doing that if it
* finds the {@link ServletRequestPathUtils#PATH_ATTRIBUTE} already exists.
*
* @author Rossen Stoyanchev
* @since 5.3
*/
public class ServletRequestPathFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ServletRequestPathUtils.parseAndCache((HttpServletRequest) request);
try {
chain.doFilter(request, response);
}
finally {
ServletRequestPathUtils.clearParsedRequestPath(request);
}
}
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright 2002-2020 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
*
* https://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.util;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.server.PathContainer;
import org.springframework.http.server.RequestPath;
import org.springframework.util.Assert;
/**
* Utility class to parse the path of an {@link HttpServletRequest} to a
* {@link RequestPath} and cache it in a request attribute for further access.
* This can then be used for URL path matching with
* {@link org.springframework.web.util.pattern.PathPattern PathPattern}s.
*
* <p>Also includes helper methods to return either a previously
* {@link UrlPathHelper#resolveAndCacheLookupPath resolved} String lookupPath
* or a previously {@link #parseAndCache parsed} {@code RequestPath} depending
* on which is cached in request attributes.
*
* @author Rossen Stoyanchev
* @since 5.3
*/
public abstract class ServletRequestPathUtils {
/** Name of Servlet request attribute that holds the parsed {@link RequestPath}. */
public static final String PATH_ATTRIBUTE = ServletRequestPathUtils.class.getName() + ".path";
/**
* Parse the {@link HttpServletRequest#getRequestURI() requestURI} of the
* request and its {@code contextPath} to create a {@link RequestPath} and
* cache it in the request attribute {@link #PATH_ATTRIBUTE}.
*
* <p>This method ignores the {@link HttpServletRequest#getServletPath()
* servletPath} and the {@link HttpServletRequest#getPathInfo() pathInfo}.
* Therefore in case of a Servlet mapping by prefix, the
* {@link RequestPath#pathWithinApplication()} will always include the
* Servlet prefix.
*/
public static RequestPath parseAndCache(HttpServletRequest request) {
String requestUri = (String) request.getAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE);
requestUri = (requestUri != null ? requestUri : request.getRequestURI());
RequestPath requestPath = RequestPath.parse(requestUri, request.getContextPath());
request.setAttribute(PATH_ATTRIBUTE, requestPath);
return requestPath;
}
/**
* Return a {@link #parseAndCache previously} parsed and cached {@code RequestPath}.
* @throws IllegalArgumentException if not found
*/
public static RequestPath getParsedRequestPath(ServletRequest request) {
RequestPath path = (RequestPath) request.getAttribute(PATH_ATTRIBUTE);
Assert.notNull(path, "Expected parsed RequestPath in request attribute \"" + PATH_ATTRIBUTE + "\".");
return path;
}
/**
* Check for a {@link #parseAndCache previously} parsed and cached {@code RequestPath}.
*/
public static boolean hasParsedRequestPath(ServletRequest request) {
return (request.getAttribute(PATH_ATTRIBUTE) != null);
}
/**
* Remove the request attribute {@link #PATH_ATTRIBUTE} that holds a
* {@link #parseAndCache previously} parsed and cached {@code RequestPath}.
*/
public static void clearParsedRequestPath(ServletRequest request) {
request.removeAttribute(PATH_ATTRIBUTE);
}
// Methods to select either parsed RequestPath or resolved String lookupPath
/**
* Return the {@link UrlPathHelper#resolveAndCacheLookupPath pre-resolved}
* String lookupPath or the {@link #parseAndCache(HttpServletRequest)
* pre-parsed} {@code RequestPath}.
* <p>In Spring MVC, when at least one {@code HandlerMapping} has parsed
* {@code PathPatterns} enabled, the {@code DispatcherServlet} eagerly parses
* and caches the {@code RequestPath} and the same can be also done earlier with
* {@link org.springframework.web.filter.ServletRequestPathFilter
* ServletRequestPathFilter}. In other cases where {@code HandlerMapping}s
* use String pattern matching with {@code PathMatcher}, the String
* lookupPath is resolved separately by each {@code HandlerMapping}.
* @param request the current request
* @return a String lookupPath or a {@code RequestPath}
* @throws IllegalArgumentException if neither is available
*/
public static Object getCachedPath(ServletRequest request) {
// The RequestPath is pre-parsed if any HandlerMapping uses PathPatterns.
// The lookupPath is re-resolved or cleared per HandlerMapping.
// So check for lookupPath first.
String lookupPath = (String) request.getAttribute(UrlPathHelper.PATH_ATTRIBUTE);
if (lookupPath != null) {
return lookupPath;
}
RequestPath requestPath = (RequestPath) request.getAttribute(PATH_ATTRIBUTE);
if (requestPath != null) {
return requestPath.pathWithinApplication();
}
throw new IllegalArgumentException(
"Neither a pre-parsed RequestPath nor a pre-resolved String lookupPath is available.");
}
/**
* Variant of {@link #getCachedPath(ServletRequest)} that returns the path
* for request mapping as a String.
* <p>If the cached path is a {@link #parseAndCache(HttpServletRequest)
* pre-parsed} {@code RequestPath} then the returned String path value is
* encoded and with path parameters removed.
* <p>If the cached path is a {@link UrlPathHelper#resolveAndCacheLookupPath
* pre-resolved} String lookupPath, then the returned String path value
* depends on how {@link UrlPathHelper} that resolved is configured.
* @param request the current request
* @return the full request mapping path as a String
*/
public static String getCachedPathValue(ServletRequest request) {
Object path = getCachedPath(request);
if (path instanceof PathContainer) {
String value = ((PathContainer) path).value();
path = UrlPathHelper.defaultInstance.removeSemicolonContent(value);
}
return (String) path;
}
/**
* Check for a previously {@link UrlPathHelper#resolveAndCacheLookupPath
* resolved} String lookupPath or a previously {@link #parseAndCache parsed}
* {@code RequestPath}.
* @param request the current request
* @return whether a pre-resolved or pre-parsed path is available
*/
public static boolean hasCachedPath(ServletRequest request) {
return (request.getAttribute(PATH_ATTRIBUTE) != null ||
request.getAttribute(UrlPathHelper.PATH_ATTRIBUTE) != null);
}
}

View File

@@ -22,6 +22,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.MappingMatch;
@@ -29,6 +30,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -51,6 +53,13 @@ import org.springframework.util.StringUtils;
*/
public class UrlPathHelper {
/**
* Name of Servlet request attribute that holds a
* {@link #getLookupPathForRequest resolved} lookupPath.
* @since 5.3
*/
public static final String PATH_ATTRIBUTE = UrlPathHelper.class.getName() + ".path";
private static boolean isServlet4Present =
ClassUtils.isPresent("javax.servlet.http.HttpServletMapping",
UrlPathHelper.class.getClassLoader());
@@ -160,24 +169,52 @@ public class UrlPathHelper {
}
/**
* {@link #getLookupPathForRequest Resolve} the lookupPath and cache it in a
* a request attribute with the key {@link #PATH_ATTRIBUTE} for subsequent
* access via {@link #getResolvedLookupPath(ServletRequest)}.
* @param request the current request
* @return the resolved path
* @since 5.3
*/
public String resolveAndCacheLookupPath(HttpServletRequest request) {
String lookupPath = getLookupPathForRequest(request);
request.setAttribute(PATH_ATTRIBUTE, lookupPath);
return lookupPath;
}
/**
* Return a previously {@link #getLookupPathForRequest resolved} lookupPath.
* @param request the current request
* @return the previously resolved lookupPath
* @throws IllegalArgumentException if the not found
* @since 5.3
*/
public static String getResolvedLookupPath(ServletRequest request) {
String lookupPath = (String) request.getAttribute(PATH_ATTRIBUTE);
Assert.notNull(lookupPath, "Expected lookupPath in request attribute \"" + PATH_ATTRIBUTE + "\".");
return lookupPath;
}
/**
* Variant of {@link #getLookupPathForRequest(HttpServletRequest)} that
* automates checking for a previously computed lookupPath saved as a
* request attribute. The attribute is only used for lookup purposes.
* @param request current HTTP request
* @param lookupPathAttributeName the request attribute to check
* @param name the request attribute that holds the lookupPath
* @return the lookup path
* @since 5.2
* @see org.springframework.web.servlet.HandlerMapping#LOOKUP_PATH
* @deprecated as of 5.3 in favor of using
* {@link #resolveAndCacheLookupPath(HttpServletRequest)} and
* {@link #getResolvedLookupPath(ServletRequest)}.
*/
public String getLookupPathForRequest(HttpServletRequest request, @Nullable String lookupPathAttributeName) {
if (lookupPathAttributeName != null) {
String result = (String) request.getAttribute(lookupPathAttributeName);
if (result != null) {
return result;
}
@Deprecated
public String getLookupPathForRequest(HttpServletRequest request, @Nullable String name) {
String result = null;
if (name != null) {
result = (String) request.getAttribute(name);
}
return getLookupPathForRequest(request);
return (result != null ? result : getLookupPathForRequest(request));
}
/**