diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java index 554d7e85da..bd58ba219a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java @@ -68,6 +68,7 @@ import org.springframework.web.servlet.support.SessionFlashMapManager; import org.springframework.web.servlet.theme.FixedThemeResolver; import org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator; import org.springframework.web.servlet.view.InternalResourceViewResolver; +import org.springframework.web.util.pattern.PathPatternParser; /** * A {@code MockMvcBuilder} that accepts {@code @Controller} registrations @@ -126,6 +127,9 @@ public class StandaloneMockMvcBuilder extends AbstractMockMvcBuilderExact path mapping URIs (such as {@code "/admin"}) are supported - * as well as Ant-style path patterns (such as {@code "/admin/**"}). + *

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 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 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. - *

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)}. + *

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. + *

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. + *

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)}. + *

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. + *

This is an advanced property that should be used only when a + * customized {@link AntPathMatcher} or a custom PathMatcher is required. + *

By default this is {@link AntPathMatcher}. + *

Note: 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. + *

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 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 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 getCorsConfigurations() { + Map 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 entry : this.corsConfigurations.entrySet()) { - if (this.pathMatcher.match(entry.getKey(), lookupPath)) { + Object path = resolvePath(request); + boolean isPathContainer = (path instanceof PathContainer); + for (Map.Entry 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)); + } + } diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSource.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSource.java index 6a454c64dd..af85fde0aa 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSource.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSource.java @@ -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. - * - *

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 corsConfigurations; - private final PathPatternParser patternParser; + private final Map 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 corsConfigurations) { + public void setCorsConfigurations(@Nullable Map 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 entry : this.corsConfigurations.entrySet()) { + if (entry.getKey().matches(path)) { + return entry.getValue(); + } + } + return null; } } diff --git a/spring-web/src/main/java/org/springframework/web/filter/CorsFilter.java b/spring-web/src/main/java/org/springframework/web/filter/CorsFilter.java index 6ca57e102d..4d0826f22a 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/CorsFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/CorsFilter.java @@ -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}. * - *

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. + *

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. * - *

This filter could be used in conjunction with {@link DelegatingFilterProxy} in order - * to help with its initialization. + *

This filter could be used in conjunction with {@link DelegatingFilterProxy} + * in order to help with its initialization. * * @author Sebastien Deleuze * @since 4.2 * @see CORS W3C recommendation + * @see UrlBasedCorsConfigurationSource */ public class CorsFilter extends OncePerRequestFilter { diff --git a/spring-web/src/main/java/org/springframework/web/filter/ServletRequestPathFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ServletRequestPathFilter.java new file mode 100644 index 0000000000..d1cda60b04 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/filter/ServletRequestPathFilter.java @@ -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}. + *

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); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java b/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java new file mode 100644 index 0000000000..31722320aa --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java @@ -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. + * + *

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}. + * + *

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}. + *

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. + *

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. + *

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); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java index a784a6cfec..d0ee04bc72 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java @@ -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)); } /** diff --git a/spring-web/src/test/java/org/springframework/http/server/DefaultRequestPathTests.java b/spring-web/src/test/java/org/springframework/http/server/DefaultRequestPathTests.java index 941edb6e51..b5094d2cd2 100644 --- a/spring-web/src/test/java/org/springframework/http/server/DefaultRequestPathTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/DefaultRequestPathTests.java @@ -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. @@ -15,8 +15,6 @@ */ package org.springframework.http.server; -import java.net.URI; - import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -25,10 +23,10 @@ import static org.assertj.core.api.Assertions.assertThat; * Unit tests for {@link DefaultRequestPath}. * @author Rossen Stoyanchev */ -public class DefaultRequestPathTests { +class DefaultRequestPathTests { @Test - public void requestPath() throws Exception { + void requestPath() { // basic testRequestPath("/app/a/b/c", "/app", "/a/b/c"); @@ -52,8 +50,7 @@ public class DefaultRequestPathTests { private void testRequestPath(String fullPath, String contextPath, String pathWithinApplication) { - URI uri = URI.create("http://localhost:8080" + fullPath); - RequestPath requestPath = RequestPath.parse(uri, contextPath); + RequestPath requestPath = RequestPath.parse(fullPath, contextPath); Object expected = contextPath.equals("/") ? "" : contextPath; assertThat(requestPath.contextPath().value()).isEqualTo(expected); @@ -61,10 +58,9 @@ public class DefaultRequestPathTests { } @Test - public void updateRequestPath() throws Exception { + void updateRequestPath() { - URI uri = URI.create("http://localhost:8080/aA/bB/cC"); - RequestPath requestPath = RequestPath.parse(uri, null); + RequestPath requestPath = RequestPath.parse("/aA/bB/cC", null); assertThat(requestPath.contextPath().value()).isEqualTo(""); assertThat(requestPath.pathWithinApplication().value()).isEqualTo("/aA/bB/cC"); diff --git a/spring-web/src/test/java/org/springframework/web/cors/UrlBasedCorsConfigurationSourceTests.java b/spring-web/src/test/java/org/springframework/web/cors/UrlBasedCorsConfigurationSourceTests.java index f818b169dc..02e6779e49 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/UrlBasedCorsConfigurationSourceTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/UrlBasedCorsConfigurationSourceTests.java @@ -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. @@ -16,44 +16,96 @@ package org.springframework.web.cors; -import org.junit.jupiter.api.Test; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.http.HttpMethod; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.UrlPathHelper; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * Unit tests for {@link UrlBasedCorsConfigurationSource}. * @author Sebastien Deleuze + * @author Rossen Stoyanchev */ -public class UrlBasedCorsConfigurationSourceTests { +class UrlBasedCorsConfigurationSourceTests { - private final UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); - - @Test - public void empty() { - MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/bar/test.html"); - assertThat(this.configSource.getCorsConfiguration(request)).isNull(); + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @ParameterizedTest + @MethodSource("pathPatternsArguments") + @interface PathPatternsParameterizedTest { } - @Test - public void registerAndMatch() { + @SuppressWarnings("unused") + private static Stream> pathPatternsArguments() { + return Stream.of( + requestUri -> { + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + ServletRequestPathUtils.parseAndCache(request); + return request; + }, + requestUri -> { + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + UrlPathHelper.defaultInstance.getLookupPathForRequest(request); + return request; + } + ); + } + + + @PathPatternsParameterizedTest + void empty(Function requestFactory) { + CorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + assertThat(source.getCorsConfiguration(requestFactory.apply("/bar/test.html"))).isNull(); + } + + @PathPatternsParameterizedTest + void registerAndMatch(Function requestFactory) { CorsConfiguration config = new CorsConfiguration(); - this.configSource.registerCorsConfiguration("/bar/**", config); + UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); + configSource.registerCorsConfiguration("/bar/**", config); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo/test.html"); - assertThat(this.configSource.getCorsConfiguration(request)).isNull(); + MockHttpServletRequest request = requestFactory.apply("/foo/test.html"); + assertThat(configSource.getCorsConfiguration(request)).isNull(); - request.setRequestURI("/bar/test.html"); - assertThat(this.configSource.getCorsConfiguration(request)).isEqualTo(config); + request = requestFactory.apply("/bar/test.html"); + assertThat(configSource.getCorsConfiguration(request)).isEqualTo(config); } @Test - public void unmodifiableConfigurationsMap() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> - this.configSource.getCorsConfigurations().put("/**", new CorsConfiguration())); + void unmodifiableConfigurationsMap() { + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.getCorsConfigurations().put("/**", new CorsConfiguration()); + }); } + @Test + void allowInitLookupPath() { + CorsConfiguration config = new CorsConfiguration(); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + assertThat(source.getCorsConfiguration(request)) + .as("The path should be resolved lazily by default") + .isSameAs(config); + + source.setAllowInitLookupPath(false); + assertThatIllegalArgumentException().isThrownBy(() -> source.getCorsConfiguration(request)); + } } diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSourceTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSourceTests.java index 6aecd98a51..693764212f 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSourceTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSourceTests.java @@ -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. @@ -30,20 +30,20 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sebastien Deleuze * @author Rossen Stoyanchev */ -public class UrlBasedCorsConfigurationSourceTests { +class UrlBasedCorsConfigurationSourceTests { private final UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); @Test - public void empty() { + void empty() { MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/bar/test.html")); assertThat(this.configSource.getCorsConfiguration(exchange)).isNull(); } @Test - public void registerAndMatch() { + void registerAndMatch() { CorsConfiguration config = new CorsConfiguration(); this.configSource.registerCorsConfiguration("/bar/**", config); diff --git a/spring-web/src/test/java/org/springframework/web/filter/CorsFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/CorsFilterTests.java index 00db048c69..35a72e528c 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/CorsFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/CorsFilterTests.java @@ -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. @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; @@ -44,19 +45,24 @@ public class CorsFilterTests { private final CorsConfiguration config = new CorsConfiguration(); + @BeforeEach - public void setup() throws Exception { + void setup() { config.setAllowedOrigins(Arrays.asList("https://domain1.com", "https://domain2.com")); config.setAllowedMethods(Arrays.asList("GET", "POST")); config.setAllowedHeaders(Arrays.asList("header1", "header2")); config.setExposedHeaders(Arrays.asList("header3", "header4")); config.setMaxAge(123L); config.setAllowCredentials(false); - filter = new CorsFilter(r -> config); + + UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); + configSource.registerCorsConfiguration("/**", config); + + filter = new CorsFilter(configSource); } @Test - public void nonCorsRequest() throws ServletException, IOException { + void nonCorsRequest() throws ServletException, IOException { MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/test.html"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -69,7 +75,7 @@ public class CorsFilterTests { } @Test - public void sameOriginRequest() throws ServletException, IOException { + void sameOriginRequest() throws ServletException, IOException { MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "https://domain1.com/test.html"); request.addHeader(HttpHeaders.ORIGIN, "https://domain1.com"); @@ -86,7 +92,7 @@ public class CorsFilterTests { } @Test - public void validActualRequest() throws ServletException, IOException { + void validActualRequest() throws ServletException, IOException { MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/test.html"); request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); @@ -101,7 +107,7 @@ public class CorsFilterTests { } @Test - public void invalidActualRequest() throws ServletException, IOException { + void invalidActualRequest() throws ServletException, IOException { MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.DELETE.name(), "/test.html"); request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); @@ -115,7 +121,7 @@ public class CorsFilterTests { } @Test - public void validPreFlightRequest() throws ServletException, IOException { + void validPreFlightRequest() throws ServletException, IOException { MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.OPTIONS.name(), "/test.html"); request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); @@ -134,7 +140,7 @@ public class CorsFilterTests { } @Test - public void invalidPreFlightRequest() throws ServletException, IOException { + void invalidPreFlightRequest() throws ServletException, IOException { MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.OPTIONS.name(), "/test.html"); request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java index 10296b8d4b..be143298c1 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java @@ -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,11 +16,11 @@ package org.springframework.web.reactive.handler; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import reactor.core.publisher.Mono; @@ -107,15 +107,16 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { */ @Nullable protected Object lookupHandler(PathContainer lookupPath, ServerWebExchange exchange) throws Exception { - - List matches = this.handlerMap.keySet().stream() - .filter(key -> key.matches(lookupPath)) - .collect(Collectors.toList()); - - if (matches.isEmpty()) { + List matches = null; + for (PathPattern pattern : this.handlerMap.keySet()) { + if (pattern.matches(lookupPath)) { + matches = (matches != null ? matches : new ArrayList<>()); + matches.add(pattern); + } + } + if (matches == null) { return null; } - if (matches.size() > 1) { matches.sort(PathPattern.SPECIFICITY_COMPARATOR); if (logger.isTraceEnabled()) { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java index 38dbf17e41..ad59c77fff 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java @@ -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. @@ -43,7 +43,7 @@ public class SimpleUrlHandlerMappingTests { @Test @SuppressWarnings("resource") - public void handlerMappingJavaConfig() throws Exception { + void handlerMappingJavaConfig() { AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); wac.register(WebConfig.class); wac.refresh(); @@ -61,7 +61,7 @@ public class SimpleUrlHandlerMappingTests { @Test @SuppressWarnings("resource") - public void handlerMappingXmlConfig() throws Exception { + void handlerMappingXmlConfig() { ClassPathXmlApplicationContext wac = new ClassPathXmlApplicationContext("map.xml", getClass()); wac.refresh(); @@ -98,7 +98,7 @@ public class SimpleUrlHandlerMappingTests { testUrl("outofpattern*ye", null, handlerMapping, null); } - private void testUrl(String url, Object bean, HandlerMapping handlerMapping, String pathWithinMapping) { + void testUrl(String url, Object bean, HandlerMapping handlerMapping, String pathWithinMapping) { MockServerHttpRequest request = MockServerHttpRequest.method(HttpMethod.GET, URI.create(url)).build(); ServerWebExchange exchange = MockServerWebExchange.from(request); Object actual = handlerMapping.getHandler(exchange).block(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 7852deda6e..5dbed6bb2d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -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. @@ -40,6 +40,8 @@ import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -50,6 +52,7 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; import org.springframework.ui.context.ThemeSource; @@ -62,7 +65,9 @@ import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartResolver; +import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.util.NestedServletException; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.WebUtils; /** @@ -347,6 +352,8 @@ public class DispatcherServlet extends FrameworkServlet { @Nullable private List viewResolvers; + private boolean parseRequestPath; + /** * Create a new {@code DispatcherServlet} that will create its own internal web @@ -622,6 +629,27 @@ public class DispatcherServlet extends FrameworkServlet { "': using default strategies from DispatcherServlet.properties"); } } + + this.parseRequestPath = initRequestPathParsing(this.handlerMappings); + } + + private boolean initRequestPathParsing(List mappings) { + for (HandlerMapping mapping : mappings) { + if (mapping instanceof AbstractHandlerMapping) { + if (((AbstractHandlerMapping) mapping).getPatternParser() != null) { + return true; + } + } + if (AopUtils.isAopProxy(mapping)) { + Object target = AopProxyUtils.getSingletonTarget(mapping); + if (target instanceof AbstractHandlerMapping) { + if (((AbstractHandlerMapping) target).getPatternParser() != null) { + return true; + } + } + } + } + return false; } /** @@ -939,6 +967,11 @@ public class DispatcherServlet extends FrameworkServlet { request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager); } + RequestPath requestPath = null; + if (this.parseRequestPath && !ServletRequestPathUtils.hasParsedRequestPath(request)) { + requestPath = ServletRequestPathUtils.parseAndCache(request); + } + try { doDispatch(request, response); } @@ -949,6 +982,9 @@ public class DispatcherServlet extends FrameworkServlet { restoreAttributesAfterInclude(request, attributesSnapshot); } } + if (requestPath != null) { + ServletRequestPathUtils.clearParsedRequestPath(request); + } } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java index 18377f0cd9..3bcd3761b6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java @@ -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,6 +16,7 @@ package org.springframework.web.servlet; +import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import org.springframework.lang.Nullable; @@ -68,7 +69,13 @@ public interface HandlerMapping { * {@link org.springframework.web.util.UrlPathHelper} could be the full path * or without the context path, decoded or not, etc. * @since 5.2 + * @deprecated as of 5.3 in favor of + * {@link org.springframework.web.util.UrlPathHelper#PATH_ATTRIBUTE} and + * {@link org.springframework.web.util.ServletRequestPathUtils#PATH_ATTRIBUTE}. + * To access the cached path used for request mapping, use + * {@link org.springframework.web.util.ServletRequestPathUtils#getCachedPathValue(ServletRequest)}. */ + @Deprecated String LOOKUP_PATH = HandlerMapping.class.getName() + ".lookupPath"; /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/InterceptorRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/InterceptorRegistration.java index 0e39604ea8..ee758ceaf4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/InterceptorRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/InterceptorRegistration.java @@ -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. @@ -21,11 +21,14 @@ import java.util.Arrays; import java.util.List; import org.springframework.lang.Nullable; +import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.handler.MappedInterceptor; +import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.pattern.PathPattern; /** * Assists with the creation of a {@link MappedInterceptor}. @@ -38,9 +41,11 @@ public class InterceptorRegistration { private final HandlerInterceptor interceptor; - private final List includePatterns = new ArrayList<>(); + @Nullable + private List includePatterns; - private final List excludePatterns = new ArrayList<>(); + @Nullable + private List excludePatterns; @Nullable private PathMatcher pathMatcher; @@ -58,7 +63,11 @@ public class InterceptorRegistration { /** - * Add URL patterns to which the registered interceptor should apply to. + * Add patterns for URLs the interceptor should be included in. + *

For pattern syntax see {@link PathPattern} when parsed patterns + * are {@link PathMatchConfigurer#setPatternParser enabled} or + * {@link AntPathMatcher} otherwise. The syntax is largely the same with + * {@link PathPattern} more tailored for web usage and more efficient. */ public InterceptorRegistration addPathPatterns(String... patterns) { return addPathPatterns(Arrays.asList(patterns)); @@ -69,12 +78,18 @@ public class InterceptorRegistration { * @since 5.0.3 */ public InterceptorRegistration addPathPatterns(List patterns) { + this.includePatterns = (this.includePatterns != null ? + this.includePatterns : new ArrayList<>(patterns.size())); this.includePatterns.addAll(patterns); return this; } /** - * Add URL patterns to which the registered interceptor should not apply to. + * Add patterns for URLs the interceptor should be excluded from. + *

For pattern syntax see {@link PathPattern} when parsed patterns + * are {@link PathMatchConfigurer#setPatternParser enabled} or + * {@link AntPathMatcher} otherwise. The syntax is largely the same with + * {@link PathPattern} more tailored for web usage and more efficient. */ public InterceptorRegistration excludePathPatterns(String... patterns) { return excludePathPatterns(Arrays.asList(patterns)); @@ -85,15 +100,22 @@ public class InterceptorRegistration { * @since 5.0.3 */ public InterceptorRegistration excludePathPatterns(List patterns) { + this.excludePatterns = (this.excludePatterns != null ? + this.excludePatterns : new ArrayList<>(patterns.size())); this.excludePatterns.addAll(patterns); return this; } /** - * A PathMatcher implementation to use with this interceptor. This is an optional, - * advanced property required only if using custom PathMatcher implementations - * that support mapping metadata other than the Ant path patterns supported - * by default. + * Configure the PathMatcher to use to match URL paths with against include + * and exclude patterns. + *

This is an advanced property that should be used only when a + * customized {@link AntPathMatcher} or a custom PathMatcher is required. + *

By default this is {@link AntPathMatcher}. + *

Note: Setting {@code PathMatcher} enforces use of + * String pattern matching even when a + * {@link ServletRequestPathUtils#parseAndCache parsed} {@code RequestPath} + * is available. */ public InterceptorRegistration pathMatcher(PathMatcher pathMatcher) { this.pathMatcher = pathMatcher; @@ -121,16 +143,20 @@ public class InterceptorRegistration { * type is {@link MappedInterceptor}; otherwise {@link HandlerInterceptor}. */ protected Object getInterceptor() { - if (this.includePatterns.isEmpty() && this.excludePatterns.isEmpty()) { + + if (this.includePatterns == null && this.excludePatterns == null) { return this.interceptor; } - String[] include = StringUtils.toStringArray(this.includePatterns); - String[] exclude = StringUtils.toStringArray(this.excludePatterns); - MappedInterceptor mappedInterceptor = new MappedInterceptor(include, exclude, this.interceptor); + MappedInterceptor mappedInterceptor = new MappedInterceptor( + StringUtils.toStringArray(this.includePatterns), + StringUtils.toStringArray(this.excludePatterns), + this.interceptor); + if (this.pathMatcher != null) { mappedInterceptor.setPathMatcher(this.pathMatcher); } + return mappedInterceptor; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java index 0dd684e2aa..b72629e597 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java @@ -21,37 +21,42 @@ import java.util.Map; import java.util.function.Predicate; import org.springframework.lang.Nullable; +import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; +import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPatternParser; /** - * Helps with configuring HandlerMappings path matching options such as trailing - * slash match, suffix registration, path matcher and path helper. - * - *

Configured path matcher and path helper instances are shared for: + * Configure path matching options. The options are applied to the following: *

* * @author Brian Clozel * @since 4.0.3 - * @see RequestMappingHandlerMapping - * @see org.springframework.web.servlet.handler.SimpleUrlHandlerMapping */ public class PathMatchConfigurer { + @Nullable + private PathPatternParser patternParser; + + @Nullable + private Boolean trailingSlashMatch; + + @Nullable + private Map>> pathPrefixes; + @Nullable private Boolean suffixPatternMatch; @Nullable private Boolean registeredSuffixPatternMatch; - @Nullable - private Boolean trailingSlashMatch; - @Nullable private UrlPathHelper urlPathHelper; @@ -59,41 +64,24 @@ public class PathMatchConfigurer { private PathMatcher pathMatcher; @Nullable - private Map>> pathPrefixes; + private UrlPathHelper defaultUrlPathHelper; + + @Nullable + private PathMatcher defaultPathMatcher; /** - * Whether to use suffix pattern match (".*") when matching patterns to - * requests. If enabled a method mapped to "/users" also matches to "/users.*". - *

By default this is set to {@code true}. - * @see #registeredSuffixPatternMatch - * @deprecated as of 5.2.4. See class-level note in - * {@link RequestMappingHandlerMapping} on the deprecation of path extension - * config options. As there is no replacement for this method, in 5.2.x it is - * necessary to set it to {@code false}. In 5.3 the default changes to - * {@code false} and use of this property becomes unnecessary. + * Enable use of parsed {@link PathPattern}s as described in + * {@link AbstractHandlerMapping#setPatternParser(PathPatternParser)}. + *

Note: This is mutually exclusive with use of + * {@link #setUrlPathHelper(UrlPathHelper)} and + * {@link #setPathMatcher(PathMatcher)}. + *

By default this is not enabled. + * @param patternParser the parser to pre-parse patterns with + * @since 5.3 */ - @Deprecated - public PathMatchConfigurer setUseSuffixPatternMatch(Boolean suffixPatternMatch) { - this.suffixPatternMatch = suffixPatternMatch; - return this; - } - - /** - * Whether suffix pattern matching should work only against path extensions - * explicitly registered when you - * {@link WebMvcConfigurer#configureContentNegotiation configure content - * negotiation}. This is generally recommended to reduce ambiguity and to - * avoid issues such as when a "." appears in the path for other reasons. - *

By default this is set to "false". - * @see WebMvcConfigurer#configureContentNegotiation - * @deprecated as of 5.2.4. See class-level note in - * {@link RequestMappingHandlerMapping} on the deprecation of path extension - * config options. - */ - @Deprecated - public PathMatchConfigurer setUseRegisteredSuffixPatternMatch(Boolean registeredSuffixPatternMatch) { - this.registeredSuffixPatternMatch = registeredSuffixPatternMatch; + public PathMatchConfigurer setPatternParser(PathPatternParser patternParser) { + this.patternParser = patternParser; return this; } @@ -107,27 +95,6 @@ public class PathMatchConfigurer { return this; } - /** - * Set the UrlPathHelper to use for resolution of lookup paths. - *

Use this to override the default UrlPathHelper with a custom subclass, - * or to share common UrlPathHelper settings across multiple HandlerMappings - * and MethodNameResolvers. - */ - public PathMatchConfigurer setUrlPathHelper(UrlPathHelper urlPathHelper) { - this.urlPathHelper = urlPathHelper; - return this; - } - - /** - * Set the PathMatcher implementation to use for matching URL paths - * against registered URL patterns. Default is AntPathMatcher. - * @see org.springframework.util.AntPathMatcher - */ - public PathMatchConfigurer setPathMatcher(PathMatcher pathMatcher) { - this.pathMatcher = pathMatcher; - return this; - } - /** * Configure a path prefix to apply to matching controller methods. *

Prefixes are used to enrich the mappings of every {@code @RequestMapping} @@ -147,16 +114,83 @@ public class PathMatchConfigurer { return this; } + /** + * Whether to use suffix pattern match (".*") when matching patterns to + * requests. If enabled a method mapped to "/users" also matches to "/users.*". + *

By default this is set to {@code true}. + *

Note: This property is mutually exclusive with and + * ignored when {@link #setPatternParser(PathPatternParser)} is set. + * @deprecated as of 5.2.4. See class-level note in + * {@link RequestMappingHandlerMapping} on the deprecation of path extension + * config options. As there is no replacement for this method, in 5.2.x it is + * necessary to set it to {@code false}. In 5.3 the default changes to + * {@code false} and use of this property becomes unnecessary. + */ + @Deprecated + public PathMatchConfigurer setUseSuffixPatternMatch(Boolean suffixPatternMatch) { + this.suffixPatternMatch = suffixPatternMatch; + return this; + } /** - * Whether to use registered suffixes for pattern matching. - * @deprecated as of 5.2.4, see deprecation note on - * {@link #setUseSuffixPatternMatch(Boolean)}. + * Whether suffix pattern matching should work only against path extensions + * explicitly registered when you + * {@link WebMvcConfigurer#configureContentNegotiation configure content + * negotiation}. This is generally recommended to reduce ambiguity and to + * avoid issues such as when a "." appears in the path for other reasons. + *

By default this is set to "false". + *

Note: This property is mutually exclusive with and + * ignored when {@link #setPatternParser(PathPatternParser)} is set. + * @deprecated as of 5.2.4. See class-level note in + * {@link RequestMappingHandlerMapping} on the deprecation of path extension + * config options. + */ + @Deprecated + public PathMatchConfigurer setUseRegisteredSuffixPatternMatch(Boolean registeredSuffixPatternMatch) { + this.registeredSuffixPatternMatch = registeredSuffixPatternMatch; + return this; + } + + /** + * Set the UrlPathHelper to use to resolve the mapping path for the application. + *

Note: This property is mutually exclusive with and + * ignored when {@link #setPatternParser(PathPatternParser)} is set. + */ + public PathMatchConfigurer setUrlPathHelper(UrlPathHelper urlPathHelper) { + this.urlPathHelper = urlPathHelper; + return this; + } + + /** + * Set the PathMatcher to use for String pattern matching. + *

By default this is {@link AntPathMatcher}. + *

Note: This property is mutually exclusive with and + * ignored when {@link #setPatternParser(PathPatternParser)} is set. + */ + public PathMatchConfigurer setPathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + return this; + } + + + /** + * Return the {@link PathPatternParser} to use, if configured. + * @since 5.3 */ + @Nullable + public PathPatternParser getPatternParser() { + return this.patternParser; + } + @Nullable @Deprecated - public Boolean isUseSuffixPatternMatch() { - return this.suffixPatternMatch; + public Boolean isUseTrailingSlashMatch() { + return this.trailingSlashMatch; + } + + @Nullable + protected Map>> getPathPrefixes() { + return this.pathPrefixes; } /** @@ -170,9 +204,15 @@ public class PathMatchConfigurer { return this.registeredSuffixPatternMatch; } + /** + * Whether to use registered suffixes for pattern matching. + * @deprecated as of 5.2.4, see deprecation note on + * {@link #setUseSuffixPatternMatch(Boolean)}. + */ @Nullable - public Boolean isUseTrailingSlashMatch() { - return this.trailingSlashMatch; + @Deprecated + public Boolean isUseSuffixPatternMatch() { + return this.suffixPatternMatch; } @Nullable @@ -185,8 +225,32 @@ public class PathMatchConfigurer { return this.pathMatcher; } - @Nullable - protected Map>> getPathPrefixes() { - return this.pathPrefixes; + /** + * Return the configure UrlPathHelper instance or a default (shared) instance. + * @since 5.3 + */ + protected UrlPathHelper getUrlPathHelperOrDefault() { + if (this.urlPathHelper != null) { + return this.urlPathHelper; + } + if (this.defaultUrlPathHelper == null) { + this.defaultUrlPathHelper = new UrlPathHelper(); + } + return this.defaultUrlPathHelper; } + + /** + * Return the configure PathMatcher instance or a default (shared) instance. + * @since 5.3 + */ + protected PathMatcher getPathMatcherOrDefault() { + if (this.pathMatcher != null) { + return this.pathMatcher; + } + if (this.defaultPathMatcher == null) { + this.defaultPathMatcher = new AntPathMatcher(); + } + return this.defaultPathMatcher; + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java index 55a92e5a20..cd4645fd32 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java @@ -28,6 +28,7 @@ import org.springframework.beans.factory.BeanInitializationException; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; import org.springframework.lang.Nullable; +import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.web.HttpRequestHandler; import org.springframework.web.accept.ContentNegotiationManager; @@ -36,18 +37,24 @@ import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.pattern.PathPattern; /** - * Stores registrations of resource handlers for serving static resources such as images, css files and others - * through Spring MVC including setting cache headers optimized for efficient loading in a web browser. - * Resources can be served out of locations under web application root, from the classpath, and others. + * Stores registrations of resource handlers for serving static resources such + * as images, css files and others through Spring MVC including setting cache + * headers optimized for efficient loading in a web browser. Resources can be + * served out of locations under web application root, from the classpath, and + * others. * - *

To create a resource handler, use {@link #addResourceHandler(String...)} providing the URL path patterns - * for which the handler should be invoked to serve static resources (e.g. {@code "/resources/**"}). + *

To create a resource handler, use {@link #addResourceHandler(String...)} + * providing the URL path patterns for which the handler should be invoked to + * serve static resources (e.g. {@code "/resources/**"}). * - *

Then use additional methods on the returned {@link ResourceHandlerRegistration} to add one or more - * locations from which to serve static content from (e.g. {{@code "/"}, - * {@code "classpath:/META-INF/public-web-resources/"}}) or to specify a cache period for served resources. + *

Then use additional methods on the returned + * {@link ResourceHandlerRegistration} to add one or more locations from which + * to serve static content from (e.g. {{@code "/"}, + * {@code "classpath:/META-INF/public-web-resources/"}}) or to specify a cache + * period for served resources. * * @author Rossen Stoyanchev * @since 3.1 @@ -110,13 +117,14 @@ public class ResourceHandlerRegistry { /** - * Add a resource handler for serving static resources based on the specified URL path patterns. - * The handler will be invoked for every incoming request that matches to one of the specified - * path patterns. - *

Patterns like {@code "/static/**"} or {@code "/css/{filename:\\w+\\.css}"} are allowed. - * See {@link org.springframework.util.AntPathMatcher} for more details on the syntax. - * @return a {@link ResourceHandlerRegistration} to use to further configure the - * registered resource handler + * Add a resource handler to serve static resources. The handler is invoked + * for requests that match one of the specified URL path patterns. + *

Patterns such as {@code "/static/**"} or {@code "/css/{filename:\\w+\\.css}"} + * are supported. + *

For pattern syntax see {@link PathPattern} when parsed patterns + * are {@link PathMatchConfigurer#setPatternParser enabled} or + * {@link AntPathMatcher} otherwise. The syntax is largely the same with + * {@link PathPattern} more tailored for web usage and more efficient. */ public ResourceHandlerRegistration addResourceHandler(String... pathPatterns) { ResourceHandlerRegistration registration = new ResourceHandlerRegistration(pathPatterns); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewControllerRegistry.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewControllerRegistry.java index a367eed58c..3cedaf2144 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewControllerRegistry.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewControllerRegistry.java @@ -24,7 +24,9 @@ import java.util.Map; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.lang.Nullable; +import org.springframework.util.AntPathMatcher; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.util.pattern.PathPattern; /** * Assists with the registration of simple automated controllers pre-configured @@ -56,27 +58,34 @@ public class ViewControllerRegistry { /** - * Map a view controller to the given URL path (or pattern) in order to render - * a response with a pre-configured status code and view. - *

Patterns like {@code "/admin/**"} or {@code "/articles/{articlename:\\w+}"} - * are allowed. See {@link org.springframework.util.AntPathMatcher} for more details on the - * syntax. + * Map a URL path or pattern to a view controller to render a response with + * the configured status code and view. + *

Patterns such as {@code "/admin/**"} or {@code "/articles/{articlename:\\w+}"} + * are supported. For pattern syntax see {@link PathPattern} when parsed + * patterns are {@link PathMatchConfigurer#setPatternParser enabled} or + * {@link AntPathMatcher} otherwise. The syntax is largely the same with + * {@link PathPattern} more tailored for web usage and more efficient. *

Note: If an {@code @RequestMapping} method is mapped * to a URL for any HTTP method then a view controller cannot handle the * same URL. For this reason it is recommended to avoid splitting URL * handling across an annotated controller and a view controller. */ - public ViewControllerRegistration addViewController(String urlPath) { - ViewControllerRegistration registration = new ViewControllerRegistration(urlPath); + public ViewControllerRegistration addViewController(String urlPathOrPattern) { + ViewControllerRegistration registration = new ViewControllerRegistration(urlPathOrPattern); registration.setApplicationContext(this.applicationContext); this.registrations.add(registration); return registration; } /** - * Map a view controller to the given URL path (or pattern) in order to redirect - * to another URL. By default the redirect URL is expected to be relative to - * the current ServletContext, i.e. as relative to the web application root. + * Map a view controller to the given URL path or pattern in order to redirect + * to another URL. + *

For pattern syntax see {@link PathPattern} when parsed patterns + * are {@link PathMatchConfigurer#setPatternParser enabled} or + * {@link AntPathMatcher} otherwise. The syntax is largely the same with + * {@link PathPattern} more tailored for web usage and more efficient. + *

By default the redirect URL is expected to be relative to the current + * ServletContext, i.e. as relative to the web application root. * @since 4.1 */ public RedirectViewControllerRegistration addRedirectViewController(String urlPath, String redirectUrl) { @@ -89,6 +98,10 @@ public class ViewControllerRegistry { /** * Map a simple controller to the given URL path (or pattern) in order to * set the response status to the given code without rendering a body. + *

For pattern syntax see {@link PathPattern} when parsed patterns + * are {@link PathMatchConfigurer#setPatternParser enabled} or + * {@link AntPathMatcher} otherwise. The syntax is largely the same with + * {@link PathPattern} more tailored for web usage and more efficient. * @since 4.1 */ public void addStatusController(String urlPath, HttpStatus statusCode) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 1000fe12b7..89dddbb966 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -22,7 +22,6 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.function.Predicate; import javax.servlet.ServletContext; @@ -287,32 +286,29 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv mapping.setContentNegotiationManager(contentNegotiationManager); mapping.setCorsConfigurations(getCorsConfigurations()); - PathMatchConfigurer configurer = getPathMatchConfigurer(); + PathMatchConfigurer pathConfig = getPathMatchConfigurer(); + if (pathConfig.getPatternParser() != null) { + mapping.setPatternParser(pathConfig.getPatternParser()); + } + else { + mapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault()); + mapping.setPathMatcher(pathConfig.getPathMatcherOrDefault()); - Boolean useSuffixPatternMatch = configurer.isUseSuffixPatternMatch(); - if (useSuffixPatternMatch != null) { - mapping.setUseSuffixPatternMatch(useSuffixPatternMatch); + Boolean useSuffixPatternMatch = pathConfig.isUseSuffixPatternMatch(); + if (useSuffixPatternMatch != null) { + mapping.setUseSuffixPatternMatch(useSuffixPatternMatch); + } + Boolean useRegisteredSuffixPatternMatch = pathConfig.isUseRegisteredSuffixPatternMatch(); + if (useRegisteredSuffixPatternMatch != null) { + mapping.setUseRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch); + } } - Boolean useRegisteredSuffixPatternMatch = configurer.isUseRegisteredSuffixPatternMatch(); - if (useRegisteredSuffixPatternMatch != null) { - mapping.setUseRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch); - } - Boolean useTrailingSlashMatch = configurer.isUseTrailingSlashMatch(); + Boolean useTrailingSlashMatch = pathConfig.isUseTrailingSlashMatch(); if (useTrailingSlashMatch != null) { mapping.setUseTrailingSlashMatch(useTrailingSlashMatch); } - - UrlPathHelper pathHelper = configurer.getUrlPathHelper(); - if (pathHelper != null) { - mapping.setUrlPathHelper(pathHelper); - } - PathMatcher pathMatcher = configurer.getPathMatcher(); - if (pathMatcher != null) { - mapping.setPathMatcher(pathMatcher); - } - Map>> pathPrefixes = configurer.getPathPrefixes(); - if (pathPrefixes != null) { - mapping.setPathPrefixes(pathPrefixes); + if (pathConfig.getPathPrefixes() != null) { + mapping.setPathPrefixes(pathConfig.getPathPrefixes()); } return mapping; @@ -335,6 +331,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv protected final Object[] getInterceptors( FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { + if (this.interceptors == null) { InterceptorRegistry registry = new InterceptorRegistry(); addInterceptors(registry); @@ -374,30 +371,31 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv protected void configurePathMatch(PathMatchConfigurer configurer) { } - /** - * Return a global {@link PathMatcher} instance for path matching - * patterns in {@link HandlerMapping HandlerMappings}. - * This instance can be configured using the {@link PathMatchConfigurer} - * in {@link #configurePathMatch(PathMatchConfigurer)}. - * @since 4.1 - */ - @Bean - public PathMatcher mvcPathMatcher() { - PathMatcher pathMatcher = getPathMatchConfigurer().getPathMatcher(); - return (pathMatcher != null ? pathMatcher : new AntPathMatcher()); - } /** - * Return a global {@link UrlPathHelper} instance for path matching - * patterns in {@link HandlerMapping HandlerMappings}. - * This instance can be configured using the {@link PathMatchConfigurer} - * in {@link #configurePathMatch(PathMatchConfigurer)}. + * Return a global {@link UrlPathHelper} instance which is used to resolve + * the request mapping path for an application. The instance can be + * configured via {@link #configurePathMatch(PathMatchConfigurer)}. + *

Note: This is only used when parsed patterns are not + * {@link PathMatchConfigurer#setPatternParser enabled}. * @since 4.1 */ @Bean public UrlPathHelper mvcUrlPathHelper() { - UrlPathHelper pathHelper = getPathMatchConfigurer().getUrlPathHelper(); - return (pathHelper != null ? pathHelper : new UrlPathHelper()); + return getPathMatchConfigurer().getUrlPathHelperOrDefault(); + } + + /** + * Return a global {@link PathMatcher} instance which is used for URL path + * matching with String patterns. The returned instance can be configured + * using {@link #configurePathMatch(PathMatchConfigurer)}. + *

Note: This is only used when parsed patterns are not + * {@link PathMatchConfigurer#setPatternParser enabled}. + * @since 4.1 + */ + @Bean + public PathMatcher mvcPathMatcher() { + return getPathMatchConfigurer().getPathMatcherOrDefault(); } /** @@ -451,10 +449,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv @Bean @Nullable public HandlerMapping viewControllerHandlerMapping( - @Qualifier("mvcPathMatcher") PathMatcher pathMatcher, - @Qualifier("mvcUrlPathHelper") UrlPathHelper urlPathHelper, @Qualifier("mvcConversionService") FormattingConversionService conversionService, @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) { + ViewControllerRegistry registry = new ViewControllerRegistry(this.applicationContext); addViewControllers(registry); @@ -462,8 +459,14 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv if (handlerMapping == null) { return null; } - handlerMapping.setPathMatcher(pathMatcher); - handlerMapping.setUrlPathHelper(urlPathHelper); + PathMatchConfigurer pathConfig = getPathMatchConfigurer(); + if (pathConfig.getPatternParser() != null) { + handlerMapping.setPatternParser(pathConfig.getPatternParser()); + } + else { + handlerMapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault()); + handlerMapping.setPathMatcher(pathConfig.getPathMatcherOrDefault()); + } handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); handlerMapping.setCorsConfigurations(getCorsConfigurations()); return handlerMapping; @@ -524,8 +527,6 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv @Bean @Nullable public HandlerMapping resourceHandlerMapping( - @Qualifier("mvcUrlPathHelper") UrlPathHelper urlPathHelper, - @Qualifier("mvcPathMatcher") PathMatcher pathMatcher, @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, @Qualifier("mvcConversionService") FormattingConversionService conversionService, @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) { @@ -533,16 +534,23 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv Assert.state(this.applicationContext != null, "No ApplicationContext set"); Assert.state(this.servletContext != null, "No ServletContext set"); + PathMatchConfigurer pathConfig = getPathMatchConfigurer(); + ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext, - this.servletContext, contentNegotiationManager, urlPathHelper); + this.servletContext, contentNegotiationManager, pathConfig.getUrlPathHelper()); addResourceHandlers(registry); AbstractHandlerMapping handlerMapping = registry.getHandlerMapping(); if (handlerMapping == null) { return null; } - handlerMapping.setPathMatcher(pathMatcher); - handlerMapping.setUrlPathHelper(urlPathHelper); + if (pathConfig.getPatternParser() != null) { + handlerMapping.setPatternParser(pathConfig.getPatternParser()); + } + else { + handlerMapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault()); + handlerMapping.setPathMatcher(pathConfig.getPathMatcherOrDefault()); + } handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); handlerMapping.setCorsConfigurations(getCorsConfigurations()); return handlerMapping; @@ -562,14 +570,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv @Bean public ResourceUrlProvider mvcResourceUrlProvider() { ResourceUrlProvider urlProvider = new ResourceUrlProvider(); - UrlPathHelper pathHelper = getPathMatchConfigurer().getUrlPathHelper(); - if (pathHelper != null) { - urlProvider.setUrlPathHelper(pathHelper); - } - PathMatcher pathMatcher = getPathMatchConfigurer().getPathMatcher(); - if (pathMatcher != null) { - urlProvider.setPathMatcher(pathMatcher); - } + urlProvider.setUrlPathHelper(getPathMatchConfigurer().getUrlPathHelperOrDefault()); + urlProvider.setPathMatcher(getPathMatchConfigurer().getPathMatcherOrDefault()); return urlProvider; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java index 3c21b2fb6d..df771df2d2 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java @@ -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. @@ -29,6 +29,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; /** @@ -47,15 +48,11 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl public interface WebMvcConfigurer { /** - * Helps with configuring HandlerMappings path matching options such as trailing slash match, - * suffix registration, path matcher and path helper. - * Configured path matcher and path helper instances are shared for: - *

    - *
  • RequestMappings
  • - *
  • ViewControllerMappings
  • - *
  • ResourcesMappings
  • - *
+ * Help with configuring {@link HandlerMapping} path matching options such as + * whether to use parsed {@code PathPatterns} or String pattern matching + * with {@code PathMatcher}, whether to match trailing slashes, and more. * @since 4.0.3 + * @see PathMatchConfigurer */ default void configurePathMatch(PathMatchConfigurer configurer) { } @@ -101,6 +98,7 @@ public interface WebMvcConfigurer { * Add handlers to serve static resources such as images, js, and, css * files from specific locations under web application root, the classpath, * and others. + * @see ResourceHandlerRegistry */ default void addResourceHandlers(ResourceHandlerRegistry registry) { } @@ -118,6 +116,7 @@ public interface WebMvcConfigurer { * cases where there is no need for custom controller logic -- e.g. render a * home page, perform simple site URL redirects, return a 404 status with * HTML content, a 204 with no content, and more. + * @see ViewControllerRegistry */ default void addViewControllers(ViewControllerRegistry registry) { } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java index fca3dbe9d6..5f62d7e2b9 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.web.servlet.function; import java.io.IOException; @@ -53,6 +52,7 @@ import org.springframework.http.MediaType; import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.PathContainer; +import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; @@ -62,10 +62,9 @@ import org.springframework.util.ObjectUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.WebRequest; -import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UriBuilder; -import org.springframework.web.util.UrlPathHelper; /** * {@code ServerRequest} implementation based on a {@link HttpServletRequest}. @@ -77,7 +76,7 @@ class DefaultServerRequest implements ServerRequest { private final ServletServerHttpRequest serverHttpRequest; - private final PathContainer pathContainer; + private final RequestPath requestPath; private final Headers headers; @@ -102,7 +101,7 @@ class DefaultServerRequest implements ServerRequest { this.params = CollectionUtils.toMultiValueMap(new ServletParametersMap(servletRequest)); this.attributes = new ServletAttributesMap(servletRequest); - this.pathContainer = PathContainer.parsePath(path()); + this.requestPath = ServletRequestPathUtils.getParsedRequestPath(servletRequest); } private static List allSupportedMediaTypes(List> messageConverters) { @@ -130,16 +129,12 @@ class DefaultServerRequest implements ServerRequest { @Override public String path() { - String path = (String) servletRequest().getAttribute(HandlerMapping.LOOKUP_PATH); - if (path == null) { - path = UrlPathHelper.defaultInstance.getLookupPathForRequest(servletRequest()); - } - return path; + return pathContainer().value(); } @Override public PathContainer pathContainer() { - return this.pathContainer; + return this.requestPath.pathWithinApplication(); } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java index 55dfa41e4d..ca4a1a91b8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java @@ -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. @@ -166,8 +166,6 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini @Nullable @Override protected Object getHandlerInternal(HttpServletRequest servletRequest) throws Exception { - String lookupPath = getUrlPathHelper().getLookupPathForRequest(servletRequest); - servletRequest.setAttribute(LOOKUP_PATH, lookupPath); if (this.routerFunction != null) { ServerRequest request = ServerRequest.create(servletRequest, this.messageConverters); servletRequest.setAttribute(RouterFunctions.REQUEST_ATTRIBUTE, request); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java index a1279b3375..adc610606e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java @@ -30,9 +30,11 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.BeanNameAware; import org.springframework.core.Ordered; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.PathMatcher; import org.springframework.web.HttpRequestHandler; import org.springframework.web.context.request.WebRequestInterceptor; @@ -48,7 +50,10 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +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; /** * Abstract base class for {@link org.springframework.web.servlet.HandlerMapping} @@ -64,9 +69,6 @@ import org.springframework.web.util.UrlPathHelper; * @since 07.04.2003 * @see #getHandlerInternal * @see #setDefaultHandler - * @see #setAlwaysUseFullPath - * @see #setUrlDecode - * @see org.springframework.util.AntPathMatcher * @see #setInterceptors * @see org.springframework.web.servlet.HandlerInterceptor */ @@ -76,6 +78,9 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport @Nullable private Object defaultHandler; + @Nullable + private PathPatternParser patternParser; + private UrlPathHelper urlPathHelper = new UrlPathHelper(); private PathMatcher pathMatcher = new AntPathMatcher(); @@ -114,9 +119,57 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } /** - * Shortcut to same property on underlying {@link #setUrlPathHelper UrlPathHelper}. + * Enable use of pre-parsed {@link PathPattern}s as an alternative to + * String pattern matching with {@link AntPathMatcher}. The syntax is + * largely the same but the {@code PathPattern} syntax is more tailored for + * web applications, and its implementation is more efficient. + *

This property is mutually exclusive with the following others which + * are effectively ignored when this is set: + *

    + *
  • {@link #setAlwaysUseFullPath} -- {@code PathPatterns} always use the + * full path and ignore the servletPath/pathInfo which are decoded and + * partially normalized and therefore not comparable against the + * {@link HttpServletRequest#getRequestURI() requestURI}. + *
  • {@link #setRemoveSemicolonContent} -- {@code PathPatterns} always + * ignore semicolon content for path matching purposes, but path parameters + * remain available for use in controllers via {@code @MatrixVariable}. + *
  • {@link #setUrlDecode} -- {@code PathPatterns} match one decoded path + * segment at a time and never need the full decoded path which can cause + * issues due to decoded reserved characters. + *
  • {@link #setUrlPathHelper} -- the request path is pre-parsed globally + * by the {@link org.springframework.web.servlet.DispatcherServlet + * DispatcherServlet} or by + * {@link org.springframework.web.filter.ServletRequestPathFilter + * ServletRequestPathFilter} using {@link ServletRequestPathUtils} and saved + * in a request attribute for re-use. + *
  • {@link #setPathMatcher} -- patterns are parsed to {@code PathPatterns} + * and used instead of String matching with {@code PathMatcher}. + *
+ *

By default this is not set. + * @param patternParser the parser to use + * @since 5.3 + */ + public void setPatternParser(PathPatternParser patternParser) { + this.patternParser = patternParser; + } + + /** + * Return the {@link #setPatternParser(PathPatternParser) configured} + * {@code PathPatternParser}, or {@code null}. + * @since 5.3 + */ + @Nullable + public PathPatternParser getPatternParser() { + return this.patternParser; + } + + /** + * Shortcut to same property on the configured {@code UrlPathHelper}. + *

Note: This property is mutually exclusive with and + * ignored when {@link #setPatternParser(PathPatternParser)} is set. * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath(boolean) */ + @SuppressWarnings("deprecation") public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) { @@ -125,9 +178,12 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } /** - * Shortcut to same property on underlying {@link #setUrlPathHelper UrlPathHelper}. + * Shortcut to same property on the underlying {@code UrlPathHelper}. + *

Note: This property is mutually exclusive with and + * ignored when {@link #setPatternParser(PathPatternParser)} is set. * @see org.springframework.web.util.UrlPathHelper#setUrlDecode(boolean) */ + @SuppressWarnings("deprecation") public void setUrlDecode(boolean urlDecode) { this.urlPathHelper.setUrlDecode(urlDecode); if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) { @@ -136,9 +192,12 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } /** - * Shortcut to same property on underlying {@link #setUrlPathHelper UrlPathHelper}. + * Shortcut to same property on the underlying {@code UrlPathHelper}. + *

Note: This property is mutually exclusive with and + * ignored when {@link #setPatternParser(PathPatternParser)} is set. * @see org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean) */ + @SuppressWarnings("deprecation") public void setRemoveSemicolonContent(boolean removeSemicolonContent) { this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent); if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) { @@ -147,11 +206,11 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } /** - * Set the UrlPathHelper to use for resolution of lookup paths. - *

Use this to override the default UrlPathHelper with a custom subclass, - * or to share common UrlPathHelper settings across multiple HandlerMappings - * and MethodNameResolvers. + * Configure the UrlPathHelper to use for resolution of lookup paths. + *

Note: This property is mutually exclusive with and + * ignored when {@link #setPatternParser(PathPatternParser)} is set. */ + @SuppressWarnings("deprecation") public void setUrlPathHelper(UrlPathHelper urlPathHelper) { Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); this.urlPathHelper = urlPathHelper; @@ -161,15 +220,17 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } /** - * Return the UrlPathHelper implementation to use for resolution of lookup paths. + * Return the {@link #setUrlPathHelper configured} {@code UrlPathHelper}. */ public UrlPathHelper getUrlPathHelper() { return this.urlPathHelper; } /** - * Set the PathMatcher implementation to use for matching URL paths - * against registered URL patterns. Default is AntPathMatcher. + * Configure the PathMatcher to use. + *

Note: This property is mutually exclusive with and + * ignored when {@link #setPatternParser(PathPatternParser)} is set. + *

By default this is {@link AntPathMatcher}. * @see org.springframework.util.AntPathMatcher */ public void setPathMatcher(PathMatcher pathMatcher) { @@ -181,8 +242,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } /** - * Return the PathMatcher implementation to use for matching URL paths - * against registered URL patterns. + * Return the {@link #setPathMatcher configured} {@code PathMatcher}. */ public PathMatcher getPathMatcher() { return this.pathMatcher; @@ -190,48 +250,74 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport /** * Set the interceptors to apply for all handlers mapped by this handler mapping. - *

Supported interceptor types are HandlerInterceptor, WebRequestInterceptor, and MappedInterceptor. + *

Supported interceptor types are {@link HandlerInterceptor}, + * {@link WebRequestInterceptor}, and {@link MappedInterceptor}. * Mapped interceptors apply only to request URLs that match its path patterns. * Mapped interceptor beans are also detected by type during initialization. * @param interceptors array of handler interceptors * @see #adaptInterceptor * @see org.springframework.web.servlet.HandlerInterceptor * @see org.springframework.web.context.request.WebRequestInterceptor + * @see MappedInterceptor */ public void setInterceptors(Object... interceptors) { this.interceptors.addAll(Arrays.asList(interceptors)); } /** - * Set the "global" CORS configurations based on URL patterns. By default the first - * matching URL pattern is combined with the CORS configuration for the handler, if any. + * Set "global" CORS configuration mappings. The first matching URL pattern + * determines the {@code CorsConfiguration} to use which is then further + * {@link CorsConfiguration#combine(CorsConfiguration) combined} with the + * {@code CorsConfiguration} for the selected handler. + *

This is mutually exclusie with + * {@link #setCorsConfigurationSource(CorsConfigurationSource)}. * @since 4.2 - * @see #setCorsConfigurationSource(CorsConfigurationSource) + * @see #setCorsProcessor(CorsProcessor) */ public void setCorsConfigurations(Map corsConfigurations) { - Assert.notNull(corsConfigurations, "corsConfigurations must not be null"); - if (!corsConfigurations.isEmpty()) { - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + if (CollectionUtils.isEmpty(corsConfigurations)) { + this.corsConfigurationSource = null; + return; + } + UrlBasedCorsConfigurationSource source; + if (getPatternParser() != null) { + source = new UrlBasedCorsConfigurationSource(getPatternParser()); + source.setCorsConfigurations(corsConfigurations); + } + else { + source = new UrlBasedCorsConfigurationSource(); source.setCorsConfigurations(corsConfigurations); source.setPathMatcher(this.pathMatcher); source.setUrlPathHelper(this.urlPathHelper); - source.setLookupPathAttributeName(LOOKUP_PATH); - this.corsConfigurationSource = source; } - else { - this.corsConfigurationSource = null; + setCorsConfigurationSource(source); + } + + /** + * Set a {@code CorsConfigurationSource} for "global" CORS config. The + * {@code CorsConfiguration} determined by the source is + * {@link CorsConfiguration#combine(CorsConfiguration) combined} with the + * {@code CorsConfiguration} for the selected handler. + *

This is mutually exclusie with {@link #setCorsConfigurations(Map)}. + * @since 5.1 + * @see #setCorsProcessor(CorsProcessor) + */ + public void setCorsConfigurationSource(CorsConfigurationSource source) { + Assert.notNull(source, "CorsConfigurationSource must not be null"); + this.corsConfigurationSource = source; + if (source instanceof UrlBasedCorsConfigurationSource) { + ((UrlBasedCorsConfigurationSource) source).setAllowInitLookupPath(false); } } /** - * Set the "global" CORS configuration source. By default the first matching URL - * pattern is combined with the CORS configuration for the handler, if any. - * @since 5.1 - * @see #setCorsConfigurations(Map) + * Return the {@link #setCorsConfigurationSource(CorsConfigurationSource) + * configured} {@code CorsConfigurationSource}, if any. + * @since 5.3 */ - public void setCorsConfigurationSource(CorsConfigurationSource corsConfigurationSource) { - Assert.notNull(corsConfigurationSource, "corsConfigurationSource must not be null"); - this.corsConfigurationSource = corsConfigurationSource; + @Nullable + public CorsConfigurationSource getCorsConfigurationSource() { + return this.corsConfigurationSource; } /** @@ -301,22 +387,22 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } /** - * Detect beans of type {@link MappedInterceptor} and add them to the list of mapped interceptors. - *

This is called in addition to any {@link MappedInterceptor MappedInterceptors} that may have been provided - * via {@link #setInterceptors}, by default adding all beans of type {@link MappedInterceptor} - * from the current context and its ancestors. Subclasses can override and refine this policy. - * @param mappedInterceptors an empty list to add {@link MappedInterceptor} instances to + * Detect beans of type {@link MappedInterceptor} and add them to the list + * of mapped interceptors. + *

This is called in addition to any {@link MappedInterceptor}s that may + * have been provided via {@link #setInterceptors}, by default adding all + * beans of type {@link MappedInterceptor} from the current context and its + * ancestors. Subclasses can override and refine this policy. + * @param mappedInterceptors an empty list to add to */ protected void detectMappedInterceptors(List mappedInterceptors) { - mappedInterceptors.addAll( - BeanFactoryUtils.beansOfTypeIncludingAncestors( - obtainApplicationContext(), MappedInterceptor.class, true, false).values()); + mappedInterceptors.addAll(BeanFactoryUtils.beansOfTypeIncludingAncestors( + obtainApplicationContext(), MappedInterceptor.class, true, false).values()); } /** - * Initialize the specified interceptors, checking for {@link MappedInterceptor MappedInterceptors} and - * adapting {@link HandlerInterceptor}s and {@link WebRequestInterceptor HandlerInterceptor}s and - * {@link WebRequestInterceptor}s if necessary. + * Initialize the specified interceptors adapting + * {@link WebRequestInterceptor}s to {@link HandlerInterceptor}. * @see #setInterceptors * @see #adaptInterceptor */ @@ -333,13 +419,13 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } /** - * Adapt the given interceptor object to the {@link HandlerInterceptor} interface. - *

By default, the supported interceptor types are {@link HandlerInterceptor} - * and {@link WebRequestInterceptor}. Each given {@link WebRequestInterceptor} - * will be wrapped in a {@link WebRequestHandlerInterceptorAdapter}. - * Can be overridden in subclasses. - * @param interceptor the specified interceptor object - * @return the interceptor wrapped as HandlerInterceptor + * Adapt the given interceptor object to {@link HandlerInterceptor}. + *

By default, the supported interceptor types are + * {@link HandlerInterceptor} and {@link WebRequestInterceptor}. Each given + * {@link WebRequestInterceptor} is wrapped with + * {@link WebRequestHandlerInterceptorAdapter}. + * @param interceptor the interceptor + * @return the interceptor downcast or adapted to HandlerInterceptor * @see org.springframework.web.servlet.HandlerInterceptor * @see org.springframework.web.context.request.WebRequestInterceptor * @see WebRequestHandlerInterceptorAdapter @@ -358,7 +444,8 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport /** * Return the adapted interceptors as {@link HandlerInterceptor} array. - * @return the array of {@link HandlerInterceptor HandlerInterceptors}, or {@code null} if none + * @return the array of {@link HandlerInterceptor HandlerInterceptors}, or + * {@code null} if none */ @Nullable protected final HandlerInterceptor[] getAdaptedInterceptors() { @@ -367,8 +454,8 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } /** - * Return all configured {@link MappedInterceptor MappedInterceptors} as an array. - * @return the array of {@link MappedInterceptor MappedInterceptors}, or {@code null} if none + * Return all configured {@link MappedInterceptor}s as an array. + * @return the array of {@link MappedInterceptor}s, or {@code null} if none */ @Nullable protected final MappedInterceptor[] getMappedInterceptors() { @@ -415,9 +502,11 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) { - CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null); - CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); - config = (config != null ? config.combine(handlerConfig) : handlerConfig); + CorsConfiguration config = getCorsConfiguration(handler, request); + if (getCorsConfigurationSource() != null) { + CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request); + config = (globalConfig != null ? globalConfig.combine(config) : config); + } executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } @@ -443,11 +532,35 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport @Nullable protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception; + /** + * Initialize the path to use for request mapping. + *

When parsed patterns are {@link #setPatternParser(PathPatternParser) + * enabled} a parsed {@code RequestPath} is expected to have been + * {@link ServletRequestPathUtils#parseAndCache(HttpServletRequest) parsed} + * externally by the {@link org.springframework.web.servlet.DispatcherServlet} + * or {@link org.springframework.web.filter.ServletRequestPathFilter}. + *

Otherwise for String pattern matching via {@code PathMatcher} the + * path is {@link UrlPathHelper#resolveAndCacheLookupPath resolved} by this + * method. + * @since 5.3 + */ + protected String initLookupPath(HttpServletRequest request) { + if (getPatternParser() != null) { + request.removeAttribute(UrlPathHelper.PATH_ATTRIBUTE); + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(request); + String lookupPath = requestPath.pathWithinApplication().value(); + return UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath); + } + else { + return getUrlPathHelper().resolveAndCacheLookupPath(request); + } + } + /** * Build a {@link HandlerExecutionChain} for the given handler, including * applicable interceptors. *

The default implementation builds a standard {@link HandlerExecutionChain} - * with the given handler, the handler mapping's common interceptors, and any + * with the given handler, the handler mappings common interceptors, and any * {@link MappedInterceptor MappedInterceptors} matching to the current request URL. Interceptors * are added in the order they were registered. Subclasses may override this * in order to extend/rearrange the list of interceptors. @@ -467,11 +580,10 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ? (HandlerExecutionChain) handler : new HandlerExecutionChain(handler)); - String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH); for (HandlerInterceptor interceptor : this.adaptedInterceptors) { if (interceptor instanceof MappedInterceptor) { MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor; - if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) { + if (mappedInterceptor.matches(request)) { chain.addInterceptor(mappedInterceptor.getInterceptor()); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index d96a4e14d6..e352e04722 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -357,8 +358,7 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap */ @Override protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { - String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); - request.setAttribute(LOOKUP_PATH, lookupPath); + String lookupPath = initLookupPath(request); this.mappingRegistry.acquireReadLock(); try { HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); @@ -456,7 +456,8 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap @Override protected boolean hasCorsConfigurationSource(Object handler) { return super.hasCorsConfigurationSource(handler) || - (handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null); + (handler instanceof HandlerMethod && + this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null); } @Override @@ -498,9 +499,27 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap /** * Extract and return the URL paths contained in the supplied mapping. + * @deprecated as of 5.3 in favor of providing non-pattern mappings via + * {@link #getDirectPaths(Object)} instead */ + @Deprecated protected abstract Set getMappingPathPatterns(T mapping); + /** + * Return the request mapping paths that are not patterns. + * @since 5.3 + */ + protected Set getDirectPaths(T mapping) { + Set urls = Collections.emptySet(); + for (String path : getMappingPathPatterns(mapping)) { + if (!getPathMatcher().isPattern(path)) { + urls = (urls.isEmpty() ? new HashSet<>(1) : urls); + urls.add(path); + } + } + return urls; + } + /** * Check if a mapping matches the current request and return a (potentially * new) mapping with conditions relevant to the current request. @@ -589,8 +608,8 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap public void register(T mapping, Object handler, Method method) { // Assert that the handler method is not a suspending one. if (KotlinDetector.isKotlinType(method.getDeclaringClass())) { - Class[] parameterTypes = method.getParameterTypes(); - if ((parameterTypes.length > 0) && "kotlin.coroutines.Continuation".equals(parameterTypes[parameterTypes.length - 1].getName())) { + Class[] types = method.getParameterTypes(); + if ((types.length > 0) && "kotlin.coroutines.Continuation".equals(types[types.length - 1].getName())) { throw new IllegalStateException("Unsupported suspending handler method detected: " + method); } } @@ -600,7 +619,7 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap validateMethodMapping(handlerMethod, mapping); this.mappingLookup.put(mapping, handlerMethod); - List directUrls = getDirectUrls(mapping); + Set directUrls = AbstractHandlerMethodMapping.this.getDirectPaths(mapping); for (String url : directUrls) { this.urlLookup.add(url, mapping); } @@ -634,16 +653,6 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap } } - private List getDirectUrls(T mapping) { - List urls = new ArrayList<>(1); - for (String path : getMappingPathPatterns(mapping)) { - if (!getPathMatcher().isPattern(path)) { - urls.add(path); - } - } - return urls; - } - private void addMappingName(String name, HandlerMethod handlerMethod) { List oldList = this.nameLookup.get(name); if (oldList == null) { @@ -722,19 +731,19 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap private final HandlerMethod handlerMethod; - private final List directUrls; + private final Set directUrls; @Nullable private final String mappingName; public MappingRegistration(T mapping, HandlerMethod handlerMethod, - @Nullable List directUrls, @Nullable String mappingName) { + @Nullable Set directUrls, @Nullable String mappingName) { Assert.notNull(mapping, "Mapping must not be null"); Assert.notNull(handlerMethod, "HandlerMethod must not be null"); this.mapping = mapping; this.handlerMethod = handlerMethod; - this.directUrls = (directUrls != null ? directUrls : Collections.emptyList()); + this.directUrls = (directUrls != null ? directUrls : Collections.emptySet()); this.mappingName = mappingName; } @@ -746,7 +755,7 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap return this.handlerMethod; } - public List getDirectUrls() { + public Set getDirectUrls() { return this.directUrls; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java index 0f8a65df1d..83af09e292 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java @@ -28,26 +28,33 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; +import org.springframework.http.server.PathContainer; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; +import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.HandlerMapping; +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; /** - * Abstract base class for URL-mapped {@link org.springframework.web.servlet.HandlerMapping} - * implementations. Provides infrastructure for mapping handlers to URLs and configurable - * URL lookup. For information on the latter, see "alwaysUseFullPath" property. + * Abstract base class for URL-mapped {@link HandlerMapping} implementations. * - *

Supports direct matches, e.g. a registered "/test" matches "/test", and - * various Ant-style pattern matches, e.g. a registered "/t*" pattern matches - * both "/test" and "/team", "/test/*" matches all paths in the "/test" directory, - * "/test/**" matches all paths below "/test". For details, see the - * {@link org.springframework.util.AntPathMatcher AntPathMatcher} javadoc. + *

Supports literal matches and pattern matches such as "/test/*", "/test/**", + * and others. For details on pattern syntax refer to {@link PathPattern} when + * parsed patterns are {@link #setPatternParser(PathPatternParser) enabled} or + * see {@link AntPathMatcher} otherwise. The syntax is largely the same but the + * {@code PathPattern} syntax is more tailored for web applications, and its + * implementation is more efficient. * - *

Will search all path patterns to find the most exact match for the - * current request path. The most exact match is defined as the longest - * path pattern that matches the current request path. + *

All path patterns are checked in order to find the most exact match for the + * current request path where the "most exact" is the longest path pattern that + * matches the current request path. * * @author Juergen Hoeller * @author Arjen Poutsma @@ -64,6 +71,8 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping i private final Map handlerMap = new LinkedHashMap<>(); + private final Map pathPatternHandlerMap = new LinkedHashMap<>(); + /** * Set the root handler for this handler mapping, that is, @@ -90,6 +99,9 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping i */ public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) { this.useTrailingSlashMatch = useTrailingSlashMatch; + if (getPatternParser() != null) { + getPatternParser().setMatchOptionalTrailingSeparator(useTrailingSlashMatch); + } } /** @@ -121,9 +133,15 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping i @Override @Nullable protected Object getHandlerInternal(HttpServletRequest request) throws Exception { - String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); - request.setAttribute(LOOKUP_PATH, lookupPath); - Object handler = lookupHandler(lookupPath, request); + String lookupPath = initLookupPath(request); + Object handler; + if (getPatternParser() != null) { + RequestPath path = ServletRequestPathUtils.getParsedRequestPath(request); + handler = lookupHandler(path, lookupPath, request); + } + else { + handler = lookupHandler(lookupPath, request); + } if (handler == null) { // We need to care for the default handler directly, since we need to // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well. @@ -148,47 +166,83 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping i } /** - * Look up a handler instance for the given URL path. - *

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. - *

Looks for the most exact pattern, where most exact is defined as - * the longest path pattern. - * @param urlPath the URL the bean is mapped to - * @param request current HTTP request (to expose the path within the mapping to) - * @return the associated handler instance, or {@code null} if not found - * @see #exposePathWithinMapping - * @see org.springframework.util.AntPathMatcher + * Look up a handler instance for the given URL path. This method is used + * when parsed {@code PathPattern}s are + * {@link #setPatternParser(PathPatternParser) enabled}. + * @param path the parsed RequestPath + * @param lookupPath the String lookupPath for checking direct hits + * @param request current HTTP request + * @return a matching handler, or {@code null} if not found + * @since 5.3 */ @Nullable - protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception { - // Direct match? - Object handler = this.handlerMap.get(urlPath); + protected Object lookupHandler( + RequestPath path, String lookupPath, HttpServletRequest request) throws Exception { + + Object handler = getDirectMatch(lookupPath, request); if (handler != null) { - // Bean name or resolved handler? - if (handler instanceof String) { - String handlerName = (String) handler; - handler = obtainApplicationContext().getBean(handlerName); + return handler; + } + + // Pattern match? + List matches = null; + for (PathPattern pattern : this.pathPatternHandlerMap.keySet()) { + if (pattern.matches(path)) { + matches = (matches != null ? matches : new ArrayList<>()); + matches.add(pattern); } - validateHandler(handler, request); - return buildPathExposingHandler(handler, urlPath, urlPath, null); + } + if (matches == null) { + return null; + } + if (matches.size() > 1) { + matches.sort(PathPattern.SPECIFICITY_COMPARATOR); + if (logger.isTraceEnabled()) { + logger.debug("Matching patterns " + matches); + } + } + PathPattern pattern = matches.get(0); + handler = this.pathPatternHandlerMap.get(pattern); + if (handler instanceof String) { + String handlerName = (String) handler; + handler = obtainApplicationContext().getBean(handlerName); + } + validateHandler(handler, request); + PathContainer pathWithinMapping = pattern.extractPathWithinPattern(path); + return buildPathExposingHandler(handler, pattern.getPatternString(), pathWithinMapping.value(), null); + } + + /** + * Look up a handler instance for the given URL path. This method is used + * when String pattern matching with {@code PathMatcher} is in use. + * @param lookupPath the path to match patterns against + * @param request current HTTP request + * @return a matching handler, or {@code null} if not found + * @see #exposePathWithinMapping + * @see AntPathMatcher + */ + @Nullable + protected Object lookupHandler(String lookupPath, HttpServletRequest request) throws Exception { + Object handler = getDirectMatch(lookupPath, request); + if (handler != null) { + return handler; } // Pattern match? List matchingPatterns = new ArrayList<>(); for (String registeredPattern : this.handlerMap.keySet()) { - if (getPathMatcher().match(registeredPattern, urlPath)) { + if (getPathMatcher().match(registeredPattern, lookupPath)) { matchingPatterns.add(registeredPattern); } else if (useTrailingSlashMatch()) { - if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) { + if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", lookupPath)) { matchingPatterns.add(registeredPattern + "/"); } } } String bestMatch = null; - Comparator patternComparator = getPathMatcher().getPatternComparator(urlPath); + Comparator patternComparator = getPathMatcher().getPatternComparator(lookupPath); if (!matchingPatterns.isEmpty()) { matchingPatterns.sort(patternComparator); if (logger.isTraceEnabled() && matchingPatterns.size() > 1) { @@ -213,14 +267,14 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping i handler = obtainApplicationContext().getBean(handlerName); } validateHandler(handler, request); - String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath); + String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, lookupPath); // There might be multiple 'best patterns', let's make sure we have the correct URI template variables // for all of them Map uriTemplateVariables = new LinkedHashMap<>(); for (String matchingPattern : matchingPatterns) { if (patternComparator.compare(bestMatch, matchingPattern) == 0) { - Map vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath); + Map vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, lookupPath); Map decodedVars = getUrlPathHelper().decodePathVariables(request, vars); uriTemplateVariables.putAll(decodedVars); } @@ -235,6 +289,21 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping i return null; } + @Nullable + private Object getDirectMatch(String urlPath, HttpServletRequest request) throws Exception { + Object handler = this.handlerMap.get(urlPath); + if (handler != null) { + // Bean name or resolved handler? + if (handler instanceof String) { + String handlerName = (String) handler; + handler = obtainApplicationContext().getBean(handlerName); + } + validateHandler(handler, request); + return buildPathExposingHandler(handler, urlPath, urlPath, null); + } + return null; + } + /** * Validate the given handler against the current request. *

The default implementation is empty. Can be overridden in subclasses, @@ -251,7 +320,8 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping i * handler, the {@link #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE}, as well as * the {@link #URI_TEMPLATE_VARIABLES_ATTRIBUTE} before executing the handler. *

The default implementation builds a {@link HandlerExecutionChain} - * with a special interceptor that exposes the path attribute and uri template variables + * with a special interceptor that exposes the path attribute and URI + * template variables * @param rawHandler the raw handler to expose * @param pathWithinMapping the path to expose before executing the handler * @param uriTemplateVariables the URI template variables, can be {@code null} if no variables found @@ -294,7 +364,8 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping i @Override @Nullable public RequestMatchResult match(HttpServletRequest request, String pattern) { - String lookupPath = getUrlPathHelper().getLookupPathForRequest(request, LOOKUP_PATH); + Assert.isNull(getPatternParser(), "This HandlerMapping uses PathPatterns."); + String lookupPath = UrlPathHelper.getResolvedLookupPath(request); if (getPathMatcher().match(pattern, lookupPath)) { return new RequestMatchResult(pattern, lookupPath, getPathMatcher()); } @@ -365,6 +436,9 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping i } else { this.handlerMap.put(urlPath, resolvedHandler); + if (getPatternParser() != null) { + this.pathPatternHandlerMap.put(getPatternParser().parse(urlPath), resolvedHandler); + } if (logger.isTraceEnabled()) { logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler)); } @@ -378,15 +452,25 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping i /** - * Return the registered handlers as an unmodifiable Map, with the registered path - * as key and the handler object (or handler bean name in case of a lazy-init handler) - * as value. + * Return the handler mappings as a read-only Map, with the registered path + * or pattern as key and the handler object (or handler bean name in case of + * a lazy-init handler), as value. * @see #getDefaultHandler() */ public final Map getHandlerMap() { return Collections.unmodifiableMap(this.handlerMap); } + /** + * Identical to {@link #getHandlerMap()} but with parsed patterns when + * {@link #setPatternParser(PathPatternParser)} is set, or otherwise empty. + * @since 5.3 + */ + public final Map getPathPatternHandlerMap() { + return (this.pathPatternHandlerMap.isEmpty() ? + Collections.emptyMap() : Collections.unmodifiableMap(this.pathPatternHandlerMap)); + } + /** * Indicates whether this handler mapping support type-level mappings. Default to {@code false}. */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 0a3f17dab5..043ac1fd20 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -44,6 +44,7 @@ import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.UrlPathHelper; /** * Helper class to get information from the {@code HandlerMapping} that would @@ -57,6 +58,12 @@ import org.springframework.web.servlet.HandlerMapping; * request. * * + *

Note: This is primarily an SPI to allow Spring Security + * to align its pattern matching with the same pattern matching that would be + * used in Spring MVC for a given request, in order to avoid security issues. + * Use of this introspector should be avoided for other purposes because it + * incurs the overhead of resolving the handler for a request. + * * @author Rossen Stoyanchev * @since 4.3.1 */ @@ -89,7 +96,7 @@ public class HandlerMappingIntrospector /** - * Return the configured HandlerMapping's. + * Return the configured or detected HandlerMapping's. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -141,7 +148,7 @@ public class HandlerMappingIntrospector @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); + RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); for (HandlerMapping handlerMapping : this.handlerMappings) { HandlerExecutionChain handler = null; try { @@ -189,7 +196,6 @@ public class HandlerMappingIntrospector catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } - String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -212,14 +218,16 @@ public class HandlerMappingIntrospector */ private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { - public RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { super(request); } @Override public void setAttribute(String name, Object value) { - // Ignore attribute change... + // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency + if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { + super.setAttribute(name, value); + } } } - } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java index 0c6a37e33f..3b1011fba4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java @@ -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,26 +16,42 @@ package org.springframework.web.servlet.handler; +import java.util.Arrays; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.http.server.PathContainer; import org.springframework.lang.Nullable; +import org.springframework.util.AntPathMatcher; import org.springframework.util.ObjectUtils; import org.springframework.util.PathMatcher; import org.springframework.web.context.request.WebRequestInterceptor; import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.ModelAndView; +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; /** - * Contains and delegates calls to a {@link HandlerInterceptor} along with - * include (and optionally exclude) path patterns to which the interceptor should apply. - * Also provides matching logic to test if the interceptor applies to a given request path. + * Wraps a {@link HandlerInterceptor} and uses URL patterns to determine whether + * it applies to a given request. * - *

A MappedInterceptor can be registered directly with any - * {@link org.springframework.web.servlet.handler.AbstractHandlerMethodMapping}. - * Furthermore, beans of type {@code MappedInterceptor} are automatically detected by - * {@code AbstractHandlerMethodMapping} (including ancestor ApplicationContext's) which - * effectively means the interceptor is registered "globally" with all handler mappings. + *

Pattern matching can be done with {@link PathMatcher} or with parsed + * {@link PathPattern}. The syntax is largely the same with the latter being more + * tailored for web usage and more efficient. The choice is driven by the + * presence of a {@link UrlPathHelper#resolveAndCacheLookupPath resolved} + * {@code String} lookupPath or a {@link ServletRequestPathUtils#parseAndCache + * parsed} {@code RequestPath} which in turn depends on the + * {@link HandlerMapping} that matched the current request. + * + *

{@code MappedInterceptor} is supported by sub-classes of + * {@link org.springframework.web.servlet.handler.AbstractHandlerMethodMapping + * AbstractHandlerMethodMapping} which detect beans of type + * {@code MappedInterceptor} and also check if interceptors directly registered + * with it are of this type. * * @author Keith Donald * @author Rossen Stoyanchev @@ -44,56 +60,83 @@ import org.springframework.web.servlet.ModelAndView; */ public final class MappedInterceptor implements HandlerInterceptor { - @Nullable - private final String[] includePatterns; + private static PathMatcher defaultPathMatcher = new AntPathMatcher(); + @Nullable - private final String[] excludePatterns; + private final PathPattern[] includePatterns; + + @Nullable + private final PathPattern[] excludePatterns; + + private PathMatcher pathMatcher = defaultPathMatcher; private final HandlerInterceptor interceptor; - @Nullable - private PathMatcher pathMatcher; - /** - * Create a new MappedInterceptor instance. - * @param includePatterns the path patterns to map (empty for matching to all paths) - * @param interceptor the HandlerInterceptor instance to map to the given patterns + * Create an instance with the given include and exclude patterns along with + * the target interceptor for the mappings. + * @param includePatterns patterns to which requests must match, or null to + * match all paths + * @param excludePatterns patterns to which requests must not match + * @param interceptor the target interceptor + * @param parser a parser to use to pre-parse patterns into {@link PathPattern}; + * when not provided, {@link PathPatternParser#defaultInstance} is used. + * @since 5.3 + */ + public MappedInterceptor(@Nullable String[] includePatterns, @Nullable String[] excludePatterns, + HandlerInterceptor interceptor, @Nullable PathPatternParser parser) { + + this.includePatterns = initPatterns(includePatterns, parser); + this.excludePatterns = initPatterns(excludePatterns, parser); + this.interceptor = interceptor; + } + + @Nullable + private static PathPattern[] initPatterns( + @Nullable String[] patterns, @Nullable PathPatternParser parser) { + + if (ObjectUtils.isEmpty(patterns)) { + return null; + } + parser = (parser != null ? parser : PathPatternParser.defaultInstance); + return Arrays.stream(patterns).map(parser::parse).toArray(PathPattern[]::new); + } + + /** + * Variant of + * {@link #MappedInterceptor(String[], String[], HandlerInterceptor, PathPatternParser)} + * with include patterns only. */ public MappedInterceptor(@Nullable String[] includePatterns, HandlerInterceptor interceptor) { this(includePatterns, null, interceptor); } /** - * Create a new MappedInterceptor instance. - * @param includePatterns the path patterns to map (empty for matching to all paths) - * @param excludePatterns the path patterns to exclude (empty for no specific excludes) - * @param interceptor the HandlerInterceptor instance to map to the given patterns + * Variant of + * {@link #MappedInterceptor(String[], String[], HandlerInterceptor, PathPatternParser)} + * without a provided parser. */ public MappedInterceptor(@Nullable String[] includePatterns, @Nullable String[] excludePatterns, HandlerInterceptor interceptor) { - this.includePatterns = includePatterns; - this.excludePatterns = excludePatterns; - this.interceptor = interceptor; + this(includePatterns, excludePatterns, interceptor, null); } - /** - * Create a new MappedInterceptor instance. - * @param includePatterns the path patterns to map (empty for matching to all paths) - * @param interceptor the WebRequestInterceptor instance to map to the given patterns + * Variant of + * {@link #MappedInterceptor(String[], String[], HandlerInterceptor, PathPatternParser)} + * with a {@link WebRequestInterceptor} as the target. */ public MappedInterceptor(@Nullable String[] includePatterns, WebRequestInterceptor interceptor) { this(includePatterns, null, interceptor); } /** - * Create a new MappedInterceptor instance. - * @param includePatterns the path patterns to map (empty for matching to all paths) - * @param excludePatterns the path patterns to exclude (empty for no specific excludes) - * @param interceptor the WebRequestInterceptor instance to map to the given patterns + * Variant of + * {@link #MappedInterceptor(String[], String[], HandlerInterceptor, PathPatternParser)} + * with a {@link WebRequestInterceptor} as the target. */ public MappedInterceptor(@Nullable String[] includePatterns, @Nullable String[] excludePatterns, WebRequestInterceptor interceptor) { @@ -103,51 +146,61 @@ public final class MappedInterceptor implements HandlerInterceptor { /** - * Configure a PathMatcher to use with this MappedInterceptor instead of the one passed - * by default to the {@link #matches(String, org.springframework.util.PathMatcher)} method. - *

This is an advanced property that is only required when using custom PathMatcher - * implementations that support mapping metadata other than the Ant-style path patterns - * supported by default. - */ - public void setPathMatcher(@Nullable PathMatcher pathMatcher) { - this.pathMatcher = pathMatcher; - } - - /** - * The configured PathMatcher, or {@code null} if none. - */ - @Nullable - public PathMatcher getPathMatcher() { - return this.pathMatcher; - } - - /** - * The path into the application the interceptor is mapped to. + * Return the patterns this interceptor is mapped to. */ @Nullable public String[] getPathPatterns() { - return this.includePatterns; + return (!ObjectUtils.isEmpty(this.includePatterns) ? + Arrays.stream(this.includePatterns).map(PathPattern::getPatternString).toArray(String[]::new) : + null); } /** - * The actual {@link HandlerInterceptor} reference. + * The target {@link HandlerInterceptor} to invoke in case of a match. */ public HandlerInterceptor getInterceptor() { return this.interceptor; } + /** + * Configure the PathMatcher to use to match URL paths with against include + * and exclude patterns. + *

This is an advanced property that should be used only when a + * customized {@link AntPathMatcher} or a custom PathMatcher is required. + *

By default this is {@link AntPathMatcher}. + *

Note: 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; + } /** - * Determine a match for the given lookup path. - * @param lookupPath the current request path - * @param pathMatcher a path matcher for path pattern matching - * @return {@code true} if the interceptor applies to the given request path + * The {@link #setPathMatcher(PathMatcher) configured} PathMatcher. */ - public boolean matches(String lookupPath, PathMatcher pathMatcher) { - PathMatcher pathMatcherToUse = (this.pathMatcher != null ? this.pathMatcher : pathMatcher); + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + + + /** + * Check whether this interceptor is mapped to the request. + *

The request mapping path is expected to have been resolved externally. + * See also class-level Javadoc. + * @param request the request to match to + * @return {@code true} if the interceptor should be applied to the request + */ + public boolean matches(HttpServletRequest request) { + Object path = ServletRequestPathUtils.getCachedPath(request); + if (this.pathMatcher != defaultPathMatcher) { + path = path.toString(); + } + boolean isPathContainer = (path instanceof PathContainer); if (!ObjectUtils.isEmpty(this.excludePatterns)) { - for (String pattern : this.excludePatterns) { - if (pathMatcherToUse.match(pattern, lookupPath)) { + for (PathPattern pattern : this.excludePatterns) { + if (matchPattern(path, isPathContainer, pattern)) { return false; } } @@ -155,14 +208,51 @@ public final class MappedInterceptor implements HandlerInterceptor { if (ObjectUtils.isEmpty(this.includePatterns)) { return true; } - for (String pattern : this.includePatterns) { - if (pathMatcherToUse.match(pattern, lookupPath)) { + for (PathPattern pattern : this.includePatterns) { + if (matchPattern(path, isPathContainer, pattern)) { return true; } } return false; } + private boolean matchPattern(Object path, boolean isPathContainer, PathPattern pattern) { + return (isPathContainer ? + pattern.matches((PathContainer) path) : + this.pathMatcher.match(pattern.getPatternString(), (String) path)); + } + + /** + * Determine a match for the given lookup path. + * @param lookupPath the current request path + * @param pathMatcher a path matcher for path pattern matching + * @return {@code true} if the interceptor applies to the given request path + * @deprecated as of 5.3 in favor of {@link #matches(HttpServletRequest)} + */ + @Deprecated + public boolean matches(String lookupPath, PathMatcher pathMatcher) { + pathMatcher = (this.pathMatcher != defaultPathMatcher ? this.pathMatcher : pathMatcher); + if (!ObjectUtils.isEmpty(this.excludePatterns)) { + for (PathPattern pattern : this.excludePatterns) { + if (pathMatcher.match(pattern.getPatternString(), lookupPath)) { + return false; + } + } + } + if (ObjectUtils.isEmpty(this.includePatterns)) { + return true; + } + for (PathPattern pattern : this.includePatterns) { + if (pathMatcher.match(pattern.getPatternString(), lookupPath)) { + return true; + } + } + return false; + } + + + // HandlerInterceptor delegation + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MatchableHandlerMapping.java index 2c70412447..7da23e7b6b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MatchableHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -18,8 +18,12 @@ package org.springframework.web.servlet.handler; import javax.servlet.http.HttpServletRequest; +import org.springframework.http.server.PathContainer; import org.springframework.lang.Nullable; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPatternParser; /** * Additional interface that a {@link HandlerMapping} can implement to expose @@ -33,7 +37,32 @@ import org.springframework.web.servlet.HandlerMapping; public interface MatchableHandlerMapping extends HandlerMapping { /** - * Determine whether the given request matches the request criteria. + * Return the parser of this {@code HandlerMapping}, if configured in which + * case pre-parsed patterns are used. + * @since 5.3 + */ + @Nullable + PathPatternParser getPatternParser(); + + /** + * Determine whether the request matches the given pattern. Use this method + * when {@link #getPatternParser()} is not {@code null} which means that the + * {@code HandlerMapping} has pre-parsed patterns enabled. + * @param request the current request + * @param pattern the pattern to match + * @return the result from request matching, or {@code null} if none + * @since 5.3 + */ + @Nullable + default RequestMatchResult match(HttpServletRequest request, PathPattern pattern) { + PathContainer path = ServletRequestPathUtils.getParsedRequestPath(request).pathWithinApplication(); + return (pattern.matches(path) ? new RequestMatchResult(pattern, path) : null); + } + + /** + * Determine whether the request matches the given pattern. Use this method + * when {@link #getPatternParser()} returns {@code null} which means that the + * {@code HandlerMapping} is uses String pattern matching. * @param request the current request * @param pattern the pattern to match * @return the result from request matching, or {@code null} if none diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java index 7641e0bb62..d5f4b09e8d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -18,8 +18,11 @@ package org.springframework.web.servlet.handler; import java.util.Map; +import org.springframework.http.server.PathContainer; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.PathMatcher; +import org.springframework.web.util.pattern.PathPattern; /** * Container for the result from request pattern matching via @@ -31,37 +34,70 @@ import org.springframework.util.PathMatcher; */ public class RequestMatchResult { - private final String matchingPattern; + @Nullable + private final PathPattern pathPattern; + @Nullable + private final PathContainer lookupPathContainer; + + + @Nullable + private final String pattern; + + @Nullable private final String lookupPath; + @Nullable private final PathMatcher pathMatcher; /** - * Create an instance with a matching pattern. - * @param matchingPattern the matching pattern, possibly not the same as the - * input pattern, e.g. inputPattern="/foo" and matchingPattern="/foo/". - * @param lookupPath the lookup path extracted from the request - * @param pathMatcher the PathMatcher used + * Create an instance with the matched {@code PathPattern}. + * @param pathPattern the pattern that was matched + * @param lookupPath the mapping path + * @since 5.3 */ - public RequestMatchResult(String matchingPattern, String lookupPath, PathMatcher pathMatcher) { - Assert.hasText(matchingPattern, "'matchingPattern' is required"); - Assert.hasText(lookupPath, "'lookupPath' is required"); - Assert.notNull(pathMatcher, "'pathMatcher' is required"); - this.matchingPattern = matchingPattern; - this.lookupPath = lookupPath; - this.pathMatcher = pathMatcher; + public RequestMatchResult(PathPattern pathPattern, PathContainer lookupPath) { + Assert.notNull(pathPattern, "PathPattern is required"); + Assert.notNull(pathPattern, "PathContainer is required"); + + this.pattern = null; + this.lookupPath = null; + this.pathMatcher = null; + + this.pathPattern = pathPattern; + this.lookupPathContainer = lookupPath; + } + /** + * Create an instance with the matched String pattern. + * @param pattern the pattern that was matched, possibly with a '/' appended + * @param lookupPath the mapping path + * @param pathMatcher the PathMatcher instance used for the match + */ + public RequestMatchResult(String pattern, String lookupPath, PathMatcher pathMatcher) { + Assert.hasText(pattern, "'matchingPattern' is required"); + Assert.hasText(lookupPath, "'lookupPath' is required"); + Assert.notNull(pathMatcher, "PathMatcher is required"); + + this.pattern = pattern; + this.lookupPath = lookupPath; + this.pathMatcher = pathMatcher; + + this.pathPattern = null; + this.lookupPathContainer = null; + } /** * Extract URI template variables from the matching pattern as defined in * {@link PathMatcher#extractUriTemplateVariables}. * @return a map with URI template variables */ + @SuppressWarnings("ConstantConditions") public Map extractUriTemplateVariables() { - return this.pathMatcher.extractUriTemplateVariables(this.matchingPattern, this.lookupPath); + return (this.pathPattern != null ? + this.pathPattern.matchAndExtract(this.lookupPathContainer).getUriVariables() : + this.pathMatcher.extractUriTemplateVariables(this.pattern, this.lookupPath)); } - } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/UrlFilenameViewController.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/UrlFilenameViewController.java index 31495ada51..4e2397d51d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/UrlFilenameViewController.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/UrlFilenameViewController.java @@ -24,6 +24,7 @@ import javax.servlet.http.HttpServletRequest; import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; /** * Simple {@code Controller} implementation that transforms the virtual @@ -111,7 +112,7 @@ public class UrlFilenameViewController extends AbstractUrlViewController { protected String extractOperableUrl(HttpServletRequest request) { String urlPath = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); if (!StringUtils.hasText(urlPath)) { - urlPath = getUrlPathHelper().getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH); + urlPath = ServletRequestPathUtils.getCachedPathValue(request); } return urlPath; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java index 2296c03354..696be902b7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java @@ -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. @@ -22,25 +22,43 @@ import java.util.Map; import java.util.Properties; import javax.servlet.ServletException; +import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.http.CacheControl; +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.ObjectUtils; import org.springframework.util.PathMatcher; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.support.WebContentGenerator; +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; /** - * Handler interceptor that checks the request and prepares the response. - * Checks for supported methods and a required session, and applies the - * specified {@link org.springframework.http.CacheControl} builder. - * See superclass bean properties for configuration options. + * Handler interceptor that checks the request for supported methods and a + * required session and prepares the response by applying the configured + * cache settings. + * + *

Cache settings may be configured for specific URLs via path pattern with + * {@link #addCacheMapping(CacheControl, String...)} and + * {@link #setCacheMappings(Properties)}, along with a fallback on default + * settings for all URLs via {@link #setCacheControl(CacheControl)}. + * + *

Pattern matching can be done with {@link PathMatcher} or with 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} + * {@code String} lookupPath or a {@link ServletRequestPathUtils#parseAndCache} + * parsed} {@code RequestPath} which in turn depends on the + * {@link HandlerMapping} that matched the current request. * *

All the settings supported by this interceptor can also be set on * {@link AbstractController}. This interceptor is mainly intended for applying @@ -48,113 +66,85 @@ import org.springframework.web.util.UrlPathHelper; * * @author Juergen Hoeller * @author Brian Clozel + * @author Rossen Stoyanchev * @since 27.11.2003 - * @see AbstractController + * @see PathMatcher + * @see AntPathMatcher */ public class WebContentInterceptor extends WebContentGenerator implements HandlerInterceptor { - private UrlPathHelper urlPathHelper = new UrlPathHelper(); - - private PathMatcher pathMatcher = new AntPathMatcher(); - - private Map cacheMappings = new HashMap<>(); - - private Map cacheControlMappings = new HashMap<>(); + private static PathMatcher defaultPathMatcher = new AntPathMatcher(); + private final PathPatternParser patternParser; + + private PathMatcher pathMatcher = defaultPathMatcher; + + private Map cacheMappings = new HashMap<>(); + + private Map cacheControlMappings = new HashMap<>(); + + + /** + * Default constructor with {@link PathPatternParser#defaultInstance}. + */ public WebContentInterceptor() { + this(PathPatternParser.defaultInstance); + } + + /** + * Constructor with a {@link PathPatternParser} to parse patterns with. + * @since 5.3 + */ + public WebContentInterceptor(PathPatternParser parser) { // No restriction of HTTP methods by default, // in particular for use with annotated controllers... super(false); + this.patternParser = parser; } /** - * Shortcut to same property on underlying {@link #setUrlPathHelper UrlPathHelper}. - *

Only relevant for the "cacheMappings" setting. - * @see #setCacheMappings - * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath + * Shortcut to the + * {@link org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath + * same property} on the configured {@code UrlPathHelper}. + * @deprecated as of 5.3, the path is resolved externally and obtained with + * {@link ServletRequestPathUtils#getCachedPathValue(ServletRequest)} */ + @Deprecated public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { - this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); } /** - * Shortcut to same property on underlying {@link #setUrlPathHelper UrlPathHelper}. - *

Only relevant for the "cacheMappings" setting. - * @see #setCacheMappings - * @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, the path is resolved externally and obtained with + * {@link ServletRequestPathUtils#getCachedPathValue(ServletRequest)} */ + @Deprecated public void setUrlDecode(boolean urlDecode) { - this.urlPathHelper.setUrlDecode(urlDecode); } /** * Set the UrlPathHelper to use for resolution of lookup paths. - *

Use this to override the default UrlPathHelper with a custom subclass, - * or to share common UrlPathHelper settings across multiple HandlerMappings - * and MethodNameResolvers. - *

Only relevant for the "cacheMappings" setting. - * @see #setCacheMappings - * @see org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#setUrlPathHelper + * @deprecated as of 5.3, the path is resolved externally and obtained with + * {@link ServletRequestPathUtils#getCachedPathValue(ServletRequest)} */ + @Deprecated public void setUrlPathHelper(UrlPathHelper urlPathHelper) { - Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); - this.urlPathHelper = urlPathHelper; } /** - * Map specific URL paths to specific cache seconds. - *

Overrides the default cache seconds setting of this interceptor. - * Can specify "-1" to exclude a URL path from default caching. - *

Supports direct matches, e.g. a registered "/test" matches "/test", - * and a various Ant-style pattern matches, e.g. a registered "/t*" matches - * both "/test" and "/team". For details, see the AntPathMatcher javadoc. - *

NOTE: Path patterns are not supposed to overlap. If a request - * matches several mappings, it is effectively undefined which one will apply - * (due to the lack of key ordering in {@code java.util.Properties}). - * @param cacheMappings a mapping between URL paths (as keys) and - * cache seconds (as values, need to be integer-parsable) - * @see #setCacheSeconds - * @see org.springframework.util.AntPathMatcher - */ - public void setCacheMappings(Properties cacheMappings) { - this.cacheMappings.clear(); - Enumeration propNames = cacheMappings.propertyNames(); - while (propNames.hasMoreElements()) { - String path = (String) propNames.nextElement(); - int cacheSeconds = Integer.parseInt(cacheMappings.getProperty(path)); - this.cacheMappings.put(path, cacheSeconds); - } - } - - /** - * Map specific URL paths to a specific {@link org.springframework.http.CacheControl}. - *

Overrides the default cache seconds setting of this interceptor. - * Can specify a empty {@link org.springframework.http.CacheControl} instance - * to exclude a URL path from default caching. - *

Supports direct matches, e.g. a registered "/test" matches "/test", - * and a various Ant-style pattern matches, e.g. a registered "/t*" matches - * both "/test" and "/team". For details, see the AntPathMatcher javadoc. - *

NOTE: Path patterns are not supposed to overlap. If a request - * matches several mappings, it is effectively undefined which one will apply - * (due to the lack of key ordering in the underlying {@code java.util.HashMap}). - * @param cacheControl the {@code CacheControl} to use - * @param paths the URL paths that will map to the given {@code CacheControl} - * @since 4.2 - * @see #setCacheSeconds - * @see org.springframework.util.AntPathMatcher - */ - public void addCacheMapping(CacheControl cacheControl, String... paths) { - for (String path : paths) { - this.cacheControlMappings.put(path, cacheControl); - } - } - - /** - * Set the PathMatcher implementation to use for matching URL paths - * against registered URL patterns, for determining cache mappings. - * Default is AntPathMatcher. + * Configure the PathMatcher to use to match URL paths against registered + * URL patterns to select the cache settings for a request. + *

This is an advanced property that should be used only when a + * customized {@link AntPathMatcher} or a custom PathMatcher is required. + *

By default this is {@link AntPathMatcher}. + *

Note: Setting {@code PathMatcher} enforces use of + * String pattern matching even when a + * {@link ServletRequestPathUtils#parseAndCache parsed} {@code RequestPath} + * is available. * @see #addCacheMapping * @see #setCacheMappings * @see org.springframework.util.AntPathMatcher @@ -164,6 +154,54 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle this.pathMatcher = pathMatcher; } + /** + * Map settings for cache seconds to specific URL paths via patterns. + *

Overrides the default cache seconds setting of this interceptor. + * Can specify "-1" to exclude a URL path from default caching. + *

For pattern syntax see {@link AntPathMatcher} and {@link PathPattern} + * as well as the class-level Javadoc for details for when each is used. + * The syntax is largely the same with {@link PathPattern} more tailored for + * web usage. + *

NOTE: Path patterns are not supposed to overlap. If a request + * matches several mappings, it is effectively undefined which one will apply + * (due to the lack of key ordering in {@code java.util.Properties}). + * @param cacheMappings a mapping between URL paths (as keys) and + * cache seconds (as values, need to be integer-parsable) + * @see #setCacheSeconds + */ + public void setCacheMappings(Properties cacheMappings) { + this.cacheMappings.clear(); + Enumeration propNames = cacheMappings.propertyNames(); + while (propNames.hasMoreElements()) { + String path = (String) propNames.nextElement(); + int cacheSeconds = Integer.parseInt(cacheMappings.getProperty(path)); + this.cacheMappings.put(this.patternParser.parse(path), cacheSeconds); + } + } + + /** + * Map specific URL paths to a specific {@link org.springframework.http.CacheControl}. + *

Overrides the default cache seconds setting of this interceptor. + * Can specify a empty {@link org.springframework.http.CacheControl} instance + * to exclude a URL path from default caching. + *

For pattern syntax see {@link AntPathMatcher} and {@link PathPattern} + * as well as the class-level Javadoc for details for when each is used. + * The syntax is largely the same with {@link PathPattern} more tailored for + * web usage. + *

NOTE: Path patterns are not supposed to overlap. If a request + * matches several mappings, it is effectively undefined which one will apply + * (due to the lack of key ordering in the underlying {@code java.util.HashMap}). + * @param cacheControl the {@code CacheControl} to use + * @param paths the URL paths that will map to the given {@code CacheControl} + * @since 4.2 + * @see #setCacheSeconds + */ + public void addCacheMapping(CacheControl cacheControl, String... paths) { + for (String path : paths) { + this.cacheControlMappings.put(this.patternParser.parse(path), cacheControl); + } + } + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) @@ -171,51 +209,51 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle checkRequest(request); - String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH); - - CacheControl cacheControl = lookupCacheControl(lookupPath); - Integer cacheSeconds = lookupCacheSeconds(lookupPath); - if (cacheControl != null) { - if (logger.isTraceEnabled()) { - logger.trace("Applying " + cacheControl); - } - applyCacheControl(response, cacheControl); - } - else if (cacheSeconds != null) { - if (logger.isTraceEnabled()) { - logger.trace("Applying cacheSeconds " + cacheSeconds); - } - applyCacheSeconds(response, cacheSeconds); - } - else { - if (logger.isTraceEnabled()) { - logger.trace("Applying default cacheSeconds"); - } - prepareResponse(response); + Object path = ServletRequestPathUtils.getCachedPath(request); + if (this.pathMatcher != defaultPathMatcher) { + path = path.toString(); } + if (!ObjectUtils.isEmpty(this.cacheControlMappings)) { + CacheControl control = (path instanceof PathContainer ? + lookupCacheControl((PathContainer) path) : lookupCacheControl((String) path)); + if (control != null) { + if (logger.isTraceEnabled()) { + logger.trace("Applying " + control); + } + applyCacheControl(response, control); + return true; + } + } + + if (!ObjectUtils.isEmpty(this.cacheMappings)) { + Integer cacheSeconds = (path instanceof PathContainer ? + lookupCacheSeconds((PathContainer) path) : lookupCacheSeconds((String) path)); + if (cacheSeconds != null) { + if (logger.isTraceEnabled()) { + logger.trace("Applying cacheSeconds " + cacheSeconds); + } + applyCacheSeconds(response, cacheSeconds); + return true; + } + } + + prepareResponse(response); return true; } /** - * Look up a {@link org.springframework.http.CacheControl} instance for the given URL path. - *

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. - * @param urlPath the URL the bean is mapped to - * @return the associated {@code CacheControl}, or {@code null} if not found - * @see org.springframework.util.AntPathMatcher + * Find a {@link org.springframework.http.CacheControl} instance for the + * given parsed {@link PathContainer path}. This is used when the + * {@code HandlerMapping} uses parsed {@code PathPatterns}. + * @param path the path to match to + * @return the matched {@code CacheControl}, or {@code null} if no match + * @since 5.3 */ @Nullable - protected CacheControl lookupCacheControl(String urlPath) { - // Direct match? - CacheControl cacheControl = this.cacheControlMappings.get(urlPath); - if (cacheControl != null) { - return cacheControl; - } - // Pattern match? - for (Map.Entry entry : this.cacheControlMappings.entrySet()) { - if (this.pathMatcher.match(entry.getKey(), urlPath)) { + protected CacheControl lookupCacheControl(PathContainer path) { + for (Map.Entry entry : this.cacheControlMappings.entrySet()) { + if (entry.getKey().matches(path)) { return entry.getValue(); } } @@ -223,30 +261,55 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle } /** - * Look up a cacheSeconds integer value for the given URL path. - *

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. - * @param urlPath the URL the bean is mapped to - * @return the cacheSeconds integer value, or {@code null} if not found - * @see org.springframework.util.AntPathMatcher + * Find a {@link org.springframework.http.CacheControl} instance for the + * given String lookupPath. This is used when the {@code HandlerMapping} + * relies on String pattern matching with {@link PathMatcher}. + * @param lookupPath the path to match to + * @return the matched {@code CacheControl}, or {@code null} if no match */ @Nullable - protected Integer lookupCacheSeconds(String urlPath) { - // Direct match? - Integer cacheSeconds = this.cacheMappings.get(urlPath); - if (cacheSeconds != null) { - return cacheSeconds; - } - // Pattern match? - for (Map.Entry entry : this.cacheMappings.entrySet()) { - if (this.pathMatcher.match(entry.getKey(), urlPath)) { + protected CacheControl lookupCacheControl(String lookupPath) { + for (Map.Entry entry : this.cacheControlMappings.entrySet()) { + if (this.pathMatcher.match(entry.getKey().getPatternString(), lookupPath)) { return entry.getValue(); } } return null; } + /** + * Find a cacheSeconds value for the given parsed {@link PathContainer path}. + * This is used when the {@code HandlerMapping} uses parsed {@code PathPatterns}. + * @param path the path to match to + * @return the matched cacheSeconds, or {@code null} if there is no match + * @since 5.3 + */ + @Nullable + protected Integer lookupCacheSeconds(PathContainer path) { + for (Map.Entry entry : this.cacheMappings.entrySet()) { + if (entry.getKey().matches(path)) { + return entry.getValue(); + } + } + return null; + } + + /** + * Find a cacheSeconds instance for the given String lookupPath. + * This is used when the {@code HandlerMapping} relies on String pattern + * matching with {@link PathMatcher}. + * @param lookupPath the path to match to + * @return the matched cacheSeconds, or {@code null} if there is no match + */ + @Nullable + protected Integer lookupCacheSeconds(String lookupPath) { + for (Map.Entry entry : this.cacheMappings.entrySet()) { + if (this.pathMatcher.match(entry.getKey().getPatternString(), lookupPath)) { + return entry.getValue(); + } + } + return null; + } /** * This implementation is empty. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestCondition.java new file mode 100644 index 0000000000..00a0b204c5 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestCondition.java @@ -0,0 +1,240 @@ +/* + * 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.servlet.mvc.condition; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.server.PathContainer; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; +import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPatternParser; + +/** + * A logical disjunction (' || ') request condition that matches a request + * against a set of URL path patterns. + * + *

In contrast to {@link PatternsRequestCondition}, this condition uses + * parsed {@link PathPattern}s instead of String pattern matching with + * {@link org.springframework.util.AntPathMatcher AntPathMatcher}. + * + * @author Rossen Stoyanchev + * @since 5.3 + */ +public final class PathPatternsRequestCondition extends AbstractRequestCondition { + + private static final SortedSet EMPTY_PATH_PATTERN = + new TreeSet<>(Collections.singleton(new PathPatternParser().parse(""))); + + private static final Set EMPTY_PATH = Collections.singleton(""); + + + private final SortedSet patterns; + + + /** + * Default constructor resulting in an {@code ""} (empty path) mapping. + */ + public PathPatternsRequestCondition() { + this(EMPTY_PATH_PATTERN); + } + + /** + * Constructor with patterns to use. + */ + public PathPatternsRequestCondition(PathPatternParser parser, String... patterns) { + this(parse(parser, patterns)); + } + + private static SortedSet parse(PathPatternParser parser, String... patterns) { + if (patterns.length == 0 || (patterns.length == 1 && !StringUtils.hasText(patterns[0]))) { + return EMPTY_PATH_PATTERN; + } + SortedSet result = new TreeSet<>(); + for (String path : patterns) { + if (StringUtils.hasText(path) && !path.startsWith("/")) { + path = "/" + path; + } + result.add(parser.parse(path)); + } + return result; + } + + private PathPatternsRequestCondition(SortedSet patterns) { + this.patterns = patterns; + } + + /** + * Return the patterns in this condition. If only the first (top) pattern + * is needed use {@link #getFirstPattern()}. + */ + public Set getPatterns() { + return this.patterns; + } + + @Override + protected Collection getContent() { + return this.patterns; + } + + @Override + protected String getToStringInfix() { + return " || "; + } + + /** + * Return the first pattern. + */ + public PathPattern getFirstPattern() { + return this.patterns.first(); + } + + /** + * Whether the condition is the "" (empty path) mapping. + */ + public boolean isEmptyPathMapping() { + return this.patterns == EMPTY_PATH_PATTERN; + } + + /** + * Return the mapping paths that are not patterns. + */ + public Set getDirectPaths() { + if (isEmptyPathMapping()) { + return EMPTY_PATH; + } + Set result = Collections.emptySet(); + for (PathPattern pattern : this.patterns) { + if (pattern.hasPatternSyntax()) { + result = (result.isEmpty() ? new HashSet<>(1) : result); + result.add(pattern.getPatternString()); + } + } + return result; + } + + /** + * Return the {@link #getPatterns()} mapped to Strings. + */ + public Set getPatternValues() { + return (isEmptyPathMapping() ? EMPTY_PATH : + getPatterns().stream().map(PathPattern::getPatternString).collect(Collectors.toSet())); + } + + /** + * Returns a new instance with URL patterns from the current instance + * ("this") and the "other" instance as follows: + *

    + *
  • If there are patterns in both instances, combine the patterns in + * "this" with the patterns in "other" using + * {@link PathPattern#combine(PathPattern)}. + *
  • If only one instance has patterns, use them. + *
  • If neither instance has patterns, use an empty String (i.e. ""). + *
+ */ + @Override + public PathPatternsRequestCondition combine(PathPatternsRequestCondition other) { + if (isEmptyPathMapping() && other.isEmptyPathMapping()) { + return this; + } + else if (other.isEmptyPathMapping()) { + return this; + } + else if (isEmptyPathMapping()) { + return other; + } + else { + SortedSet combined = new TreeSet<>(); + for (PathPattern pattern1 : this.patterns) { + for (PathPattern pattern2 : other.patterns) { + combined.add(pattern1.combine(pattern2)); + } + } + return new PathPatternsRequestCondition(combined); + } + } + + /** + * Checks if any of the patterns match the given request and returns an + * instance that is guaranteed to contain matching patterns, sorted. + * @param request the current request + * @return the same instance if the condition contains no patterns; + * or a new condition with sorted matching patterns; + * or {@code null} if no patterns match. + */ + @Override + @Nullable + public PathPatternsRequestCondition getMatchingCondition(HttpServletRequest request) { + PathContainer path = ServletRequestPathUtils.getParsedRequestPath(request).pathWithinApplication(); + SortedSet matches = getMatchingPatterns(path); + return (matches != null ? new PathPatternsRequestCondition(matches) : null); + } + + @Nullable + private SortedSet getMatchingPatterns(PathContainer path) { + TreeSet result = null; + for (PathPattern pattern : this.patterns) { + if (pattern.matches(path)) { + result = (result != null ? result : new TreeSet<>()); + result.add(pattern); + } + } + return result; + } + + /** + * Compare the two conditions based on the URL patterns they contain. + * Patterns are compared one at a time, from top to bottom. If all compared + * patterns match equally, but one instance has more patterns, it is + * considered a closer match. + *

It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(HttpServletRequest)} to ensure they + * contain only patterns that match the request and are sorted with + * the best matches on top. + */ + @Override + public int compareTo(PathPatternsRequestCondition other, HttpServletRequest request) { + Iterator iterator = this.patterns.iterator(); + Iterator iteratorOther = other.getPatterns().iterator(); + while (iterator.hasNext() && iteratorOther.hasNext()) { + int result = PathPattern.SPECIFICITY_COMPARATOR.compare(iterator.next(), iteratorOther.next()); + if (result != 0) { + return result; + } + } + if (iterator.hasNext()) { + return -1; + } + else if (iteratorOther.hasNext()) { + return 1; + } + else { + return 0; + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java index 4062a05340..71eeff6144 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -32,13 +33,17 @@ import org.springframework.util.AntPathMatcher; import org.springframework.util.ObjectUtils; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; -import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.pattern.PathPattern; /** * A logical disjunction (' || ') request condition that matches a request * against a set of URL path patterns. * + *

In contrast to {@link PathPatternsRequestCondition} which uses parsed + * {@link PathPattern}s, this condition does String pattern matching via + * {@link org.springframework.util.AntPathMatcher AntPathMatcher}. + * * @author Rossen Stoyanchev * @since 3.1 */ @@ -49,8 +54,6 @@ public class PatternsRequestCondition extends AbstractRequestCondition patterns; - private final UrlPathHelper pathHelper; - private final PathMatcher pathMatcher; private final boolean useSuffixPatternMatch; @@ -61,37 +64,50 @@ public class PatternsRequestCondition extends AbstractRequestConditionAs of 5.3 the the path is obtained through the static method + * {@link UrlPathHelper#getResolvedLookupPath} and a {@code UrlPathHelper} + * does not need to be passed in. + * @since 5.2.4 + * @deprecated as of 5.3 in favor of + * {@link #PatternsRequestCondition(String[], boolean, PathMatcher)}. + */ + @Deprecated public PatternsRequestCondition(String[] patterns, @Nullable UrlPathHelper urlPathHelper, @Nullable PathMatcher pathMatcher, boolean useTrailingSlashMatch) { - this(patterns, urlPathHelper, pathMatcher, false, useTrailingSlashMatch, null); + this(patterns, urlPathHelper, pathMatcher, false, useTrailingSlashMatch); } /** - * Alternative constructor with additional optional parameters. - * @param patterns the URL patterns to use; if 0, the condition will match to every request. - * @param urlPathHelper for determining the lookup path of a request - * @param pathMatcher for path matching with patterns - * @param useSuffixPatternMatch whether to enable matching by suffix (".*") - * @param useTrailingSlashMatch whether to match irrespective of a trailing slash + * Variant of {@link #PatternsRequestCondition(String...)} with a + * {@link UrlPathHelper} and a {@link PathMatcher}, and flags for matching + * with suffixes and trailing slashes. + *

As of 5.3 the the path is obtained through the static method + * {@link UrlPathHelper#getResolvedLookupPath} and a {@code UrlPathHelper} + * does not need to be passed in. * @deprecated as of 5.2.4. See class-level note in * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping} * on the deprecation of path extension config options. @@ -104,13 +120,12 @@ public class PatternsRequestCondition extends AbstractRequestConditionAs of 5.3 the the path is obtained through the static method + * {@link UrlPathHelper#getResolvedLookupPath} and a {@code UrlPathHelper} + * does not need to be passed in. * @deprecated as of 5.2.4. See class-level note in * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping} * on the deprecation of path extension config options. @@ -121,7 +136,6 @@ public class PatternsRequestCondition extends AbstractRequestCondition fileExtensions) { this.patterns = initPatterns(patterns); - this.pathHelper = urlPathHelper != null ? urlPathHelper : UrlPathHelper.defaultInstance; this.pathMatcher = pathMatcher != null ? pathMatcher : new AntPathMatcher(); this.useSuffixPatternMatch = useSuffixPatternMatch; this.useTrailingSlashMatch = useTrailingSlashMatch; @@ -166,7 +180,6 @@ public class PatternsRequestCondition extends AbstractRequestCondition patterns, PatternsRequestCondition other) { this.patterns = patterns; - this.pathHelper = other.pathHelper; this.pathMatcher = other.pathMatcher; this.useSuffixPatternMatch = other.useSuffixPatternMatch; this.useTrailingSlashMatch = other.useTrailingSlashMatch; @@ -188,6 +201,31 @@ public class PatternsRequestCondition extends AbstractRequestCondition getDirectPaths() { + if (isEmptyPathMapping()) { + return EMPTY_PATH_PATTERN; + } + Set result = Collections.emptySet(); + for (String pattern : this.patterns) { + if (this.pathMatcher.isPattern(pattern)) { + result = (result.isEmpty() ? new HashSet<>(1) : result); + result.add(pattern); + } + } + return result; + } + /** * Returns a new instance with URL patterns from the current instance ("this") and * the "other" instance as follows: @@ -200,13 +238,13 @@ public class PatternsRequestCondition extends AbstractRequestCondition result = new LinkedHashSet<>(); @@ -220,10 +258,6 @@ public class PatternsRequestCondition extends AbstractRequestCondition matches = getMatchingPatterns(lookupPath); return !matches.isEmpty() ? new PatternsRequestCondition(new LinkedHashSet<>(matches), this) : null; } @@ -318,7 +352,7 @@ public class PatternsRequestCondition extends AbstractRequestCondition patternComparator = this.pathMatcher.getPatternComparator(lookupPath); Iterator iterator = this.patterns.iterator(); Iterator iteratorOther = other.patterns.iterator(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java index a011f06fe9..ab9320bfa3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java @@ -19,30 +19,38 @@ package org.springframework.web.servlet.mvc.method; import java.util.List; import java.util.Set; +import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition; import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition; +import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestCondition; import org.springframework.web.servlet.mvc.condition.RequestConditionHolder; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +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; /** - * Request mapping information. Encapsulates the following request mapping conditions: + * Request mapping information. A composite for the the following conditions: *

    - *
  1. {@link PatternsRequestCondition} + *
  2. {@link PathPatternsRequestCondition} with parsed {@code PathPatterns} or + * {@link PatternsRequestCondition} with String patterns via {@code PathMatcher} *
  3. {@link RequestMethodsRequestCondition} *
  4. {@link ParamsRequestCondition} *
  5. {@link HeadersRequestCondition} @@ -57,6 +65,8 @@ import org.springframework.web.util.UrlPathHelper; */ public final class RequestMappingInfo implements RequestCondition { + private static final PathPatternsRequestCondition EMPTY_PATH_PATTERNS = new PathPatternsRequestCondition(); + private static final PatternsRequestCondition EMPTY_PATTERNS = new PatternsRequestCondition(); private static final RequestMethodsRequestCondition EMPTY_REQUEST_METHODS = new RequestMethodsRequestCondition(); @@ -75,6 +85,10 @@ public final class RequestMappingInfo implements RequestCondition custom) { - this.name = (StringUtils.hasText(name) ? name : null); - this.patternsCondition = (patterns != null ? patterns : EMPTY_PATTERNS); - this.methodsCondition = (methods != null ? methods : EMPTY_REQUEST_METHODS); - this.paramsCondition = (params != null ? params : EMPTY_PARAMS); - this.headersCondition = (headers != null ? headers : EMPTY_HEADERS); - this.consumesCondition = (consumes != null ? consumes : EMPTY_CONSUMES); - this.producesCondition = (produces != null ? produces : EMPTY_PRODUCES); - this.customConditionHolder = (custom != null ? new RequestConditionHolder(custom) : EMPTY_CUSTOM); - - this.hashCode = calculateHashCode( - this.patternsCondition, this.methodsCondition, this.paramsCondition, this.headersCondition, - this.consumesCondition, this.producesCondition, this.customConditionHolder); + this(name, null, + (patterns != null ? patterns : EMPTY_PATTERNS), + (methods != null ? methods : EMPTY_REQUEST_METHODS), + (params != null ? params : EMPTY_PARAMS), + (headers != null ? headers : EMPTY_HEADERS), + (consumes != null ? consumes : EMPTY_CONSUMES), + (produces != null ? produces : EMPTY_PRODUCES), + (custom != null ? new RequestConditionHolder(custom) : EMPTY_CUSTOM)); } /** - * Creates a new instance with the given request conditions. + * Create an instance with the given conditions. + * @deprecated as of 5.3 in favor using {@link RequestMappingInfo.Builder} via + * {@link #paths(String...)}. */ + @Deprecated public RequestMappingInfo(@Nullable PatternsRequestCondition patterns, @Nullable RequestMethodsRequestCondition methods, @Nullable ParamsRequestCondition params, @Nullable HeadersRequestCondition headers, @Nullable ConsumesRequestCondition consumes, @@ -124,12 +143,41 @@ public final class RequestMappingInfo implements RequestCondition customRequestCondition) { this(info.name, info.patternsCondition, info.methodsCondition, info.paramsCondition, info.headersCondition, info.consumesCondition, info.producesCondition, customRequestCondition); } + private RequestMappingInfo(@Nullable String name, + @Nullable PathPatternsRequestCondition pathPatternsCondition, + @Nullable PatternsRequestCondition patternsCondition, + RequestMethodsRequestCondition methodsCondition, ParamsRequestCondition paramsCondition, + HeadersRequestCondition headersCondition, ConsumesRequestCondition consumesCondition, + ProducesRequestCondition producesCondition, RequestConditionHolder customCondition) { + + Assert.isTrue(pathPatternsCondition != null || patternsCondition != null, + "Neither PathPatterns nor String patterns condition"); + + this.name = (StringUtils.hasText(name) ? name : null); + this.pathPatternsCondition = pathPatternsCondition; + this.patternsCondition = patternsCondition; + this.methodsCondition = methodsCondition; + this.paramsCondition = paramsCondition; + this.headersCondition = headersCondition; + this.consumesCondition = consumesCondition; + this.producesCondition = producesCondition; + this.customConditionHolder = customCondition; + + this.hashCode = calculateHashCode( + this.pathPatternsCondition, this.patternsCondition, + this.methodsCondition, this.paramsCondition, this.headersCondition, + this.consumesCondition, this.producesCondition, this.customConditionHolder); + } + + /** * Return the name for this mapping, or {@code null}. */ @@ -139,13 +187,71 @@ public final class RequestMappingInfo implements RequestConditionThis is mutually exclusive with {@link #getPatternsCondition()} such + * that when one returns {@code null} the other one returns an instance. + * @since 5.3 + * @see #getActivePatternsCondition() */ + @Nullable + public PathPatternsRequestCondition getPathPatternsCondition() { + return this.pathPatternsCondition; + } + + /** + * Return the patterns condition when String pattern matching via + * {@link PathMatcher} is in use. + *

    This is mutually exclusive with {@link #getPathPatternsCondition()} + * such that when one returns {@code null} the other one returns an instance. + */ + @Nullable public PatternsRequestCondition getPatternsCondition() { return this.patternsCondition; } + /** + * Returns either {@link #getPathPatternsCondition()} or + * {@link #getPatternsCondition()} depending on which is not null. + * @since 5.3 + */ + @SuppressWarnings("unchecked") + public RequestCondition getActivePatternsCondition() { + if (this.pathPatternsCondition != null) { + return (RequestCondition) this.pathPatternsCondition; + } + else if (this.patternsCondition != null) { + return (RequestCondition) this.patternsCondition; + } + else { + // Already checked in the constructor... + throw new IllegalStateException(); + } + } + + /** + * Return the mapping paths that are not patterns. + * @since 5.3 + */ + public Set getDirectPaths() { + RequestCondition condition = getActivePatternsCondition(); + return (condition instanceof PathPatternsRequestCondition ? + ((PathPatternsRequestCondition) condition).getDirectPaths() : + ((PatternsRequestCondition) condition).getDirectPaths()); + } + + /** + * Return the patterns for the {@link #getActivePatternsCondition() active} + * patterns condition as Strings. + * @since 5.3 + */ + public Set getPatternValues() { + RequestCondition condition = getActivePatternsCondition(); + return (condition instanceof PathPatternsRequestCondition ? + ((PathPatternsRequestCondition) condition).getPatternValues() : + ((PatternsRequestCondition) condition).getPatterns()); + } + /** * Return the HTTP request methods of this {@link RequestMappingInfo}; * or instance with 0 request methods (never {@code null}). @@ -194,16 +300,36 @@ public final class RequestMappingInfo implements RequestCondition customCondition) { + return new RequestMappingInfo(this.name, this.pathPatternsCondition, this.patternsCondition, + this.methodsCondition, this.paramsCondition, this.headersCondition, + this.consumesCondition, this.producesCondition, new RequestConditionHolder(customCondition)); + } /** - * Combine "this" request mapping info (i.e. the current instance) with another request mapping info instance. + * Combine "this" request mapping info (i.e. the current instance) with + * another request mapping info instance. *

    Example: combine type- and method-level request mappings. * @return a new request mapping info instance; never {@code null} */ @Override public RequestMappingInfo combine(RequestMappingInfo other) { String name = combineNames(other); - PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition); + + PathPatternsRequestCondition pathPatterns = + (this.pathPatternsCondition != null && other.pathPatternsCondition != null ? + this.pathPatternsCondition.combine(other.pathPatternsCondition) : null); + + PatternsRequestCondition patterns = + (this.patternsCondition != null && other.patternsCondition != null ? + this.patternsCondition.combine(other.patternsCondition) : null); + RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition); ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition); HeadersRequestCondition headers = this.headersCondition.combine(other.headersCondition); @@ -211,8 +337,8 @@ public final class RequestMappingInfo implements RequestConditionFor example the returned instance may contain the subset of URL patterns that match to - * the current request, sorted with best matching patterns on top. - * @return a new instance in case all conditions match; or {@code null} otherwise + * Checks if all conditions in this request mapping info match the provided + * request and returns a potentially new request mapping info with conditions + * tailored to the current request. + *

    For example the returned instance may contain the subset of URL + * patterns that match to the current request, sorted with best matching + * patterns on top. + * @return a new instance in case of a match; or {@code null} otherwise */ @Override @Nullable @@ -259,24 +387,34 @@ public final class RequestMappingInfo implements RequestConditionNote: It is assumed both instances have been obtained via - * {@link #getMatchingCondition(HttpServletRequest)} to ensure they have conditions with - * content relevant to current request. + * {@link #getMatchingCondition(HttpServletRequest)} to ensure they have + * conditions with content relevant to current request. */ @Override public int compareTo(RequestMappingInfo other, HttpServletRequest request) { @@ -288,7 +426,7 @@ public final class RequestMappingInfo implements RequestCondition httpMethods = this.methodsCondition.getMethods(); builder.append(httpMethods.size() == 1 ? httpMethods.iterator().next() : httpMethods); } - if (!this.patternsCondition.isEmpty()) { - Set patterns = this.patternsCondition.getPatterns(); - builder.append(" ").append(patterns.size() == 1 ? patterns.iterator().next() : patterns); - } + + // Patterns conditions are never empty and have "" (empty path) at least. + builder.append(" ").append(getActivePatternsCondition()); + if (!this.paramsCondition.isEmpty()) { builder.append(", params ").append(this.paramsCondition); } @@ -401,7 +541,7 @@ public final class RequestMappingInfo implements RequestConditionBy default this is not set. - * @since 4.2.8 + * Enable use of parsed {@link PathPattern} as described in + * {@link AbstractHandlerMapping#setPatternParser(PathPatternParser)}. + *

    Note: This property is mutually exclusive with + * {@link #setPathMatcher(PathMatcher)}. + *

    By default this is not enabled. + * @since 5.3 */ - public void setUrlPathHelper(@Nullable UrlPathHelper urlPathHelper) { - this.urlPathHelper = urlPathHelper; + public void setPatternParser(@Nullable PathPatternParser patternParser) { + this.patternParser = patternParser; } /** - * Return a custom UrlPathHelper to use for the PatternsRequestCondition, if any. + * Return the {@link #setPatternParser(PathPatternParser) configured} + * {@code PathPatternParser}, or {@code null}. + * @since 5.3 */ @Nullable + public PathPatternParser getPatternParser() { + return this.patternParser; + } + + /** + * Set a custom UrlPathHelper to use for the PatternsRequestCondition. + *

    By default this is not set. + * @deprecated as of 5.3, the path is resolved externally and obtained with + * {@link ServletRequestPathUtils#getCachedPathValue(ServletRequest)} + * @since 4.2.8 + */ + @Deprecated + public void setUrlPathHelper(@Nullable UrlPathHelper urlPathHelper) { + } + + /** + * Return the configured UrlPathHelper. + * @deprecated as of 5.3, the path is resolved externally and obtained with + * {@link ServletRequestPathUtils#getCachedPathValue(ServletRequest)}; + * this method always returns {@link UrlPathHelper#defaultInstance}. + */ + @Nullable + @Deprecated public UrlPathHelper getUrlPathHelper() { - return this.urlPathHelper; + return UrlPathHelper.defaultInstance; } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java index 0d6bd80895..a52e6c7db4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java @@ -33,6 +33,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; +import org.springframework.http.server.PathContainer; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; @@ -45,8 +47,13 @@ import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping; import org.springframework.web.servlet.mvc.condition.NameValueExpression; +import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition; +import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition; +import org.springframework.web.servlet.mvc.condition.RequestCondition; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.WebUtils; +import org.springframework.web.util.pattern.PathPattern; /** * Abstract base class for classes for which {@link RequestMappingInfo} defines @@ -80,8 +87,14 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe * Get the URL path patterns associated with the supplied {@link RequestMappingInfo}. */ @Override + @SuppressWarnings("deprecation") protected Set getMappingPathPatterns(RequestMappingInfo info) { - return info.getPatternsCondition().getPatterns(); + return info.getPatternValues(); + } + + @Override + protected Set getDirectPaths(RequestMappingInfo info) { + return info.getDirectPaths(); } /** @@ -124,37 +137,61 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) { super.handleMatch(info, lookupPath, request); - String bestPattern; - Map uriVariables; - - Set patterns = info.getPatternsCondition().getPatterns(); - if (patterns.isEmpty()) { - bestPattern = lookupPath; - uriVariables = Collections.emptyMap(); + RequestCondition condition = info.getActivePatternsCondition(); + if (condition instanceof PathPatternsRequestCondition) { + extractMatchDetails((PathPatternsRequestCondition) condition, lookupPath, request); } else { - bestPattern = patterns.iterator().next(); - uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath); + extractMatchDetails((PatternsRequestCondition) condition, lookupPath, request); } - request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern); - - if (isMatrixVariableContentAvailable()) { - Map> matrixVars = extractMatrixVariables(request, uriVariables); - request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, matrixVars); - } - - Map decodedUriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables); - request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables); - if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) { Set mediaTypes = info.getProducesCondition().getProducibleMediaTypes(); request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes); } } - private boolean isMatrixVariableContentAvailable() { - return !getUrlPathHelper().shouldRemoveSemicolonContent(); + private void extractMatchDetails( + PathPatternsRequestCondition condition, String lookupPath, HttpServletRequest request) { + + PathPattern bestPattern; + Map uriVariables; + if (condition.isEmptyPathMapping()) { + bestPattern = condition.getFirstPattern(); + uriVariables = Collections.emptyMap(); + } + else { + PathContainer path = ServletRequestPathUtils.getParsedRequestPath(request).pathWithinApplication(); + bestPattern = condition.getFirstPattern(); + PathPattern.PathMatchInfo result = bestPattern.matchAndExtract(path); + Assert.notNull(result, () -> + "Expected bestPattern: " + bestPattern + " to match lookupPath " + path); + uriVariables = result.getUriVariables(); + request.setAttribute(MATRIX_VARIABLES_ATTRIBUTE, result.getMatrixVariables()); + } + request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern.getPatternString()); + request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables); + } + + private void extractMatchDetails( + PatternsRequestCondition condition, String lookupPath, HttpServletRequest request) { + + String bestPattern; + Map uriVariables; + if (condition.isEmptyPathMapping()) { + bestPattern = lookupPath; + uriVariables = Collections.emptyMap(); + } + else { + bestPattern = condition.getPatterns().iterator().next(); + uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath); + if (!getUrlPathHelper().shouldRemoveSemicolonContent()) { + request.setAttribute(MATRIX_VARIABLES_ATTRIBUTE, extractMatrixVariables(request, uriVariables)); + } + uriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables); + } + request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern); + request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables); } private Map> extractMatrixVariables( @@ -250,7 +287,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe public PartialMatchHelper(Set infos, HttpServletRequest request) { for (RequestMappingInfo info : infos) { - if (info.getPatternsCondition().getMatchingCondition(request) != null) { + if (info.getActivePatternsCondition().getMatchingCondition(request) != null) { this.partialMatches.add(new PartialMatch(info, request)); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java index d865db0709..fcda20d468 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java @@ -97,9 +97,7 @@ import org.springframework.web.util.UriComponentsBuilder; */ public class MvcUriComponentsBuilder { - /** - * Well-known name for the {@link CompositeUriComponentsContributor} object in the bean factory. - */ + /** Well-known name for the {@link CompositeUriComponentsContributor} object in the bean factory. */ public static final String MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME = "mvcUriComponentsContributor"; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 81a07b2cf6..8f8c84a9b6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -23,7 +23,6 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.function.Predicate; import javax.servlet.http.HttpServletRequest; @@ -52,6 +51,8 @@ import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; +import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.pattern.PathPatternParser; /** * Creates {@link RequestMappingInfo} instances from type and method-level @@ -81,7 +82,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi private boolean useTrailingSlashMatch = true; - private Map>> pathPrefixes = new LinkedHashMap<>(); + private Map>> pathPrefixes = Collections.emptyMap(); private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); @@ -97,6 +98,8 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi *

    By default value this is set to {@code false}. *

    Also see {@link #setUseRegisteredSuffixPatternMatch(boolean)} for * more fine-grained control over specific suffixes to allow. + *

    Note: This property is ignored when + * {@link #setPatternParser(PathPatternParser)} is configured. * @deprecated as of 5.2.4. See class level note on the deprecation of * path extension config options. As there is no replacement for this method, * in 5.2.x it is necessary to set it to {@code false}. In 5.3 the default @@ -113,6 +116,8 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi * is generally recommended to reduce ambiguity and to avoid issues such as * when a "." appears in the path for other reasons. *

    By default this is set to "false". + *

    Note: This property is ignored when + * {@link #setPatternParser(PathPatternParser)} is configured. * @deprecated as of 5.2.4. See class level note on the deprecation of * path extension config options. */ @@ -129,6 +134,9 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi */ public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) { this.useTrailingSlashMatch = useTrailingSlashMatch; + if (getPatternParser() != null) { + getPatternParser().setMatchOptionalTrailingSeparator(useTrailingSlashMatch); + } } /** @@ -142,7 +150,9 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi * @since 5.1 */ public void setPathPrefixes(Map>> prefixes) { - this.pathPrefixes = Collections.unmodifiableMap(new LinkedHashMap<>(prefixes)); + this.pathPrefixes = (!prefixes.isEmpty() ? + Collections.unmodifiableMap(new LinkedHashMap<>(prefixes)) : + Collections.emptyMap()); } /** @@ -177,14 +187,22 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi @Override @SuppressWarnings("deprecation") public void afterPropertiesSet() { + this.config = new RequestMappingInfo.BuilderConfiguration(); - this.config.setUrlPathHelper(getUrlPathHelper()); - this.config.setPathMatcher(getPathMatcher()); - this.config.setSuffixPatternMatch(useSuffixPatternMatch()); this.config.setTrailingSlashMatch(useTrailingSlashMatch()); - this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch()); this.config.setContentNegotiationManager(getContentNegotiationManager()); + if (getPatternParser() != null) { + this.config.setPatternParser(getPatternParser()); + Assert.isTrue(!this.useSuffixPatternMatch && !this.useRegisteredSuffixPatternMatch, + "Suffix pattern matching not supported with PathPatternParser."); + } + else { + this.config.setSuffixPatternMatch(useSuffixPatternMatch()); + this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch()); + this.config.setPathMatcher(getPathMatcher()); + } + super.afterPropertiesSet(); } @@ -393,14 +411,14 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi @Override public RequestMatchResult match(HttpServletRequest request, String pattern) { + Assert.isNull(getPatternParser(), "This HandlerMapping requires a PathPattern."); RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build(); - RequestMappingInfo matchingInfo = info.getMatchingCondition(request); - if (matchingInfo == null) { - return null; - } - Set patterns = matchingInfo.getPatternsCondition().getPatterns(); - String lookupPath = getUrlPathHelper().getLookupPathForRequest(request, LOOKUP_PATH); - return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher()); + RequestMappingInfo match = info.getMatchingCondition(request); + return (match != null ? + new RequestMatchResult( + match.getPatternsCondition().getPatterns().iterator().next(), + UrlPathHelper.getResolvedLookupPath(request), + getPathMatcher()) : null); } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java index b91892cdeb..8e0bde0c7a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java @@ -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. @@ -57,10 +57,12 @@ public class ResourceUrlEncodingFilter extends GenericFilterBean { if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { throw new ServletException("ResourceUrlEncodingFilter only supports HTTP requests"); } + ResourceUrlEncodingRequestWrapper wrappedRequest = new ResourceUrlEncodingRequestWrapper((HttpServletRequest) request); ResourceUrlEncodingResponseWrapper wrappedResponse = new ResourceUrlEncodingResponseWrapper(wrappedRequest, (HttpServletResponse) response); + filterChain.doFilter(wrappedRequest, wrappedResponse); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java index 690de8df2e..67c2ad455b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java @@ -35,7 +35,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; -import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.util.UrlPathHelper; @@ -181,8 +180,11 @@ public class ResourceUrlProvider implements ApplicationListenerUse this to override the default UrlPathHelper with a custom subclass, * or to share common UrlPathHelper settings across multiple web components. + * @deprecated as of 5.3, the path is resolved externally and obtained with + * {@link ServletRequestPathUtils#getCachedPathValue(ServletRequest)} */ + @Deprecated public void setUrlPathHelper(UrlPathHelper urlPathHelper) { - Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); - this.urlPathHelper = urlPathHelper; } /** * Translates the request URI of the incoming {@link HttpServletRequest} * into the view name based on the configured parameters. - * @see org.springframework.web.util.UrlPathHelper#getLookupPathForRequest + * @see ServletRequestPathUtils#getCachedPath(ServletRequest) * @see #transformPath + * @throws IllegalArgumentException if neither a parsed RequestPath, nor a + * String lookupPath have been resolved and cached as a request attribute. */ @Override public String getViewName(HttpServletRequest request) { - String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH); - return (this.prefix + transformPath(lookupPath) + this.suffix); + String path = ServletRequestPathUtils.getCachedPathValue(request); + return (this.prefix + transformPath(path) + this.suffix); } /** diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java index ecb89f9fc3..052eca7679 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java @@ -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. @@ -16,9 +16,9 @@ package org.springframework.web.servlet.config.annotation; -import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.BiConsumer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -34,16 +34,20 @@ import org.springframework.util.PathMatcher; import org.springframework.validation.DefaultMessageCodesResolver; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; +import org.springframework.web.testfixture.servlet.MockServletContext; import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -82,15 +86,16 @@ public class DelegatingWebMvcConfigurationTests { @Captor private ArgumentCaptor> exceptionResolvers; - private final DelegatingWebMvcConfiguration delegatingConfig = new DelegatingWebMvcConfiguration(); + private final DelegatingWebMvcConfiguration webMvcConfig = new DelegatingWebMvcConfiguration(); @Test - public void requestMappingHandlerAdapter() throws Exception { - delegatingConfig.setConfigurers(Collections.singletonList(webMvcConfigurer)); - RequestMappingHandlerAdapter adapter = this.delegatingConfig.requestMappingHandlerAdapter( - this.delegatingConfig.mvcContentNegotiationManager(), this.delegatingConfig.mvcConversionService(), - this.delegatingConfig.mvcValidator()); + public void requestMappingHandlerAdapter() { + webMvcConfig.setConfigurers(Collections.singletonList(webMvcConfigurer)); + RequestMappingHandlerAdapter adapter = this.webMvcConfig.requestMappingHandlerAdapter( + this.webMvcConfig.mvcContentNegotiationManager(), + this.webMvcConfig.mvcConversionService(), + this.webMvcConfig.mvcValidator()); ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) adapter.getWebBindingInitializer(); @@ -114,10 +119,9 @@ public class DelegatingWebMvcConfigurationTests { @Test public void configureMessageConverters() { - final HttpMessageConverter customConverter = mock(HttpMessageConverter.class); - final StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(); - List configurers = new ArrayList<>(); - configurers.add(new WebMvcConfigurer() { + HttpMessageConverter customConverter = mock(HttpMessageConverter.class); + StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(); + WebMvcConfigurer configurer = new WebMvcConfigurer() { @Override public void configureMessageConverters(List> converters) { converters.add(stringConverter); @@ -127,13 +131,15 @@ public class DelegatingWebMvcConfigurationTests { public void extendMessageConverters(List> converters) { converters.add(0, customConverter); } - }); - delegatingConfig.setConfigurers(configurers); + }; + webMvcConfig.setConfigurers(Collections.singletonList(configurer)); - RequestMappingHandlerAdapter adapter = delegatingConfig.requestMappingHandlerAdapter( - this.delegatingConfig.mvcContentNegotiationManager(), this.delegatingConfig.mvcConversionService(), - this.delegatingConfig.mvcValidator()); - assertThat(adapter.getMessageConverters().size()).as("Only one custom converter should be registered").isEqualTo(2); + RequestMappingHandlerAdapter adapter = webMvcConfig.requestMappingHandlerAdapter( + this.webMvcConfig.mvcContentNegotiationManager(), + this.webMvcConfig.mvcConversionService(), + this.webMvcConfig.mvcValidator()); + + assertThat(adapter.getMessageConverters().size()).as("One custom converter expected").isEqualTo(2); assertThat(adapter.getMessageConverters().get(0)).isSameAs(customConverter); assertThat(adapter.getMessageConverters().get(1)).isSameAs(stringConverter); } @@ -142,8 +148,8 @@ public class DelegatingWebMvcConfigurationTests { public void getCustomValidator() { given(webMvcConfigurer.getValidator()).willReturn(new LocalValidatorFactoryBean()); - delegatingConfig.setConfigurers(Collections.singletonList(webMvcConfigurer)); - delegatingConfig.mvcValidator(); + webMvcConfig.setConfigurers(Collections.singletonList(webMvcConfigurer)); + webMvcConfig.mvcValidator(); verify(webMvcConfigurer).getValidator(); } @@ -152,16 +158,16 @@ public class DelegatingWebMvcConfigurationTests { public void getCustomMessageCodesResolver() { given(webMvcConfigurer.getMessageCodesResolver()).willReturn(new DefaultMessageCodesResolver()); - delegatingConfig.setConfigurers(Collections.singletonList(webMvcConfigurer)); - delegatingConfig.getMessageCodesResolver(); + webMvcConfig.setConfigurers(Collections.singletonList(webMvcConfigurer)); + webMvcConfig.getMessageCodesResolver(); verify(webMvcConfigurer).getMessageCodesResolver(); } @Test - public void handlerExceptionResolver() throws Exception { - delegatingConfig.setConfigurers(Collections.singletonList(webMvcConfigurer)); - delegatingConfig.handlerExceptionResolver(delegatingConfig.mvcContentNegotiationManager()); + public void handlerExceptionResolver() { + webMvcConfig.setConfigurers(Collections.singletonList(webMvcConfigurer)); + webMvcConfig.handlerExceptionResolver(webMvcConfig.mvcContentNegotiationManager()); verify(webMvcConfigurer).configureMessageConverters(converters.capture()); verify(webMvcConfigurer).configureContentNegotiation(contentNegotiationConfigurer.capture()); @@ -178,29 +184,30 @@ public class DelegatingWebMvcConfigurationTests { } @Test - public void configureExceptionResolvers() throws Exception { - List configurers = new ArrayList<>(); - configurers.add(new WebMvcConfigurer() { + public void configureExceptionResolvers() { + WebMvcConfigurer configurer = new WebMvcConfigurer() { @Override - public void configureHandlerExceptionResolvers(List exceptionResolvers) { - exceptionResolvers.add(new DefaultHandlerExceptionResolver()); + public void configureHandlerExceptionResolvers(List resolvers) { + resolvers.add(new DefaultHandlerExceptionResolver()); } - }); - delegatingConfig.setConfigurers(configurers); + }; + webMvcConfig.setConfigurers(Collections.singletonList(configurer)); HandlerExceptionResolverComposite composite = - (HandlerExceptionResolverComposite) delegatingConfig - .handlerExceptionResolver(delegatingConfig.mvcContentNegotiationManager()); - assertThat(composite.getExceptionResolvers().size()).as("Only one custom converter is expected").isEqualTo(1); + (HandlerExceptionResolverComposite) webMvcConfig + .handlerExceptionResolver(webMvcConfig.mvcContentNegotiationManager()); + + assertThat(composite.getExceptionResolvers().size()) + .as("Only one custom converter is expected") + .isEqualTo(1); } @Test - public void configurePathMatch() throws Exception { - final PathMatcher pathMatcher = mock(PathMatcher.class); - final UrlPathHelper pathHelper = mock(UrlPathHelper.class); + public void configurePathMatcher() { + PathMatcher pathMatcher = mock(PathMatcher.class); + UrlPathHelper pathHelper = mock(UrlPathHelper.class); - List configurers = new ArrayList<>(); - configurers.add(new WebMvcConfigurer() { + WebMvcConfigurer configurer = new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { configurer.setUseRegisteredSuffixPatternMatch(true) @@ -208,18 +215,122 @@ public class DelegatingWebMvcConfigurationTests { .setUrlPathHelper(pathHelper) .setPathMatcher(pathMatcher); } - }); - delegatingConfig.setConfigurers(configurers); + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("home"); + } + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/resources/**").addResourceLocations("/"); + } + }; - RequestMappingHandlerMapping handlerMapping = delegatingConfig.requestMappingHandlerMapping( - delegatingConfig.mvcContentNegotiationManager(), delegatingConfig.mvcConversionService(), - delegatingConfig.mvcResourceUrlProvider()); - assertThat(handlerMapping).isNotNull(); - assertThat(handlerMapping.useRegisteredSuffixPatternMatch()).as("PathMatchConfigurer should configure RegisteredSuffixPatternMatch").isEqualTo(true); - assertThat(handlerMapping.useSuffixPatternMatch()).as("PathMatchConfigurer should configure SuffixPatternMatch").isEqualTo(true); - assertThat(handlerMapping.useTrailingSlashMatch()).as("PathMatchConfigurer should configure TrailingSlashMatch").isEqualTo(false); - assertThat(handlerMapping.getUrlPathHelper()).as("PathMatchConfigurer should configure UrlPathHelper").isEqualTo(pathHelper); - assertThat(handlerMapping.getPathMatcher()).as("PathMatchConfigurer should configure PathMatcher").isEqualTo(pathMatcher); + MockServletContext servletContext = new MockServletContext(); + webMvcConfig.setConfigurers(Collections.singletonList(configurer)); + webMvcConfig.setServletContext(servletContext); + webMvcConfig.setApplicationContext(new GenericWebApplicationContext(servletContext)); + + + BiConsumer configAssertion = (helper, matcher) -> { + assertThat(helper).isSameAs(pathHelper); + assertThat(matcher).isSameAs(pathMatcher); + }; + + RequestMappingHandlerMapping annotationsMapping = webMvcConfig.requestMappingHandlerMapping( + webMvcConfig.mvcContentNegotiationManager(), + webMvcConfig.mvcConversionService(), + webMvcConfig.mvcResourceUrlProvider()); + + assertThat(annotationsMapping).isNotNull(); + assertThat(annotationsMapping.useRegisteredSuffixPatternMatch()).isEqualTo(true); + assertThat(annotationsMapping.useSuffixPatternMatch()).isEqualTo(true); + assertThat(annotationsMapping.useTrailingSlashMatch()).isEqualTo(false); + configAssertion.accept(annotationsMapping.getUrlPathHelper(), annotationsMapping.getPathMatcher()); + + SimpleUrlHandlerMapping mapping = (SimpleUrlHandlerMapping) webMvcConfig.viewControllerHandlerMapping( + webMvcConfig.mvcConversionService(), + webMvcConfig.mvcResourceUrlProvider()); + + assertThat(mapping).isNotNull(); + configAssertion.accept(mapping.getUrlPathHelper(), mapping.getPathMatcher()); + + mapping = (SimpleUrlHandlerMapping) webMvcConfig.resourceHandlerMapping( + webMvcConfig.mvcContentNegotiationManager(), + webMvcConfig.mvcConversionService(), + webMvcConfig.mvcResourceUrlProvider()); + + assertThat(mapping).isNotNull(); + configAssertion.accept(mapping.getUrlPathHelper(), mapping.getPathMatcher()); + + configAssertion.accept( + webMvcConfig.mvcResourceUrlProvider().getUrlPathHelper(), + webMvcConfig.mvcResourceUrlProvider().getPathMatcher()); + + configAssertion.accept(webMvcConfig.mvcUrlPathHelper(), webMvcConfig.mvcPathMatcher()); } + @Test + public void configurePathPatternParser() { + + PathPatternParser patternParser = new PathPatternParser(); + PathMatcher pathMatcher = mock(PathMatcher.class); + UrlPathHelper pathHelper = mock(UrlPathHelper.class); + + WebMvcConfigurer configurer = new WebMvcConfigurer() { + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.setPatternParser(patternParser) + .setUrlPathHelper(pathHelper) + .setPathMatcher(pathMatcher); + } + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("home"); + } + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/resources/**").addResourceLocations("/"); + } + }; + + MockServletContext servletContext = new MockServletContext(); + webMvcConfig.setConfigurers(Collections.singletonList(configurer)); + webMvcConfig.setServletContext(servletContext); + webMvcConfig.setApplicationContext(new GenericWebApplicationContext(servletContext)); + + + BiConsumer configAssertion = (helper, matcher) -> { + assertThat(helper).isNotSameAs(pathHelper); + assertThat(matcher).isNotSameAs(pathMatcher); + }; + + RequestMappingHandlerMapping annotationsMapping = webMvcConfig.requestMappingHandlerMapping( + webMvcConfig.mvcContentNegotiationManager(), + webMvcConfig.mvcConversionService(), + webMvcConfig.mvcResourceUrlProvider()); + + assertThat(annotationsMapping).isNotNull(); + assertThat(annotationsMapping.getPatternParser()).isSameAs(patternParser); + configAssertion.accept(annotationsMapping.getUrlPathHelper(), annotationsMapping.getPathMatcher()); + + SimpleUrlHandlerMapping mapping = (SimpleUrlHandlerMapping) webMvcConfig.viewControllerHandlerMapping( + webMvcConfig.mvcConversionService(), + webMvcConfig.mvcResourceUrlProvider()); + + assertThat(mapping).isNotNull(); + assertThat(mapping.getPatternParser()).isSameAs(patternParser); + configAssertion.accept(mapping.getUrlPathHelper(), mapping.getPathMatcher()); + + mapping = (SimpleUrlHandlerMapping) webMvcConfig.resourceHandlerMapping( + webMvcConfig.mvcContentNegotiationManager(), + webMvcConfig.mvcConversionService(), + webMvcConfig.mvcResourceUrlProvider()); + + assertThat(mapping).isNotNull(); + assertThat(mapping.getPatternParser()).isSameAs(patternParser); + configAssertion.accept(mapping.getUrlPathHelper(), mapping.getPathMatcher()); + + assertThat(webMvcConfig.mvcResourceUrlProvider().getUrlPathHelper()).isSameAs(pathHelper); + assertThat(webMvcConfig.mvcResourceUrlProvider().getPathMatcher()).isSameAs(pathMatcher); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java index 02b3307d60..9f36c0fd6d 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java @@ -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. @@ -150,10 +150,9 @@ public class WebMvcConfigurationSupportExtensionTests { .findFirst() .orElseThrow(() -> new AssertionError("UserController bean not found")) .getKey(); - assertThat(info.getPatternsCondition().getPatterns()).isEqualTo(Collections.singleton("/api/user/{id}")); + assertThat(info.getPatternValues()).isEqualTo(Collections.singleton("/api/user/{id}")); AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) this.config.viewControllerHandlerMapping( - this.config.mvcPathMatcher(), this.config.mvcUrlPathHelper(), this.config.mvcConversionService(), this.config.mvcResourceUrlProvider()); handlerMapping.setApplicationContext(this.context); assertThat(handlerMapping).isNotNull(); @@ -171,7 +170,6 @@ public class WebMvcConfigurationSupportExtensionTests { assertThat(chain.getHandler()).isNotNull(); handlerMapping = (AbstractHandlerMapping) this.config.resourceHandlerMapping( - this.config.mvcUrlPathHelper(), this.config.mvcPathMatcher(), this.config.mvcContentNegotiationManager(), this.config.mvcConversionService(), this.config.mvcResourceUrlProvider()); handlerMapping.setApplicationContext(this.context); @@ -290,7 +288,6 @@ public class WebMvcConfigurationSupportExtensionTests { request.setRequestURI("/resources/foo.gif"); SimpleUrlHandlerMapping handlerMapping = (SimpleUrlHandlerMapping) this.config.resourceHandlerMapping( - this.config.mvcUrlPathHelper(), this.config.mvcPathMatcher(), this.config.mvcContentNegotiationManager(), this.config.mvcConversionService(), this.config.mvcResourceUrlProvider()); handlerMapping.setApplicationContext(this.context); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java index 108111ebee..f4d1330ac3 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java @@ -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. @@ -156,7 +156,7 @@ public class WebMvcConfigurationSupportTests { } @Test - public void requestMappingHandlerAdapter() throws Exception { + public void requestMappingHandlerAdapter() { ApplicationContext context = initContext(WebConfig.class); RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); List> converters = adapter.getMessageConverters(); @@ -196,7 +196,7 @@ public class WebMvcConfigurationSupportTests { } @Test - public void uriComponentsContributor() throws Exception { + public void uriComponentsContributor() { ApplicationContext context = initContext(WebConfig.class); CompositeUriComponentsContributor uriComponentsContributor = context.getBean( MvcUriComponentsBuilder.MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME, @@ -207,7 +207,7 @@ public class WebMvcConfigurationSupportTests { @Test @SuppressWarnings("unchecked") - public void handlerExceptionResolver() throws Exception { + public void handlerExceptionResolver() { ApplicationContext context = initContext(WebConfig.class); HandlerExceptionResolverComposite compositeResolver = context.getBean("handlerExceptionResolver", HandlerExceptionResolverComposite.class); @@ -300,7 +300,7 @@ public class WebMvcConfigurationSupportTests { } @Test - public void defaultPathMatchConfiguration() throws Exception { + public void defaultPathMatchConfiguration() { ApplicationContext context = initContext(WebConfig.class); UrlPathHelper urlPathHelper = context.getBean(UrlPathHelper.class); PathMatcher pathMatcher = context.getBean(PathMatcher.class); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerRequestBuilderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerRequestBuilderTests.java index 627251812b..cc20f5c98f 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerRequestBuilderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerRequestBuilderTests.java @@ -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. @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpMethod; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.web.servlet.handler.PathPatternsTestUtils; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; @@ -35,14 +36,15 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Arjen Poutsma */ -public class DefaultServerRequestBuilderTests { +class DefaultServerRequestBuilderTests { + + private final List> messageConverters = + Collections.singletonList(new StringHttpMessageConverter()); - private final List> messageConverters = Collections.singletonList( - new StringHttpMessageConverter()); @Test - public void from() throws ServletException, IOException { - MockHttpServletRequest request = new MockHttpServletRequest("POST", "https://example.com"); + void from() throws ServletException, IOException { + MockHttpServletRequest request = PathPatternsTestUtils.initRequest("POST", "https://example.com", true); request.addHeader("foo", "bar"); ServerRequest other = ServerRequest.create(request, messageConverters); @@ -65,7 +67,7 @@ public class DefaultServerRequestBuilderTests { assertThat(result.cookies().size()).isEqualTo(2); assertThat(result.cookies().getFirst("foo").getValue()).isEqualTo("bar"); assertThat(result.cookies().getFirst("baz").getValue()).isEqualTo("qux"); - assertThat(result.attributes().size()).isEqualTo(2); + assertThat(result.attributes().size()).isEqualTo(other.attributes().size() + 2); assertThat(result.attributes().get("foo")).isEqualTo("bar"); assertThat(result.attributes().get("baz")).isEqualTo("qux"); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerRequestTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerRequestTests.java index 04ace77cff..7b38b06d50 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerRequestTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerRequestTests.java @@ -51,6 +51,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.servlet.handler.PathPatternsTestUtils; import org.springframework.web.testfixture.server.MockServerWebExchange; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpSession; @@ -66,23 +67,22 @@ import static org.springframework.web.testfixture.http.server.reactive.MockServe * @author Arjen Poutsma * @since 5.1 */ -public class DefaultServerRequestTests { +class DefaultServerRequestTests { private final List> messageConverters = Collections.singletonList( new StringHttpMessageConverter()); @Test - public void method() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("HEAD", "/"); - DefaultServerRequest request = - new DefaultServerRequest(servletRequest, this.messageConverters); + void method() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("HEAD", "/", true); + DefaultServerRequest request = new DefaultServerRequest(servletRequest, this.messageConverters); assertThat(request.method()).isEqualTo(HttpMethod.HEAD); } @Test - public void uri() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + void uri() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); servletRequest.setServerName("example.com"); servletRequest.setScheme("https"); servletRequest.setServerPort(443); @@ -94,8 +94,8 @@ public class DefaultServerRequestTests { } @Test - public void uriBuilder() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/path"); + void uriBuilder() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/path", true); servletRequest.setQueryString("a=1"); DefaultServerRequest request = @@ -110,8 +110,8 @@ public class DefaultServerRequestTests { } @Test - public void attribute() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + void attribute() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); servletRequest.setAttribute("foo", "bar"); DefaultServerRequest request = @@ -121,8 +121,8 @@ public class DefaultServerRequestTests { } @Test - public void params() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + void params() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); servletRequest.setParameter("foo", "bar"); DefaultServerRequest request = @@ -132,11 +132,11 @@ public class DefaultServerRequestTests { } @Test - public void multipartData() throws Exception { + void multipartData() throws Exception { MockPart formPart = new MockPart("form", "foo".getBytes(UTF_8)); MockPart filePart = new MockPart("file", "foo.txt", "foo".getBytes(UTF_8)); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("POST", "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("POST", "/", true); servletRequest.addPart(formPart); servletRequest.addPart(filePart); @@ -151,8 +151,8 @@ public class DefaultServerRequestTests { } @Test - public void emptyQueryParam() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + void emptyQueryParam() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); servletRequest.setParameter("foo", ""); DefaultServerRequest request = @@ -162,8 +162,8 @@ public class DefaultServerRequestTests { } @Test - public void absentQueryParam() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + void absentQueryParam() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); servletRequest.setParameter("foo", ""); DefaultServerRequest request = @@ -173,8 +173,8 @@ public class DefaultServerRequestTests { } @Test - public void pathVariable() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + void pathVariable() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); Map pathVariables = Collections.singletonMap("foo", "bar"); servletRequest .setAttribute(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, pathVariables); @@ -186,8 +186,8 @@ public class DefaultServerRequestTests { } @Test - public void pathVariableNotFound() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + void pathVariableNotFound() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); Map pathVariables = Collections.singletonMap("foo", "bar"); servletRequest .setAttribute(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, pathVariables); @@ -200,8 +200,8 @@ public class DefaultServerRequestTests { } @Test - public void pathVariables() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + void pathVariables() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); Map pathVariables = Collections.singletonMap("foo", "bar"); servletRequest .setAttribute(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, pathVariables); @@ -213,7 +213,7 @@ public class DefaultServerRequestTests { } @Test - public void header() { + void header() { HttpHeaders httpHeaders = new HttpHeaders(); List accept = Collections.singletonList(MediaType.APPLICATION_JSON); @@ -229,7 +229,7 @@ public class DefaultServerRequestTests { List range = Collections.singletonList(HttpRange.createByteRange(0, 42)); httpHeaders.setRange(range); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); httpHeaders.forEach(servletRequest::addHeader); servletRequest.setContentType(MediaType.TEXT_PLAIN_VALUE); @@ -247,10 +247,10 @@ public class DefaultServerRequestTests { } @Test - public void cookies() { + void cookies() { Cookie cookie = new Cookie("foo", "bar"); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); servletRequest.setCookies(cookie); DefaultServerRequest request = new DefaultServerRequest(servletRequest, @@ -264,8 +264,8 @@ public class DefaultServerRequestTests { } @Test - public void bodyClass() throws Exception { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + void bodyClass() throws Exception { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); servletRequest.setContentType(MediaType.TEXT_PLAIN_VALUE); servletRequest.setContent("foo".getBytes(UTF_8)); @@ -277,8 +277,8 @@ public class DefaultServerRequestTests { } @Test - public void bodyParameterizedTypeReference() throws Exception { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + void bodyParameterizedTypeReference() throws Exception { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE); servletRequest.setContent("[\"foo\",\"bar\"]".getBytes(UTF_8)); @@ -292,8 +292,8 @@ public class DefaultServerRequestTests { } @Test - public void bodyUnacceptable() throws Exception { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + void bodyUnacceptable() throws Exception { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); servletRequest.setContentType(MediaType.TEXT_PLAIN_VALUE); servletRequest.setContent("foo".getBytes(UTF_8)); @@ -305,8 +305,8 @@ public class DefaultServerRequestTests { } @Test - public void session() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + void session() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); MockHttpSession session = new MockHttpSession(); servletRequest.setSession(session); @@ -318,8 +318,8 @@ public class DefaultServerRequestTests { } @Test - public void principal() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + void principal() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); Principal principal = new Principal() { @Override public String getName() { @@ -336,7 +336,7 @@ public class DefaultServerRequestTests { @ParameterizedHttpMethodTest void checkNotModifiedTimestamp(String method) throws Exception { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(method, "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest(method, "/", true); Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, now.toEpochMilli()); @@ -352,7 +352,7 @@ public class DefaultServerRequestTests { @ParameterizedHttpMethodTest void checkModifiedTimestamp(String method) { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(method, "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest(method, "/", true); Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); Instant oneMinuteAgo = now.minus(1, ChronoUnit.MINUTES); servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, oneMinuteAgo.toEpochMilli()); @@ -366,7 +366,7 @@ public class DefaultServerRequestTests { @ParameterizedHttpMethodTest void checkNotModifiedETag(String method) { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(method, "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest(method, "/", true); String eTag = "\"Foo\""; servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, eTag); @@ -382,7 +382,7 @@ public class DefaultServerRequestTests { @ParameterizedHttpMethodTest void checkNotModifiedETagWithSeparatorChars(String method) { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(method, "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest(method, "/", true); String eTag = "\"Foo, Bar\""; servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, eTag); @@ -398,7 +398,7 @@ public class DefaultServerRequestTests { @ParameterizedHttpMethodTest void checkModifiedETag(String method) { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(method, "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest(method, "/", true); String currentETag = "\"Foo\""; String oldEtag = "Bar"; servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, oldEtag); @@ -412,7 +412,7 @@ public class DefaultServerRequestTests { @ParameterizedHttpMethodTest void checkNotModifiedUnpaddedETag(String method) { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(method, "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest(method, "/", true); String eTag = "Foo"; String paddedEtag = String.format("\"%s\"", eTag); servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, paddedEtag); @@ -429,7 +429,7 @@ public class DefaultServerRequestTests { @ParameterizedHttpMethodTest void checkModifiedUnpaddedETag(String method) { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(method, "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest(method, "/", true); String currentETag = "Foo"; String oldEtag = "Bar"; servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, oldEtag); @@ -443,7 +443,7 @@ public class DefaultServerRequestTests { @ParameterizedHttpMethodTest void checkNotModifiedWildcardIsIgnored(String method) { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(method, "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest(method, "/", true); String eTag = "\"Foo\""; servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, "*"); DefaultServerRequest request = new DefaultServerRequest(servletRequest, this.messageConverters); @@ -455,7 +455,7 @@ public class DefaultServerRequestTests { @ParameterizedHttpMethodTest void checkNotModifiedETagAndTimestamp(String method) { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(method, "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest(method, "/", true); String eTag = "\"Foo\""; Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, eTag); @@ -475,7 +475,7 @@ public class DefaultServerRequestTests { @ParameterizedHttpMethodTest void checkNotModifiedETagAndModifiedTimestamp(String method) { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(method, "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest(method, "/", true); String eTag = "\"Foo\""; Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); Instant oneMinuteAgo = now.minus(1, ChronoUnit.MINUTES); @@ -498,8 +498,8 @@ public class DefaultServerRequestTests { } @ParameterizedHttpMethodTest - void checkModifiedETagAndNotModifiedTimestamp(String method) throws Exception { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(method, "/"); + void checkModifiedETagAndNotModifiedTimestamp(String method) { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest(method, "/", true); String currentETag = "\"Foo\""; String oldEtag = "\"Bar\""; Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/PathResourceLookupFunctionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/PathResourceLookupFunctionTests.java index 8ca9ff4167..ef82052bc5 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/PathResourceLookupFunctionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/PathResourceLookupFunctionTests.java @@ -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. @@ -25,26 +25,20 @@ import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.servlet.handler.PathPatternsTestUtils; import static org.assertj.core.api.Assertions.assertThat; /** * @author Arjen Poutsma */ -public class PathResourceLookupFunctionTests { +class PathResourceLookupFunctionTests { @Test - public void normal() throws Exception { - ClassPathResource location = - new ClassPathResource("org/springframework/web/servlet/function/"); - - PathResourceLookupFunction function = - new PathResourceLookupFunction("/resources/**", location); - - MockHttpServletRequest servletRequest = - new MockHttpServletRequest("GET", "/resources/response.txt"); - ServerRequest request = new DefaultServerRequest(servletRequest, Collections.emptyList()); + void normal() throws Exception { + ClassPathResource location = new ClassPathResource("org/springframework/web/servlet/function/"); + PathResourceLookupFunction function = new PathResourceLookupFunction("/resources/**", location); + ServerRequest request = initRequest("GET", "/resources/response.txt"); Optional result = function.apply(request); assertThat(result.isPresent()).isTrue(); @@ -54,44 +48,30 @@ public class PathResourceLookupFunctionTests { } @Test - public void subPath() throws Exception { - ClassPathResource location = - new ClassPathResource("org/springframework/web/servlet/function/"); - - PathResourceLookupFunction function = - new PathResourceLookupFunction("/resources/**", location); - - MockHttpServletRequest servletRequest = - new MockHttpServletRequest("GET", "/resources/child/response.txt"); - ServerRequest request = new DefaultServerRequest(servletRequest, Collections.emptyList()); + void subPath() throws Exception { + ClassPathResource location = new ClassPathResource("org/springframework/web/servlet/function/"); + PathResourceLookupFunction function = new PathResourceLookupFunction("/resources/**", location); + ServerRequest request = initRequest("GET", "/resources/child/response.txt"); Optional result = function.apply(request); assertThat(result.isPresent()).isTrue(); - File expected = - new ClassPathResource("org/springframework/web/servlet/function/child/response.txt") - .getFile(); + File expected = new ClassPathResource("org/springframework/web/servlet/function/child/response.txt").getFile(); assertThat(result.get().getFile()).isEqualTo(expected); } @Test - public void notFound() { - ClassPathResource location = - new ClassPathResource("org/springframework/web/reactive/function/server/"); - - PathResourceLookupFunction function = - new PathResourceLookupFunction("/resources/**", location); - - MockHttpServletRequest servletRequest = - new MockHttpServletRequest("GET", "/resources/foo.txt"); - ServerRequest request = new DefaultServerRequest(servletRequest, Collections.emptyList()); + void notFound() { + ClassPathResource location = new ClassPathResource("org/springframework/web/reactive/function/server/"); + PathResourceLookupFunction function = new PathResourceLookupFunction("/resources/**", location); + ServerRequest request = initRequest("GET", "/resources/foo.txt"); Optional result = function.apply(request); assertThat(result.isPresent()).isFalse(); } @Test - public void composeResourceLookupFunction() throws Exception { + void composeResourceLookupFunction() throws Exception { ClassPathResource defaultResource = new ClassPathResource("response.txt", getClass()); Function> lookupFunction = @@ -108,8 +88,7 @@ public class PathResourceLookupFunctionTests { } }); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/resources/foo"); - ServerRequest request = new DefaultServerRequest(servletRequest, Collections.emptyList()); + ServerRequest request = initRequest("GET", "/resources/foo"); Optional result = customLookupFunction.apply(request); assertThat(result.isPresent()).isTrue(); @@ -117,4 +96,10 @@ public class PathResourceLookupFunctionTests { assertThat(result.get().getFile()).isEqualTo(defaultResource.getFile()); } + private ServerRequest initRequest(String httpMethod, String requestUri) { + return new DefaultServerRequest( + PathPatternsTestUtils.initRequest(httpMethod, requestUri, true), + Collections.emptyList()); + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicateTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicateTests.java index 0391bae5ae..cd2fc90c46 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicateTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicateTests.java @@ -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. @@ -21,6 +21,7 @@ import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.web.servlet.handler.PathPatternsTestUtils; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; @@ -28,18 +29,18 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Arjen Poutsma */ -public class RequestPredicateTests { +class RequestPredicateTests { private ServerRequest request; @BeforeEach - public void createRequest() { - this.request = new DefaultServerRequest(new MockHttpServletRequest(), - Collections.emptyList()); + void createRequest() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); + this.request = new DefaultServerRequest(servletRequest, Collections.emptyList()); } @Test - public void and() { + void and() { RequestPredicate predicate1 = request -> true; RequestPredicate predicate2 = request -> true; RequestPredicate predicate3 = request -> false; @@ -50,7 +51,7 @@ public class RequestPredicateTests { } @Test - public void negate() { + void negate() { RequestPredicate predicate = request -> false; RequestPredicate negated = predicate.negate(); @@ -63,7 +64,7 @@ public class RequestPredicateTests { } @Test - public void or() { + void or() { RequestPredicate predicate1 = request -> true; RequestPredicate predicate2 = request -> false; RequestPredicate predicate3 = request -> false; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicatesTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicatesTests.java index 1297bb61ed..1cf7a57f0b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicatesTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicatesTests.java @@ -17,6 +17,7 @@ package org.springframework.web.servlet.function; import java.util.Collections; +import java.util.function.Consumer; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -24,245 +25,189 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.web.servlet.handler.PathPatternsTestUtils; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.pattern.PathPatternParser; -import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.http.MediaType.TEXT_XML_VALUE; /** * @author Arjen Poutsma */ -public class RequestPredicatesTests { +class RequestPredicatesTests { @Test - public void all() { + void all() { RequestPredicate predicate = RequestPredicates.all(); - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest request = initRequest("GET", "/"); assertThat(predicate.test(request)).isTrue(); } @Test - public void method() { + void method() { HttpMethod httpMethod = HttpMethod.GET; RequestPredicate predicate = RequestPredicates.method(httpMethod); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "https://example.com"); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); - assertThat(predicate.test(request)).isTrue(); - - servletRequest.setMethod("POST"); - assertThat(predicate.test(request)).isFalse(); + assertThat(predicate.test(initRequest("GET", "https://example.com"))).isTrue(); + assertThat(predicate.test(initRequest("POST", "https://example.com"))).isFalse(); } @Test - public void methodCorsPreFlight() { + void methodCorsPreFlight() { RequestPredicate predicate = RequestPredicates.method(HttpMethod.PUT); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("OPTIONS", "https://example.com"); - servletRequest.addHeader("Origin", "https://example.com"); - servletRequest.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest request = initRequest("OPTIONS", "https://example.com", servletRequest -> { + servletRequest.addHeader("Origin", "https://example.com"); + servletRequest.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + }); assertThat(predicate.test(request)).isTrue(); - servletRequest.removeHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD); - servletRequest.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); - request = new DefaultServerRequest(servletRequest, emptyList()); + request = initRequest("OPTIONS", "https://example.com", servletRequest -> { + servletRequest.removeHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD); + servletRequest.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); + }); assertThat(predicate.test(request)).isFalse(); } @Test - public void methods() { + void methods() { RequestPredicate predicate = RequestPredicates.methods(HttpMethod.GET, HttpMethod.HEAD); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "https://example.com"); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); - assertThat(predicate.test(request)).isTrue(); - - servletRequest.setMethod("HEAD"); - assertThat(predicate.test(request)).isTrue(); - - servletRequest.setMethod("POST"); - assertThat(predicate.test(request)).isFalse(); + assertThat(predicate.test(initRequest("GET", "https://example.com"))).isTrue(); + assertThat(predicate.test(initRequest("HEAD", "https://example.com"))).isTrue(); + assertThat(predicate.test(initRequest("POST", "https://example.com"))).isFalse(); } @Test - public void allMethods() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/path"); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); - + void allMethods() { RequestPredicate predicate = RequestPredicates.GET("/p*"); - assertThat(predicate.test(request)).isTrue(); + assertThat(predicate.test(initRequest("GET", "/path"))).isTrue(); predicate = RequestPredicates.HEAD("/p*"); - servletRequest.setMethod("HEAD"); - assertThat(predicate.test(request)).isTrue(); + assertThat(predicate.test(initRequest("HEAD", "/path"))).isTrue(); predicate = RequestPredicates.POST("/p*"); - servletRequest.setMethod("POST"); - assertThat(predicate.test(request)).isTrue(); + assertThat(predicate.test(initRequest("POST", "/path"))).isTrue(); predicate = RequestPredicates.PUT("/p*"); - servletRequest.setMethod("PUT"); - assertThat(predicate.test(request)).isTrue(); + assertThat(predicate.test(initRequest("PUT", "/path"))).isTrue(); predicate = RequestPredicates.PATCH("/p*"); - servletRequest.setMethod("PATCH"); - assertThat(predicate.test(request)).isTrue(); + assertThat(predicate.test(initRequest("PATCH", "/path"))).isTrue(); predicate = RequestPredicates.DELETE("/p*"); - servletRequest.setMethod("DELETE"); - assertThat(predicate.test(request)).isTrue(); + assertThat(predicate.test(initRequest("DELETE", "/path"))).isTrue(); predicate = RequestPredicates.OPTIONS("/p*"); - servletRequest.setMethod("OPTIONS"); - assertThat(predicate.test(request)).isTrue(); + assertThat(predicate.test(initRequest("OPTIONS", "/path"))).isTrue(); } @Test - public void path() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/path"); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); + void path() { RequestPredicate predicate = RequestPredicates.path("/p*"); - assertThat(predicate.test(request)).isTrue(); - - servletRequest = new MockHttpServletRequest("GET", "/foo"); - request = new DefaultServerRequest(servletRequest, emptyList()); - assertThat(predicate.test(request)).isFalse(); + assertThat(predicate.test(initRequest("GET", "/path"))).isTrue(); + assertThat(predicate.test(initRequest("GET", "/foo"))).isFalse(); } @Test - public void servletPath() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/foo/bar"); - servletRequest.setServletPath("/foo"); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); + void contextPath() { RequestPredicate predicate = RequestPredicates.path("/bar"); + + ServerRequest request = new DefaultServerRequest( + PathPatternsTestUtils.initRequest("GET", "/foo", "/bar", true), + Collections.emptyList()); + assertThat(predicate.test(request)).isTrue(); - servletRequest = new MockHttpServletRequest("GET", "/foo"); - request = new DefaultServerRequest(servletRequest, emptyList()); + request = initRequest("GET", "/foo"); assertThat(predicate.test(request)).isFalse(); } @Test - public void pathNoLeadingSlash() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/path"); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); + void pathNoLeadingSlash() { RequestPredicate predicate = RequestPredicates.path("p*"); - assertThat(predicate.test(request)).isTrue(); + assertThat(predicate.test(initRequest("GET", "/path"))).isTrue(); } @Test - public void pathEncoded() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/foo%20bar"); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); - + void pathEncoded() { RequestPredicate predicate = RequestPredicates.path("/foo bar"); - assertThat(predicate.test(request)).isTrue(); - - servletRequest = new MockHttpServletRequest(); - request = new DefaultServerRequest(servletRequest, emptyList()); - assertThat(predicate.test(request)).isFalse(); + assertThat(predicate.test(initRequest("GET", "/foo%20bar"))).isTrue(); + assertThat(predicate.test(initRequest("GET", ""))).isFalse(); } @Test - public void pathPredicates() { + void pathPredicates() { PathPatternParser parser = new PathPatternParser(); parser.setCaseSensitive(false); Function pathPredicates = RequestPredicates.pathPredicates(parser); RequestPredicate predicate = pathPredicates.apply("/P*"); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/path"); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); - assertThat(predicate.test(request)).isTrue(); + assertThat(predicate.test(initRequest("GET", "/path"))).isTrue(); } @Test - public void headers() { + void headers() { String name = "MyHeader"; String value = "MyValue"; RequestPredicate predicate = RequestPredicates.headers( headers -> headers.header(name).equals(Collections.singletonList(value))); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/path"); - servletRequest.addHeader(name, value); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest request = initRequest("GET", "/path", req -> req.addHeader(name, value)); assertThat(predicate.test(request)).isTrue(); - - servletRequest = new MockHttpServletRequest(); - request = new DefaultServerRequest(servletRequest, emptyList()); - assertThat(predicate.test(request)).isFalse(); + assertThat(predicate.test(initRequest("GET", ""))).isFalse(); } @Test - public void headersCors() { + void headersCors() { RequestPredicate predicate = RequestPredicates.headers(headers -> false); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("OPTIONS", "https://example.com"); - servletRequest.addHeader("Origin", "https://example.com"); - servletRequest.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest request = initRequest("OPTIONS", "https://example.com", servletRequest -> { + servletRequest.addHeader("Origin", "https://example.com"); + servletRequest.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + }); assertThat(predicate.test(request)).isTrue(); } @Test - public void contentType() { + void contentType() { MediaType json = MediaType.APPLICATION_JSON; RequestPredicate predicate = RequestPredicates.contentType(json); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/path"); - servletRequest.setContentType(json.toString()); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); - + ServerRequest request = initRequest("GET", "/path", req -> req.setContentType(json.toString())); assertThat(predicate.test(request)).isTrue(); - - servletRequest = new MockHttpServletRequest(); - request = new DefaultServerRequest(servletRequest, emptyList()); - assertThat(predicate.test(request)).isFalse(); + assertThat(predicate.test(initRequest("GET", ""))).isFalse(); } @Test - public void accept() { + void accept() { MediaType json = MediaType.APPLICATION_JSON; RequestPredicate predicate = RequestPredicates.accept(json); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/path"); - servletRequest.addHeader("Accept", json.toString()); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest request = initRequest("GET", "/path", req -> req.addHeader("Accept", json.toString())); assertThat(predicate.test(request)).isTrue(); - servletRequest = new MockHttpServletRequest(); - servletRequest.addHeader("Accept", TEXT_XML_VALUE); - request = new DefaultServerRequest(servletRequest, emptyList()); + request = initRequest("GET", "", req -> req.addHeader("Accept", TEXT_XML_VALUE)); assertThat(predicate.test(request)).isFalse(); } @Test - public void pathExtension() { + void pathExtension() { RequestPredicate predicate = RequestPredicates.pathExtension("txt"); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/file.txt"); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); - assertThat(predicate.test(request)).isTrue(); - - servletRequest = new MockHttpServletRequest("GET", "/FILE.TXT"); - request = new DefaultServerRequest(servletRequest, emptyList()); - assertThat(predicate.test(request)).isTrue(); + assertThat(predicate.test(initRequest("GET", "/file.txt"))).isTrue(); + assertThat(predicate.test(initRequest("GET", "/FILE.TXT"))).isTrue(); predicate = RequestPredicates.pathExtension("bar"); - assertThat(predicate.test(request)).isFalse(); + assertThat(predicate.test(initRequest("GET", "/FILE.TXT"))).isFalse(); - servletRequest = new MockHttpServletRequest("GET", "/file.foo"); - request = new DefaultServerRequest(servletRequest, emptyList()); - assertThat(predicate.test(request)).isFalse(); + assertThat(predicate.test(initRequest("GET", "/file.foo"))).isFalse(); } @Test - public void param() { + void param() { RequestPredicate predicate = RequestPredicates.param("foo", "bar"); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/path"); - servletRequest.addParameter("foo", "bar"); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest request = initRequest("GET", "/path", req -> req.addParameter("foo", "bar")); assertThat(predicate.test(request)).isTrue(); predicate = RequestPredicates.param("foo", s -> s.equals("bar")); @@ -275,4 +220,17 @@ public class RequestPredicatesTests { assertThat(predicate.test(request)).isFalse(); } + + private ServerRequest initRequest(String httpMethod, String requestUri) { + return initRequest(httpMethod, requestUri, null); + } + + private ServerRequest initRequest( + String httpMethod, String requestUri, @Nullable Consumer initializer) { + + return new DefaultServerRequest( + PathPatternsTestUtils.initRequest(httpMethod, null, requestUri, true, initializer), + Collections.emptyList()); + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java index ed24be9c27..ccefb80b45 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java @@ -22,7 +22,6 @@ import java.nio.file.Files; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; -import java.util.List; import javax.servlet.ServletException; @@ -35,10 +34,10 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.ResourceRegionHttpMessageConverter; import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.handler.PathPatternsTestUtils; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; @@ -61,19 +60,13 @@ public class ResourceHandlerFunctionTests { public void createContext() { this.messageConverter = new ResourceHttpMessageConverter(); ResourceRegionHttpMessageConverter regionConverter = new ResourceRegionHttpMessageConverter(); - this.context = new ServerResponse.Context() { - @Override - public List> messageConverters() { - return Arrays.asList(messageConverter, regionConverter); - } - - }; + this.context = () -> Arrays.asList(messageConverter, regionConverter); } @Test public void get() throws IOException, ServletException { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); ServerRequest request = new DefaultServerRequest(servletRequest, Collections.singletonList(messageConverter)); ServerResponse response = this.handlerFunction.handle(request); @@ -97,7 +90,7 @@ public class ResourceHandlerFunctionTests { @Test public void getRange() throws IOException, ServletException { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); servletRequest.addHeader("Range", "bytes=0-5"); ServerRequest request = new DefaultServerRequest(servletRequest, Collections.singletonList(messageConverter)); @@ -126,7 +119,7 @@ public class ResourceHandlerFunctionTests { @Test public void getInvalidRange() throws IOException, ServletException { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); servletRequest.addHeader("Range", "bytes=0-10, 0-10, 0-10, 0-10, 0-10, 0-10"); ServerRequest request = new DefaultServerRequest(servletRequest, Collections.singletonList(messageConverter)); @@ -152,7 +145,7 @@ public class ResourceHandlerFunctionTests { @Test public void head() throws IOException, ServletException { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("HEAD", "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("HEAD", "/", true); ServerRequest request = new DefaultServerRequest(servletRequest, Collections.singletonList(messageConverter)); ServerResponse response = this.handlerFunction.handle(request); @@ -174,10 +167,9 @@ public class ResourceHandlerFunctionTests { assertThat(servletResponse.getContentLength()).isEqualTo(this.resource.contentLength()); } - @Test public void options() throws ServletException, IOException { - MockHttpServletRequest servletRequest = new MockHttpServletRequest("OPTIONS", "/"); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("OPTIONS", "/", true); ServerRequest request = new DefaultServerRequest(servletRequest, Collections.singletonList(messageConverter)); ServerResponse response = this.handlerFunction.handle(request); @@ -194,5 +186,4 @@ public class ResourceHandlerFunctionTests { assertThat(actualBytes.length).isEqualTo(0); } - } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionBuilderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionBuilderTests.java index 278a3871d4..55d0e484a9 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionBuilderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionBuilderTests.java @@ -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. @@ -18,6 +18,7 @@ package org.springframework.web.servlet.function; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; @@ -25,6 +26,8 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.web.servlet.handler.PathPatternsTestUtils; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import static java.util.Collections.emptyList; @@ -36,10 +39,10 @@ import static org.springframework.web.servlet.function.RequestPredicates.HEAD; * * @author Arjen Poutsma */ -public class RouterFunctionBuilderTests { +class RouterFunctionBuilderTests { @Test - public void route() { + void route() { RouterFunction route = RouterFunctions.route() .GET("/foo", request -> ServerResponse.ok().build()) .POST("/", RequestPredicates.contentType(MediaType.TEXT_PLAIN), @@ -47,8 +50,7 @@ public class RouterFunctionBuilderTests { .route(HEAD("/foo"), request -> ServerResponse.accepted().build()) .build(); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/foo"); - ServerRequest getFooRequest = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest getFooRequest = initRequest("GET", "/foo"); Optional responseStatus = route.route(getFooRequest) .map(handlerFunction -> handle(handlerFunction, getFooRequest)) @@ -56,8 +58,7 @@ public class RouterFunctionBuilderTests { .map(HttpStatus::value); assertThat(responseStatus.get().intValue()).isEqualTo(200); - servletRequest = new MockHttpServletRequest("HEAD", "/foo"); - ServerRequest headFooRequest = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest headFooRequest = initRequest("HEAD", "/foo"); responseStatus = route.route(headFooRequest) .map(handlerFunction -> handle(handlerFunction, getFooRequest)) @@ -65,9 +66,7 @@ public class RouterFunctionBuilderTests { .map(HttpStatus::value); assertThat(responseStatus.get().intValue()).isEqualTo(202); - servletRequest = new MockHttpServletRequest("POST", "/"); - servletRequest.setContentType("text/plain"); - ServerRequest barRequest = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest barRequest = initRequest("POST", "/", req -> req.setContentType("text/plain")); responseStatus = route.route(barRequest) .map(handlerFunction -> handle(handlerFunction, barRequest)) @@ -75,8 +74,7 @@ public class RouterFunctionBuilderTests { .map(HttpStatus::value); assertThat(responseStatus.get().intValue()).isEqualTo(204); - servletRequest = new MockHttpServletRequest("POST", "/"); - ServerRequest invalidRequest = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest invalidRequest = initRequest("POST", "/"); responseStatus = route.route(invalidRequest) .map(handlerFunction -> handle(handlerFunction, invalidRequest)) @@ -84,7 +82,6 @@ public class RouterFunctionBuilderTests { .map(HttpStatus::value); assertThat(responseStatus.isPresent()).isFalse(); - } private static ServerResponse handle(HandlerFunction handlerFunction, @@ -98,7 +95,7 @@ public class RouterFunctionBuilderTests { } @Test - public void resources() { + void resources() { Resource resource = new ClassPathResource("/org/springframework/web/servlet/function/"); assertThat(resource.exists()).isTrue(); @@ -106,9 +103,7 @@ public class RouterFunctionBuilderTests { .resources("/resources/**", resource) .build(); - MockHttpServletRequest servletRequest = - new MockHttpServletRequest("GET", "/resources/response.txt"); - ServerRequest resourceRequest = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest resourceRequest = initRequest("GET", "/resources/response.txt"); Optional responseStatus = route.route(resourceRequest) .map(handlerFunction -> handle(handlerFunction, resourceRequest)) @@ -116,8 +111,7 @@ public class RouterFunctionBuilderTests { .map(HttpStatus::value); assertThat(responseStatus.get().intValue()).isEqualTo(200); - servletRequest = new MockHttpServletRequest("POST", "/resources/foo.txt"); - ServerRequest invalidRequest = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest invalidRequest = initRequest("POST", "/resources/foo.txt"); responseStatus = route.route(invalidRequest) .map(handlerFunction -> handle(handlerFunction, invalidRequest)) @@ -127,7 +121,7 @@ public class RouterFunctionBuilderTests { } @Test - public void nest() { + void nest() { RouterFunction route = RouterFunctions.route() .path("/foo", builder -> builder.path("/bar", @@ -136,8 +130,7 @@ public class RouterFunctionBuilderTests { .build())) .build(); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/foo/bar/baz"); - ServerRequest fooRequest = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest fooRequest = initRequest("GET", "/foo/bar/baz"); Optional responseStatus = route.route(fooRequest) .map(handlerFunction -> handle(handlerFunction, fooRequest)) @@ -147,7 +140,7 @@ public class RouterFunctionBuilderTests { } @Test - public void filters() { + void filters() { AtomicInteger filterCount = new AtomicInteger(); RouterFunction route = RouterFunctions.route() @@ -178,8 +171,7 @@ public class RouterFunctionBuilderTests { .build()) .build(); - MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/foo"); - ServerRequest fooRequest = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest fooRequest = initRequest("GET", "/foo"); route.route(fooRequest) .map(handlerFunction -> handle(handlerFunction, fooRequest)); @@ -187,8 +179,7 @@ public class RouterFunctionBuilderTests { filterCount.set(0); - servletRequest = new MockHttpServletRequest("GET", "/bar"); - ServerRequest barRequest = new DefaultServerRequest(servletRequest, emptyList()); + ServerRequest barRequest = initRequest("GET", "/bar"); Optional responseStatus = route.route(barRequest) .map(handlerFunction -> handle(handlerFunction, barRequest)) @@ -197,4 +188,16 @@ public class RouterFunctionBuilderTests { assertThat(responseStatus.get().intValue()).isEqualTo(500); } + + private ServerRequest initRequest(String httpMethod, String requestUri) { + return initRequest(httpMethod, requestUri, null); + } + + private ServerRequest initRequest( + String httpMethod, String requestUri, @Nullable Consumer consumer) { + + return new DefaultServerRequest( + PathPatternsTestUtils.initRequest(httpMethod, null, requestUri, true, consumer), emptyList()); + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionTests.java index 7ae3437827..6a1345663b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionTests.java @@ -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. @@ -16,22 +16,26 @@ package org.springframework.web.servlet.function; +import java.util.Collections; import java.util.Optional; import org.junit.jupiter.api.Test; -import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.servlet.handler.PathPatternsTestUtils; -import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; /** * @author Arjen Poutsma */ -public class RouterFunctionTests { +class RouterFunctionTests { + + private final ServerRequest request = new DefaultServerRequest( + PathPatternsTestUtils.initRequest("GET", "", true), Collections.emptyList()); + @Test - public void and() { + void and() { HandlerFunction handlerFunction = request -> ServerResponse.ok().build(); RouterFunction routerFunction1 = request -> Optional.empty(); RouterFunction routerFunction2 = request -> Optional.of(handlerFunction); @@ -39,9 +43,6 @@ public class RouterFunctionTests { RouterFunction result = routerFunction1.and(routerFunction2); assertThat(result).isNotNull(); - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); - Optional> resultHandlerFunction = result.route(request); assertThat(resultHandlerFunction.isPresent()).isTrue(); assertThat(resultHandlerFunction.get()).isEqualTo(handlerFunction); @@ -49,7 +50,7 @@ public class RouterFunctionTests { @Test - public void andOther() { + void andOther() { HandlerFunction handlerFunction = request -> ServerResponse.ok().body("42"); RouterFunction routerFunction1 = request -> Optional.empty(); RouterFunction routerFunction2 = request -> Optional.of(handlerFunction); @@ -57,9 +58,6 @@ public class RouterFunctionTests { RouterFunction result = routerFunction1.andOther(routerFunction2); assertThat(result).isNotNull(); - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); - Optional> resultHandlerFunction = result.route(request); assertThat(resultHandlerFunction.isPresent()).isTrue(); assertThat(resultHandlerFunction.get()).isEqualTo(handlerFunction); @@ -67,23 +65,20 @@ public class RouterFunctionTests { @Test - public void andRoute() { + void andRoute() { RouterFunction routerFunction1 = request -> Optional.empty(); RequestPredicate requestPredicate = request -> true; RouterFunction result = routerFunction1.andRoute(requestPredicate, this::handlerMethod); assertThat(result).isNotNull(); - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); - Optional> resultHandlerFunction = result.route(request); assertThat(resultHandlerFunction.isPresent()).isTrue(); } @Test - public void filter() { + void filter() { String string = "42"; HandlerFunction> handlerFunction = request -> EntityResponse.fromObject(string).build(); @@ -100,9 +95,6 @@ public class RouterFunctionTests { RouterFunction> result = routerFunction.filter(filterFunction); assertThat(result).isNotNull(); - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - ServerRequest request = new DefaultServerRequest(servletRequest, emptyList()); - Optional> resultHandlerFunction = result.route(request) .map(hf -> { try { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionsTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionsTests.java index a8f697628f..f46a377044 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionsTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionsTests.java @@ -21,7 +21,7 @@ import java.util.Optional; import org.junit.jupiter.api.Test; -import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.servlet.handler.PathPatternsTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -32,12 +32,14 @@ import static org.mockito.Mockito.mock; */ public class RouterFunctionsTests { + private final ServerRequest request = new DefaultServerRequest( + PathPatternsTestUtils.initRequest("GET", "", true), Collections.emptyList()); + + @Test public void routeMatch() { HandlerFunction handlerFunction = request -> ServerResponse.ok().build(); - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - ServerRequest request = new DefaultServerRequest(servletRequest, Collections.emptyList()); RequestPredicate requestPredicate = mock(RequestPredicate.class); given(requestPredicate.test(request)).willReturn(true); @@ -54,8 +56,6 @@ public class RouterFunctionsTests { public void routeNoMatch() { HandlerFunction handlerFunction = request -> ServerResponse.ok().build(); - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - ServerRequest request = new DefaultServerRequest(servletRequest, Collections.emptyList()); RequestPredicate requestPredicate = mock(RequestPredicate.class); given(requestPredicate.test(request)).willReturn(false); @@ -71,8 +71,6 @@ public class RouterFunctionsTests { HandlerFunction handlerFunction = request -> ServerResponse.ok().build(); RouterFunction routerFunction = request -> Optional.of(handlerFunction); - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - ServerRequest request = new DefaultServerRequest(servletRequest, Collections.emptyList()); RequestPredicate requestPredicate = mock(RequestPredicate.class); given(requestPredicate.nest(request)).willReturn(Optional.of(request)); @@ -89,8 +87,6 @@ public class RouterFunctionsTests { HandlerFunction handlerFunction = request -> ServerResponse.ok().build(); RouterFunction routerFunction = request -> Optional.of(handlerFunction); - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - ServerRequest request = new DefaultServerRequest(servletRequest, Collections.emptyList()); RequestPredicate requestPredicate = mock(RequestPredicate.class); given(requestPredicate.nest(request)).willReturn(Optional.empty()); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/CorsAbstractHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/CorsAbstractHandlerMappingTests.java index 5a37e119c9..a9aa4f37b9 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/CorsAbstractHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/CorsAbstractHandlerMappingTests.java @@ -16,22 +16,18 @@ package org.springframework.web.servlet.handler; -import java.io.IOException; import java.util.Collections; +import java.util.stream.Stream; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.DirectFieldAccessor; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.util.ObjectUtils; +import org.springframework.http.server.RequestPath; +import org.springframework.lang.Nullable; import org.springframework.web.HttpRequestHandler; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; @@ -39,6 +35,8 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.support.WebContentGenerator; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -50,205 +48,214 @@ import static org.mockito.Mockito.mock; */ class CorsAbstractHandlerMappingTests { - private MockHttpServletRequest request; - - private AbstractHandlerMapping handlerMapping; - - - @BeforeEach - void setup() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - this.handlerMapping = new TestHandlerMapping(); - this.handlerMapping.setInterceptors(mock(HandlerInterceptor.class)); - this.handlerMapping.setApplicationContext(context); - this.request = new MockHttpServletRequest(); - this.request.setRemoteHost("domain1.com"); + @SuppressWarnings("unused") + private static Stream pathPatternsArguments() { + return Stream.of(new TestHandlerMapping(new PathPatternParser()), new TestHandlerMapping()); } - @Test - void actualRequestWithoutCorsConfigurationProvider() throws Exception { - this.request.setMethod(RequestMethod.GET.name()); - this.request.setRequestURI("/foo"); - this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); - this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + + @PathPatternsParameterizedTest + void actualRequestWithoutCorsConfig(TestHandlerMapping mapping) throws Exception { + + HandlerExecutionChain chain = mapping.getHandler(getCorsRequest("/foo")); assertThat(chain).isNotNull(); assertThat(chain.getHandler()).isInstanceOf(SimpleHandler.class); + assertThat(mapping.hasSavedCorsConfig()).isFalse(); } - @Test - void preflightRequestWithoutCorsConfigurationProvider() throws Exception { - this.request.setMethod(RequestMethod.OPTIONS.name()); - this.request.setRequestURI("/foo"); - this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); - this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + @PathPatternsParameterizedTest + void preflightRequestWithoutCorsConfig(TestHandlerMapping mapping) throws Exception { + + HandlerExecutionChain chain = mapping.getHandler(getPreFlightRequest("/foo")); assertThat(chain).isNotNull(); assertThat(chain.getHandler()).isNotNull(); assertThat(chain.getHandler().getClass().getSimpleName()).isEqualTo("PreFlightHandler"); + assertThat(mapping.hasSavedCorsConfig()).isFalse(); } - @Test - void actualRequestWithCorsConfigurationProvider() throws Exception { - this.request.setMethod(RequestMethod.GET.name()); - this.request.setRequestURI("/cors"); - this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); - this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + @PathPatternsParameterizedTest + void actualRequestWithCorsConfigProvider(TestHandlerMapping mapping) throws Exception { + + HandlerExecutionChain chain = mapping.getHandler(getCorsRequest("/cors")); assertThat(chain).isNotNull(); assertThat(chain.getHandler()).isInstanceOf(CorsAwareHandler.class); - assertThat(getRequiredCorsConfiguration(chain, false).getAllowedOrigins()).containsExactly("*"); + assertThat(mapping.getRequiredCorsConfig().getAllowedOrigins()).containsExactly("*"); } - @Test // see gh-23843 - void actualRequestWithCorsConfigurationProviderForHandlerChain() throws Exception { - this.request.setMethod(RequestMethod.GET.name()); - this.request.setRequestURI("/chain"); - this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); - this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + @PathPatternsParameterizedTest // see gh-23843 + void actualRequestWithCorsConfigProviderForHandlerChain(TestHandlerMapping mapping) throws Exception { + + HandlerExecutionChain chain = mapping.getHandler(getCorsRequest("/chain")); assertThat(chain).isNotNull(); assertThat(chain.getHandler()).isInstanceOf(CorsAwareHandler.class); - assertThat(getRequiredCorsConfiguration(chain, false).getAllowedOrigins()).containsExactly("*"); + assertThat(mapping.getRequiredCorsConfig().getAllowedOrigins()).containsExactly("*"); } - @Test - void preflightRequestWithCorsConfigurationProvider() throws Exception { - this.request.setMethod(RequestMethod.OPTIONS.name()); - this.request.setRequestURI("/cors"); - this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); - this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + @PathPatternsParameterizedTest + void preflightRequestWithCorsConfigProvider(TestHandlerMapping mapping) throws Exception { + + HandlerExecutionChain chain = mapping.getHandler(getPreFlightRequest("/cors")); assertThat(chain).isNotNull(); assertThat(chain.getHandler()).isNotNull(); assertThat(chain.getHandler().getClass().getSimpleName()).isEqualTo("PreFlightHandler"); - assertThat(getRequiredCorsConfiguration(chain, true).getAllowedOrigins()).containsExactly("*"); + assertThat(mapping.getRequiredCorsConfig().getAllowedOrigins()).containsExactly("*"); } - @Test - void actualRequestWithMappedCorsConfiguration() throws Exception { + @PathPatternsParameterizedTest + void actualRequestWithMappedCorsConfig(TestHandlerMapping mapping) throws Exception { + CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); - this.handlerMapping.setCorsConfigurations(Collections.singletonMap("/foo", config)); - this.request.setMethod(RequestMethod.GET.name()); - this.request.setRequestURI("/foo"); - this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); - this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + mapping.setCorsConfigurations(Collections.singletonMap("/foo", config)); + + HandlerExecutionChain chain = mapping.getHandler(getCorsRequest("/foo")); assertThat(chain).isNotNull(); assertThat(chain.getHandler()).isInstanceOf(SimpleHandler.class); - assertThat(getRequiredCorsConfiguration(chain, false).getAllowedOrigins()).containsExactly("*"); + assertThat(mapping.getRequiredCorsConfig().getAllowedOrigins()).containsExactly("*"); } - @Test - void preflightRequestWithMappedCorsConfiguration() throws Exception { + @PathPatternsParameterizedTest + void preflightRequestWithMappedCorsConfig(TestHandlerMapping mapping) throws Exception { + CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); - this.handlerMapping.setCorsConfigurations(Collections.singletonMap("/foo", config)); - this.request.setMethod(RequestMethod.OPTIONS.name()); - this.request.setRequestURI("/foo"); - this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); - this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + mapping.setCorsConfigurations(Collections.singletonMap("/foo", config)); + + HandlerExecutionChain chain = mapping.getHandler(getPreFlightRequest("/foo")); assertThat(chain).isNotNull(); assertThat(chain.getHandler()).isNotNull(); assertThat(chain.getHandler().getClass().getSimpleName()).isEqualTo("PreFlightHandler"); - assertThat(getRequiredCorsConfiguration(chain, true).getAllowedOrigins()).containsExactly("*"); + assertThat(mapping.getRequiredCorsConfig().getAllowedOrigins()).containsExactly("*"); } - @Test - void actualRequestWithCorsConfigurationSource() throws Exception { - this.handlerMapping.setCorsConfigurationSource(new CustomCorsConfigurationSource()); - this.request.setMethod(RequestMethod.GET.name()); - this.request.setRequestURI("/foo"); - this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); - this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + @PathPatternsParameterizedTest + void actualRequestWithCorsConfigSource(TestHandlerMapping mapping) throws Exception { + + mapping.setCorsConfigurationSource(new CustomCorsConfigurationSource()); + HandlerExecutionChain chain = mapping.getHandler(getCorsRequest("/foo")); assertThat(chain).isNotNull(); assertThat(chain.getHandler()).isInstanceOf(SimpleHandler.class); - CorsConfiguration config = getRequiredCorsConfiguration(chain, false); + + CorsConfiguration config = mapping.getRequiredCorsConfig(); assertThat(config).isNotNull(); assertThat(config.getAllowedOrigins()).containsExactly("*"); assertThat(config.getAllowCredentials()).isTrue(); } - @Test - void preflightRequestWithCorsConfigurationSource() throws Exception { - this.handlerMapping.setCorsConfigurationSource(new CustomCorsConfigurationSource()); - this.request.setMethod(RequestMethod.OPTIONS.name()); - this.request.setRequestURI("/foo"); - this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); - this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + @PathPatternsParameterizedTest + void preflightRequestWithCorsConfigSource(TestHandlerMapping mapping) throws Exception { + + mapping.setCorsConfigurationSource(new CustomCorsConfigurationSource()); + HandlerExecutionChain chain = mapping.getHandler(getPreFlightRequest("/foo")); assertThat(chain).isNotNull(); assertThat(chain.getHandler()).isNotNull(); assertThat(chain.getHandler().getClass().getSimpleName()).isEqualTo("PreFlightHandler"); - CorsConfiguration config = getRequiredCorsConfiguration(chain, true); + + CorsConfiguration config = mapping.getRequiredCorsConfig(); assertThat(config).isNotNull(); assertThat(config.getAllowedOrigins()).containsExactly("*"); assertThat(config.getAllowCredentials()).isTrue(); } - @SuppressWarnings("ConstantConditions") - private CorsConfiguration getRequiredCorsConfiguration(HandlerExecutionChain chain, boolean isPreFlightRequest) { - CorsConfiguration corsConfig = null; - if (isPreFlightRequest) { - Object handler = chain.getHandler(); - assertThat(handler.getClass().getSimpleName()).isEqualTo("PreFlightHandler"); - DirectFieldAccessor accessor = new DirectFieldAccessor(handler); - corsConfig = (CorsConfiguration) accessor.getPropertyValue("config"); + private MockHttpServletRequest getCorsRequest(String requestURI) { + return createCorsRequest(HttpMethod.GET, requestURI); + } + + private MockHttpServletRequest getPreFlightRequest(String requestURI) { + return createCorsRequest(HttpMethod.OPTIONS, requestURI); + } + + private MockHttpServletRequest createCorsRequest(HttpMethod method, String requestURI) { + MockHttpServletRequest request = new MockHttpServletRequest(method.name(), requestURI); + request.setRemoteHost("domain1.com"); + request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + return request; + } + + + private static class TestHandlerMapping extends AbstractHandlerMapping { + + @Nullable + private CorsConfiguration savedCorsConfig; + + + TestHandlerMapping() { + this(null); } - else { - HandlerInterceptor[] interceptors = chain.getInterceptors(); - if (!ObjectUtils.isEmpty(interceptors)) { - DirectFieldAccessor accessor = new DirectFieldAccessor(interceptors[0]); - corsConfig = (CorsConfiguration) accessor.getPropertyValue("config"); + + TestHandlerMapping(@Nullable PathPatternParser parser) { + setInterceptors(mock(HandlerInterceptor.class)); + setApplicationContext(new StaticWebApplicationContext()); + if (parser != null) { + setPatternParser(parser); } } - assertThat(corsConfig).isNotNull(); - return corsConfig; - } - public class TestHandlerMapping extends AbstractHandlerMapping { + boolean hasSavedCorsConfig() { + return this.savedCorsConfig != null; + } + + CorsConfiguration getRequiredCorsConfig() { + assertThat(this.savedCorsConfig).isNotNull(); + return this.savedCorsConfig; + } @Override - protected Object getHandlerInternal(HttpServletRequest request) throws Exception { - if (request.getRequestURI().equals("/cors")) { + protected Object getHandlerInternal(HttpServletRequest request) { + String lookupPath = initLookupPath(request); + if (lookupPath.equals("/cors")) { return new CorsAwareHandler(); } - else if (request.getRequestURI().equals("/chain")) { + else if (lookupPath.equals("/chain")) { return new HandlerExecutionChain(new CorsAwareHandler()); } return new SimpleHandler(); } + + @Override + protected String initLookupPath(HttpServletRequest request) { + // At runtime this is done by the DispatcherServlet + if (getPatternParser() != null) { + RequestPath requestPath = ServletRequestPathUtils.parseAndCache(request); + return requestPath.pathWithinApplication().value(); + } + return super.initLookupPath(request); + } + + @Override + protected HandlerExecutionChain getCorsHandlerExecutionChain( + HttpServletRequest request, HandlerExecutionChain chain, @Nullable CorsConfiguration config) { + + this.savedCorsConfig = config; + return super.getCorsHandlerExecutionChain(request, chain, config); + } } - public class SimpleHandler extends WebContentGenerator implements HttpRequestHandler { + private static class SimpleHandler extends WebContentGenerator implements HttpRequestHandler { - public SimpleHandler() { + SimpleHandler() { super(METHOD_GET); } @Override - public void handleRequest(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - + public void handleRequest(HttpServletRequest request, HttpServletResponse response) { response.setStatus(HttpStatus.OK.value()); } } - public class CorsAwareHandler extends SimpleHandler implements CorsConfigurationSource { + private static class CorsAwareHandler extends SimpleHandler implements CorsConfigurationSource { @Override public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { @@ -259,7 +266,7 @@ class CorsAbstractHandlerMappingTests { } - public class CustomCorsConfigurationSource implements CorsConfigurationSource { + private static class CustomCorsConfigurationSource implements CorsConfigurationSource { @Override public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index d2a1630e33..3e476af444 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -23,8 +23,9 @@ import java.util.List; import javax.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.MutablePropertyValues; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; @@ -33,12 +34,16 @@ import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -53,85 +58,116 @@ import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_PATTE public class HandlerMappingIntrospectorTests { @Test - public void detectHandlerMappings() throws Exception { - StaticWebApplicationContext cxt = new StaticWebApplicationContext(); - cxt.registerSingleton("hmA", SimpleUrlHandlerMapping.class); - cxt.registerSingleton("hmB", SimpleUrlHandlerMapping.class); - cxt.registerSingleton("hmC", SimpleUrlHandlerMapping.class); - cxt.refresh(); + void detectHandlerMappings() { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.registerSingleton("A", SimpleUrlHandlerMapping.class); + context.registerSingleton("B", SimpleUrlHandlerMapping.class); + context.registerSingleton("C", SimpleUrlHandlerMapping.class); + context.refresh(); - List expected = Arrays.asList(cxt.getBean("hmA"), cxt.getBean("hmB"), cxt.getBean("hmC")); - List actual = getIntrospector(cxt).getHandlerMappings(); + List expected = Arrays.asList(context.getBean("A"), context.getBean("B"), context.getBean("C")); + List actual = initIntrospector(context).getHandlerMappings(); assertThat(actual).isEqualTo(expected); } @Test - public void detectHandlerMappingsOrdered() throws Exception { - StaticWebApplicationContext cxt = new StaticWebApplicationContext(); - MutablePropertyValues pvs = new MutablePropertyValues(Collections.singletonMap("order", "3")); - cxt.registerSingleton("hmA", SimpleUrlHandlerMapping.class, pvs); - pvs = new MutablePropertyValues(Collections.singletonMap("order", "2")); - cxt.registerSingleton("hmB", SimpleUrlHandlerMapping.class, pvs); - pvs = new MutablePropertyValues(Collections.singletonMap("order", "1")); - cxt.registerSingleton("hmC", SimpleUrlHandlerMapping.class, pvs); - cxt.refresh(); + void detectHandlerMappingsOrdered() { + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("B", SimpleUrlHandlerMapping.class, () -> { + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); + mapping.setOrder(2); + return mapping; + }); + context.registerBean("C", SimpleUrlHandlerMapping.class, () -> { + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); + mapping.setOrder(3); + return mapping; + }); + context.registerBean("A", SimpleUrlHandlerMapping.class, () -> { + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); + mapping.setOrder(1); + return mapping; + }); + context.refresh(); - List expected = Arrays.asList(cxt.getBean("hmC"), cxt.getBean("hmB"), cxt.getBean("hmA")); - List actual = getIntrospector(cxt).getHandlerMappings(); + List expected = Arrays.asList(context.getBean("A"), context.getBean("B"), context.getBean("C")); + List actual = initIntrospector(context).getHandlerMappings(); assertThat(actual).isEqualTo(expected); } - public void defaultHandlerMappings() throws Exception { - StaticWebApplicationContext cxt = new StaticWebApplicationContext(); - cxt.refresh(); + void defaultHandlerMappings() { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.refresh(); + List actual = initIntrospector(context).getHandlerMappings(); - List actual = getIntrospector(cxt).getHandlerMappings(); assertThat(actual.size()).isEqualTo(2); assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); } - @Test - public void getMatchable() throws Exception { - MutablePropertyValues pvs = new MutablePropertyValues( - Collections.singletonMap("urlMap", Collections.singletonMap("/path", new Object()))); + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void getMatchable(boolean usePathPatterns) throws Exception { - StaticWebApplicationContext cxt = new StaticWebApplicationContext(); - cxt.registerSingleton("hm", SimpleUrlHandlerMapping.class, pvs); - cxt.refresh(); + PathPatternParser parser = new PathPatternParser(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path"); - MatchableHandlerMapping hm = getIntrospector(cxt).getMatchableHandlerMapping(request); + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("mapping", SimpleUrlHandlerMapping.class, () -> { + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); + if (usePathPatterns) { + mapping.setPatternParser(parser); + } + mapping.setUrlMap(Collections.singletonMap("/path/*", new Object())); + return mapping; + }); + context.refresh(); - assertThat(hm).isEqualTo(cxt.getBean("hm")); - assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attributes changes not ignored").isNull(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); + + // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. + if (usePathPatterns) { + ServletRequestPathUtils.parseAndCache(request); + } + + MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); + + assertThat(mapping).isNotNull(); + assertThat(mapping).isEqualTo(context.getBean("mapping")); + assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + + String pattern = "/p*/*"; + PathPattern pathPattern = parser.parse(pattern); + assertThat(usePathPatterns ? mapping.match(request, pathPattern) : mapping.match(request, pattern)).isNotNull(); + + pattern = "/b*/*"; + pathPattern = parser.parse(pattern); + assertThat(usePathPatterns ? mapping.match(request, pathPattern) : mapping.match(request, pattern)).isNull(); } @Test - public void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() throws Exception { + void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { StaticWebApplicationContext cxt = new StaticWebApplicationContext(); - cxt.registerSingleton("hm1", TestHandlerMapping.class); + cxt.registerSingleton("mapping", TestHandlerMapping.class); cxt.refresh(); MockHttpServletRequest request = new MockHttpServletRequest(); - assertThatIllegalStateException().isThrownBy(() -> - getIntrospector(cxt).getMatchableHandlerMapping(request)); + assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } @Test - public void getCorsConfigurationPreFlight() throws Exception { - AnnotationConfigWebApplicationContext cxt = new AnnotationConfigWebApplicationContext(); - cxt.register(TestConfig.class); - cxt.refresh(); + void getCorsConfigurationPreFlight() { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); // PRE-FLIGHT MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/path"); request.addHeader("Origin", "http://localhost:9000"); request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); - CorsConfiguration corsConfig = getIntrospector(cxt).getCorsConfiguration(request); + CorsConfiguration corsConfig = initIntrospector(context).getCorsConfiguration(request); assertThat(corsConfig).isNotNull(); assertThat(corsConfig.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:9000")); @@ -139,23 +175,23 @@ public class HandlerMappingIntrospectorTests { } @Test - public void getCorsConfigurationActual() throws Exception { - AnnotationConfigWebApplicationContext cxt = new AnnotationConfigWebApplicationContext(); - cxt.register(TestConfig.class); - cxt.refresh(); + void getCorsConfigurationActual() { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); request.addHeader("Origin", "http://localhost:9000"); - CorsConfiguration corsConfig = getIntrospector(cxt).getCorsConfiguration(request); + CorsConfiguration corsConfig = initIntrospector(context).getCorsConfiguration(request); assertThat(corsConfig).isNotNull(); assertThat(corsConfig.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:9000")); assertThat(corsConfig.getAllowedMethods()).isEqualTo(Collections.singletonList("POST")); } - private HandlerMappingIntrospector getIntrospector(WebApplicationContext cxt) { + private HandlerMappingIntrospector initIntrospector(WebApplicationContext context) { HandlerMappingIntrospector introspector = new HandlerMappingIntrospector(); - introspector.setApplicationContext(cxt); + introspector.setApplicationContext(context); introspector.afterPropertiesSet(); return introspector; } @@ -164,14 +200,13 @@ public class HandlerMappingIntrospectorTests { private static class TestHandlerMapping implements HandlerMapping { @Override - public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + public HandlerExecutionChain getHandler(HttpServletRequest request) { return new HandlerExecutionChain(new Object()); } } @Configuration - @SuppressWarnings({"WeakerAccess", "unused"}) static class TestConfig { @Bean @@ -191,7 +226,7 @@ public class HandlerMappingIntrospectorTests { private static class TestController { @PostMapping("/path") - public void handle() { + void handle() { } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingTests.java index d8156a750a..ae204b38b3 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingTests.java @@ -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. @@ -16,20 +16,18 @@ package org.springframework.web.servlet.handler; -import java.io.IOException; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.Arguments; -import org.springframework.http.HttpStatus; -import org.springframework.web.HttpRequestHandler; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.support.WebContentGenerator; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; @@ -38,49 +36,51 @@ import static org.mockito.Mockito.mock; /** * Unit tests for {@link org.springframework.web.servlet.HandlerMapping}. * @author Brian Clozel + * @author Rossen Stoyanchev */ -public class HandlerMappingTests { +class HandlerMappingTests { - private AbstractHandlerMapping handlerMapping = new TestHandlerMapping(); - private StaticWebApplicationContext context = new StaticWebApplicationContext(); - private MockHttpServletRequest request = new MockHttpServletRequest(); + @SuppressWarnings("unused") + private static Stream pathPatternsArguments() { + List> factories = + PathPatternsTestUtils.requestArguments().collect(Collectors.toList()); + return Stream.of( + Arguments.arguments(new TestHandlerMapping(), factories.get(0)), + Arguments.arguments(new TestHandlerMapping(), factories.get(1)) + ); + } - @Test - public void orderedInterceptors() throws Exception { - HandlerInterceptor i1 = mock(HandlerInterceptor.class); - MappedInterceptor mappedInterceptor1 = new MappedInterceptor(new String[]{"/**"}, i1); + @PathPatternsParameterizedTest + void orderedInterceptors( + TestHandlerMapping mapping, Function requestFactory) + throws Exception { + + MappedInterceptor i1 = new MappedInterceptor(new String[] {"/**"}, mock(HandlerInterceptor.class)); HandlerInterceptor i2 = mock(HandlerInterceptor.class); - HandlerInterceptor i3 = mock(HandlerInterceptor.class); - MappedInterceptor mappedInterceptor3 = new MappedInterceptor(new String[]{"/**"}, i3); + MappedInterceptor i3 = new MappedInterceptor(new String[] {"/**"}, mock(HandlerInterceptor.class)); HandlerInterceptor i4 = mock(HandlerInterceptor.class); - this.handlerMapping.setInterceptors(mappedInterceptor1, i2, mappedInterceptor3, i4); - this.handlerMapping.setApplicationContext(this.context); - HandlerExecutionChain chain = this.handlerMapping.getHandlerExecutionChain(new SimpleHandler(), this.request); - assertThat(chain.getInterceptors()).contains( - mappedInterceptor1.getInterceptor(), i2, mappedInterceptor3.getInterceptor(), i4); + mapping.setInterceptors(i1, i2, i3, i4); + mapping.setApplicationContext(new StaticWebApplicationContext()); + + HandlerExecutionChain chain = mapping.getHandler(requestFactory.apply("/")); + + assertThat(chain).isNotNull(); + assertThat(chain.getInterceptors()).contains(i1.getInterceptor(), i2, i3.getInterceptor(), i4); } - class TestHandlerMapping extends AbstractHandlerMapping { - @Override - protected Object getHandlerInternal(HttpServletRequest request) throws Exception { - return new SimpleHandler(); - } - } + private static class TestHandlerMapping extends AbstractHandlerMapping { - class SimpleHandler extends WebContentGenerator implements HttpRequestHandler { - - public SimpleHandler() { - super(METHOD_GET); + TestHandlerMapping() { } @Override - public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - response.setStatus(HttpStatus.OK.value()); + protected Object getHandlerInternal(HttpServletRequest request) { + initLookupPath(request); + return new Object(); } - } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/MappedInterceptorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/MappedInterceptorTests.java index 7490cab6b0..6501d808c1 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/MappedInterceptorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/MappedInterceptorTests.java @@ -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,18 +17,19 @@ package org.springframework.web.servlet.handler; import java.util.Comparator; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -37,96 +38,95 @@ import static org.mockito.Mockito.mock; /** * Test fixture for {@link MappedInterceptor} tests. - * * @author Rossen Stoyanchev */ -public class MappedInterceptorTests { +class MappedInterceptorTests { - private LocaleChangeInterceptor interceptor; + private static final LocaleChangeInterceptor delegate = new LocaleChangeInterceptor(); - private final AntPathMatcher pathMatcher = new AntPathMatcher(); - @BeforeEach - public void setup() { - this.interceptor = new LocaleChangeInterceptor(); + @SuppressWarnings("unused") + private static Stream> pathPatternsArguments() { + return PathPatternsTestUtils.requestArguments(); + } + + + @PathPatternsParameterizedTest + void noPatterns(Function requestFactory) { + MappedInterceptor interceptor = new MappedInterceptor(null, null, delegate); + assertThat(interceptor.matches(requestFactory.apply("/foo"))).isTrue(); + } + + @PathPatternsParameterizedTest + void includePattern(Function requestFactory) { + MappedInterceptor interceptor = new MappedInterceptor(new String[] { "/foo/*" }, null, delegate); + + assertThat(interceptor.matches(requestFactory.apply("/foo/bar"))).isTrue(); + assertThat(interceptor.matches(requestFactory.apply("/bar/foo"))).isFalse(); + } + + @PathPatternsParameterizedTest + void includePatternWithMatrixVariables(Function requestFactory) { + MappedInterceptor interceptor = new MappedInterceptor(new String[] { "/foo*/*" }, null, delegate); + assertThat(interceptor.matches(requestFactory.apply("/foo;q=1/bar;s=2"))).isTrue(); + } + + @PathPatternsParameterizedTest + void excludePattern(Function requestFactory) { + MappedInterceptor interceptor = new MappedInterceptor(null, new String[] { "/admin/**" }, delegate); + + assertThat(interceptor.matches(requestFactory.apply("/foo"))).isTrue(); + assertThat(interceptor.matches(requestFactory.apply("/admin/foo"))).isFalse(); + } + + @PathPatternsParameterizedTest + void includeAndExcludePatterns(Function requestFactory) { + MappedInterceptor interceptor = + new MappedInterceptor(new String[] { "/**" }, new String[] { "/admin/**" }, delegate); + + assertThat(interceptor.matches(requestFactory.apply("/foo"))).isTrue(); + assertThat(interceptor.matches(requestFactory.apply("/admin/foo"))).isFalse(); + } + + @PathPatternsParameterizedTest + void customPathMatcher(Function requestFactory) { + MappedInterceptor interceptor = new MappedInterceptor(new String[] { "/foo/[0-9]*" }, null, delegate); + interceptor.setPathMatcher(new TestPathMatcher()); + + assertThat(interceptor.matches(requestFactory.apply("/foo/123"))).isTrue(); + assertThat(interceptor.matches(requestFactory.apply("/foo/bar"))).isFalse(); } @Test - public void noPatterns() { - MappedInterceptor mappedInterceptor = new MappedInterceptor(null, null, this.interceptor); - assertThat(mappedInterceptor.matches("/foo", pathMatcher)).isTrue(); + void preHandle() throws Exception { + HandlerInterceptor delegate = mock(HandlerInterceptor.class); + + new MappedInterceptor(null, delegate).preHandle( + mock(HttpServletRequest.class), mock(HttpServletResponse.class), null); + + then(delegate).should().preHandle(any(HttpServletRequest.class), any(HttpServletResponse.class), any()); } @Test - public void includePattern() { - MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[] { "/foo/*" }, this.interceptor); + void postHandle() throws Exception { + HandlerInterceptor delegate = mock(HandlerInterceptor.class); - assertThat(mappedInterceptor.matches("/foo/bar", pathMatcher)).isTrue(); - assertThat(mappedInterceptor.matches("/bar/foo", pathMatcher)).isFalse(); + new MappedInterceptor(null, delegate).postHandle( + mock(HttpServletRequest.class), mock(HttpServletResponse.class), null, mock(ModelAndView.class)); + + then(delegate).should().postHandle(any(), any(), any(), any()); } @Test - public void includePatternWithMatrixVariables() { - MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[] { "/foo*/*" }, this.interceptor); - assertThat(mappedInterceptor.matches("/foo;q=1/bar;s=2", pathMatcher)).isTrue(); + void afterCompletion() throws Exception { + HandlerInterceptor delegate = mock(HandlerInterceptor.class); + + new MappedInterceptor(null, delegate).afterCompletion( + mock(HttpServletRequest.class), mock(HttpServletResponse.class), null, mock(Exception.class)); + + then(delegate).should().afterCompletion(any(), any(), any(), any()); } - @Test - public void excludePattern() { - MappedInterceptor mappedInterceptor = new MappedInterceptor(null, new String[] { "/admin/**" }, this.interceptor); - - assertThat(mappedInterceptor.matches("/foo", pathMatcher)).isTrue(); - assertThat(mappedInterceptor.matches("/admin/foo", pathMatcher)).isFalse(); - } - - @Test - public void includeAndExcludePatterns() { - MappedInterceptor mappedInterceptor = new MappedInterceptor( - new String[] { "/**" }, new String[] { "/admin/**" }, this.interceptor); - - assertThat(mappedInterceptor.matches("/foo", pathMatcher)).isTrue(); - assertThat(mappedInterceptor.matches("/admin/foo", pathMatcher)).isFalse(); - } - - @Test - public void customPathMatcher() { - MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[] { "/foo/[0-9]*" }, this.interceptor); - mappedInterceptor.setPathMatcher(new TestPathMatcher()); - - assertThat(mappedInterceptor.matches("/foo/123", pathMatcher)).isTrue(); - assertThat(mappedInterceptor.matches("/foo/bar", pathMatcher)).isFalse(); - } - - @Test - public void preHandle() throws Exception { - HandlerInterceptor interceptor = mock(HandlerInterceptor.class); - MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[] { "/**" }, interceptor); - mappedInterceptor.preHandle(mock(HttpServletRequest.class), mock(HttpServletResponse.class), null); - - then(interceptor).should().preHandle(any(HttpServletRequest.class), any(HttpServletResponse.class), any()); - } - - @Test - public void postHandle() throws Exception { - HandlerInterceptor interceptor = mock(HandlerInterceptor.class); - MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[] { "/**" }, interceptor); - mappedInterceptor.postHandle(mock(HttpServletRequest.class), mock(HttpServletResponse.class), - null, mock(ModelAndView.class)); - - then(interceptor).should().postHandle(any(), any(), any(), any()); - } - - @Test - public void afterCompletion() throws Exception { - HandlerInterceptor interceptor = mock(HandlerInterceptor.class); - MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[] { "/**" }, interceptor); - mappedInterceptor.afterCompletion(mock(HttpServletRequest.class), mock(HttpServletResponse.class), - null, mock(Exception.class)); - - then(interceptor).should().afterCompletion(any(), any(), any(), any()); - } - - public static class TestPathMatcher implements PathMatcher { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathMatchingUrlHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathMatchingUrlHandlerMappingTests.java index e0c2dd1a88..81d9f658ba 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathMatchingUrlHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathMatchingUrlHandlerMappingTests.java @@ -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. @@ -16,16 +16,19 @@ package org.springframework.web.servlet.handler; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import java.util.stream.Stream; + +import org.junit.jupiter.params.provider.Arguments; import org.springframework.web.context.ConfigurableWebApplicationContext; +import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.XmlWebApplicationContext; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockServletContext; +import org.springframework.web.util.ServletRequestPathUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -35,41 +38,49 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class PathMatchingUrlHandlerMappingTests { - public static final String CONF = "/org/springframework/web/servlet/handler/map3.xml"; + @SuppressWarnings("unused") + static Stream pathPatternsArguments() { + String location = "/org/springframework/web/servlet/handler/map3.xml"; + WebApplicationContext wac = initConfig(location); - private HandlerMapping hm; + SimpleUrlHandlerMapping mapping1 = wac.getBean("urlMapping1", SimpleUrlHandlerMapping.class); + assertThat(mapping1.getPathPatternHandlerMap()).isNotEmpty(); - private ConfigurableWebApplicationContext wac; + SimpleUrlHandlerMapping mapping2 = wac.getBean("urlMapping2", SimpleUrlHandlerMapping.class); + assertThat(mapping2.getPathPatternHandlerMap()).isEmpty(); - @BeforeEach - public void setUp() throws Exception { - MockServletContext sc = new MockServletContext(""); - wac = new XmlWebApplicationContext(); - wac.setServletContext(sc); - wac.setConfigLocations(new String[] {CONF}); - wac.refresh(); - hm = (HandlerMapping) wac.getBean("urlMapping"); + return Stream.of(Arguments.of(mapping1, wac), Arguments.of(mapping2, wac)); } - @Test - public void requestsWithHandlers() throws Exception { + private static WebApplicationContext initConfig(String... configLocations) { + MockServletContext sc = new MockServletContext(""); + ConfigurableWebApplicationContext context = new XmlWebApplicationContext(); + context.setServletContext(sc); + context.setConfigLocations(configLocations); + context.refresh(); + return context; + } + + + @PathPatternsParameterizedTest + void requestsWithHandlers(HandlerMapping mapping, WebApplicationContext wac) throws Exception { Object bean = wac.getBean("mainController"); MockHttpServletRequest req = new MockHttpServletRequest("GET", "/welcome.html"); - HandlerExecutionChain hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + HandlerExecutionChain hec = getHandler(mapping, wac, req); + assertThat(hec.getHandler() == bean).isTrue(); req = new MockHttpServletRequest("GET", "/show.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + hec = getHandler(mapping, wac, req); + assertThat(hec.getHandler() == bean).isTrue(); req = new MockHttpServletRequest("GET", "/bookseats.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + hec = getHandler(mapping, wac, req); + assertThat(hec.getHandler() == bean).isTrue(); } - @Test - public void actualPathMatching() throws Exception { + @PathPatternsParameterizedTest + void actualPathMatching(SimpleUrlHandlerMapping mapping, WebApplicationContext wac) throws Exception { // there a couple of mappings defined with which we can test the // path matching, let's do that... @@ -77,178 +88,199 @@ public class PathMatchingUrlHandlerMappingTests { Object defaultBean = wac.getBean("starController"); // testing some normal behavior - MockHttpServletRequest req = new MockHttpServletRequest("GET", "/pathmatchingTest.html"); - HandlerExecutionChain hec = getHandler(req); - assertThat(hec != null).as("Handler is null").isTrue(); - assertThat(hec.getHandler() == bean).as("Handler is correct bean").isTrue(); - assertThat(req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("/pathmatchingTest.html"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/pathmatchingTest.html"); + HandlerExecutionChain chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); + assertThat(request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)) + .isEqualTo("/pathmatchingTest.html"); // no match, no forward slash included - req = new MockHttpServletRequest("GET", "welcome.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); - assertThat(req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("welcome.html"); + request = new MockHttpServletRequest("GET", "welcome.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); + assertThat(request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)) + .isEqualTo("welcome.html"); // testing some ????? behavior - req = new MockHttpServletRequest("GET", "/pathmatchingAA.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); - assertThat(req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("pathmatchingAA.html"); + request = new MockHttpServletRequest("GET", "/pathmatchingAA.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); + assertThat(request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)) + .isEqualTo("pathmatchingAA.html"); // testing some ????? behavior - req = new MockHttpServletRequest("GET", "/pathmatchingA.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); - assertThat(req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("/pathmatchingA.html"); + request = new MockHttpServletRequest("GET", "/pathmatchingA.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); + assertThat(request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)) + .isEqualTo("/pathmatchingA.html"); // testing some ????? behavior - req = new MockHttpServletRequest("GET", "/administrator/pathmatching.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/administrator/pathmatching.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); // testing simple /**/behavior - req = new MockHttpServletRequest("GET", "/administrator/test/pathmatching.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/administrator/test/pathmatching.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); // this should not match because of the administratorT - req = new MockHttpServletRequest("GET", "/administratort/pathmatching.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/administratort/pathmatching.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); // this should match because of *.jsp - req = new MockHttpServletRequest("GET", "/bla.jsp"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/bla.jsp"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); // should match because exact pattern is there - req = new MockHttpServletRequest("GET", "/administrator/another/bla.xml"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/administrator/another/bla.xml"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); // should not match, because there's not .gif extension in there - req = new MockHttpServletRequest("GET", "/administrator/another/bla.gif"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/administrator/another/bla.gif"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); // should match because there testlast* in there - req = new MockHttpServletRequest("GET", "/administrator/test/testlastbit"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/administrator/test/testlastbit"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); // but this not, because it's testlast and not testla - req = new MockHttpServletRequest("GET", "/administrator/test/testla"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/administrator/test/testla"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); - req = new MockHttpServletRequest("GET", "/administrator/testing/longer/bla"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/administrator/testing/longer/bla"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/administrator/testing/longer/test.jsp"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/administrator/testing/longer/test.jsp"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/administrator/testing/longer2/notmatching/notmatching"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/administrator/testing/longer2/notmatching/notmatching"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); - req = new MockHttpServletRequest("GET", "/shortpattern/testing/toolong"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/shortpattern/testing/toolong"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); - req = new MockHttpServletRequest("GET", "/XXpathXXmatching.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/XXpathXXmatching.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/pathXXmatching.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/pathXXmatching.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/XpathXXmatching.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/XpathXXmatching.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); - req = new MockHttpServletRequest("GET", "/XXpathmatching.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/XXpathmatching.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); - req = new MockHttpServletRequest("GET", "/show12.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/show12.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/show123.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/show123.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/show1.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/show1.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/reallyGood-test-is-this.jpeg"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/reallyGood-test-is-this.jpeg"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/reallyGood-tst-is-this.jpeg"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/reallyGood-tst-is-this.jpeg"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); - req = new MockHttpServletRequest("GET", "/testing/test.jpeg"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/testing/test.jpeg"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/testing/test.jpg"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/testing/test.jpg"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); - req = new MockHttpServletRequest("GET", "/anotherTest"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/anotherTest"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/stillAnotherTest"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/stillAnotherTest"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); // there outofpattern*yeah in the pattern, so this should fail - req = new MockHttpServletRequest("GET", "/outofpattern*ye"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/outofpattern*ye"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); - req = new MockHttpServletRequest("GET", "/test't est/path'm atching.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/test't%20est/path'm%20atching.html"); + chain = getHandler(mapping, wac, request); + assertThat(chain.getHandler()).isSameAs(defaultBean); - req = new MockHttpServletRequest("GET", "/test%26t%20est/path%26m%20atching.html"); - hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + request = new MockHttpServletRequest("GET", "/test%26t%20est/path%26m%20atching.html"); + chain = getHandler(mapping, wac, request); + if (!mapping.getPathPatternHandlerMap().isEmpty()) { + assertThat(chain.getHandler()) + .as("PathPattern always matches to encoded paths.") + .isSameAs(bean); + } + else { + assertThat(chain.getHandler()) + .as("PathMatcher should not match encoded pattern with urlDecode=true") + .isSameAs(defaultBean); + } } - @Test - public void defaultMapping() throws Exception { + @PathPatternsParameterizedTest + void defaultMapping(HandlerMapping mapping, WebApplicationContext wac) throws Exception { Object bean = wac.getBean("starController"); MockHttpServletRequest req = new MockHttpServletRequest("GET", "/goggog.html"); - HandlerExecutionChain hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + HandlerExecutionChain hec = getHandler(mapping, wac, req); + assertThat(hec.getHandler()).isSameAs(bean); } - @Test - public void mappingExposedInRequest() throws Exception { + @PathPatternsParameterizedTest + void mappingExposedInRequest(HandlerMapping mapping, WebApplicationContext wac) throws Exception { Object bean = wac.getBean("mainController"); MockHttpServletRequest req = new MockHttpServletRequest("GET", "/show.html"); - HandlerExecutionChain hec = getHandler(req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); - assertThat(req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).as("Mapping not exposed").isEqualTo("show.html"); + HandlerExecutionChain hec = getHandler(mapping, wac, req); + assertThat(hec.getHandler()).isSameAs(bean); + assertThat(req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)) + .as("Mapping not exposed").isEqualTo("show.html"); } - private HandlerExecutionChain getHandler(MockHttpServletRequest req) throws Exception { - HandlerExecutionChain hec = hm.getHandler(req); - HandlerInterceptor[] interceptors = hec.getInterceptors(); + private HandlerExecutionChain getHandler( + HandlerMapping mapping, WebApplicationContext wac, MockHttpServletRequest request) + throws Exception { + + // At runtime this is done by the DispatcherServlet + if (((AbstractHandlerMapping) mapping).getPatternParser() != null) { + ServletRequestPathUtils.parseAndCache(request); + } + + HandlerExecutionChain executionChain = mapping.getHandler(request); + HandlerInterceptor[] interceptors = executionChain.getInterceptors(); if (interceptors != null) { for (HandlerInterceptor interceptor : interceptors) { - interceptor.preHandle(req, null, hec.getHandler()); + interceptor.preHandle(request, null, executionChain.getHandler()); } } - return hec; + return executionChain; } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathPatternsParameterizedTest.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathPatternsParameterizedTest.java new file mode 100644 index 0000000000..7dc77ff73d --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathPatternsParameterizedTest.java @@ -0,0 +1,36 @@ +/* + * 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.servlet.handler; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for tests parameterized to use either + * {@link org.springframework.web.util.pattern.PathPatternParser} or + * {@link org.springframework.util.PathMatcher} for URL pattern matching. + * + * @author Rossen Stoyanchev + * @since 5.3 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@org.junit.jupiter.params.ParameterizedTest +@org.junit.jupiter.params.provider.MethodSource("pathPatternsArguments") +public @interface PathPatternsParameterizedTest { +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathPatternsTestUtils.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathPatternsTestUtils.java new file mode 100644 index 0000000000..cf034fd160 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathPatternsTestUtils.java @@ -0,0 +1,111 @@ +/* + * 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.servlet.handler; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.springframework.lang.Nullable; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.UrlPathHelper; + +/** + * Utility methods to help with parameterized tests for URL pattern matching + * via pre-parsed {@code PathPattern}s or String pattern matching with + * {@code PathMatcher}. + * + * @author Rossen Stoyanchev + * @since 5.3 + */ +public abstract class PathPatternsTestUtils { + + public static Stream> requestArguments() { + return requestArguments(null); + } + + public static Stream> requestArguments(@Nullable String contextPath) { + return Stream.of( + path -> { + MockHttpServletRequest request = createRequest("GET", contextPath, path); + ServletRequestPathUtils.parseAndCache(request); + return request; + }, + path -> { + MockHttpServletRequest request = createRequest("GET", contextPath, path); + UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request); + return request; + } + ); + } + + /** + * Create a MockHttpServletRequest and initialize the request attributes for + * the lookupPath via {@link ServletRequestPathUtils} or {@link UrlPathHelper} + * depending on the setting of the parsedPatterns argument. + *

    At runtime this would be done by the DispatcherServlet (for the RequestPath) + * and by the AbstractHandlerMapping (for UrlPathHelper). + */ + public static MockHttpServletRequest initRequest(String method, String requestUri, boolean parsedPatterns) { + return initRequest(method, null, requestUri, parsedPatterns); + } + + /** + * See {@link #initRequest(String, String, boolean)}. This variant adds a contextPath. + */ + public static MockHttpServletRequest initRequest( + String method, @Nullable String contextPath, String path, boolean parsedPatterns) { + + return initRequest(method, contextPath, path, parsedPatterns, null); + } + + /** + * See {@link #initRequest(String, String, boolean)}. This variant adds a contextPath + * and a post-construct initializer to apply further changes before the + * lookupPath is resolved. + */ + public static MockHttpServletRequest initRequest( + String method, @Nullable String contextPath, String path, + boolean parsedPatterns, @Nullable Consumer postConstructInitializer) { + + MockHttpServletRequest request = createRequest(method, contextPath, path); + if (postConstructInitializer != null) { + postConstructInitializer.accept(request); + } + // At runtime this is done by the DispatcherServlet and AbstractHandlerMapping + if (parsedPatterns) { + ServletRequestPathUtils.parseAndCache(request); + } + else { + UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request); + } + return request; + } + + private static MockHttpServletRequest createRequest(String method, @Nullable String contextPath, String path) { + if (contextPath != null) { + String requestUri = contextPath + (path.startsWith("/") ? "" : "/") + path; + MockHttpServletRequest request = new MockHttpServletRequest(method, requestUri); + request.setContextPath(contextPath); + return request; + } + else { + return new MockHttpServletRequest(method, path); + } + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/SimpleUrlHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/SimpleUrlHandlerMappingTests.java index 5546caad1a..a6a75d0142 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/SimpleUrlHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/SimpleUrlHandlerMappingTests.java @@ -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. @@ -19,6 +19,8 @@ package org.springframework.web.servlet.handler; import java.util.Collections; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.FatalBeanException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -33,6 +35,8 @@ import org.springframework.web.util.WebUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE; +import static org.springframework.web.servlet.HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE; /** * @author Rod Johnson @@ -48,44 +52,38 @@ public class SimpleUrlHandlerMappingTests { root.setServletContext(sc); root.setConfigLocations("/org/springframework/web/servlet/handler/map1.xml"); root.refresh(); + XmlWebApplicationContext wac = new XmlWebApplicationContext(); wac.setParent(root); wac.setServletContext(sc); wac.setNamespace("map2err"); wac.setConfigLocations("/org/springframework/web/servlet/handler/map2err.xml"); - assertThatExceptionOfType(FatalBeanException.class).isThrownBy( - wac::refresh) - .withCauseInstanceOf(NoSuchBeanDefinitionException.class) - .satisfies(ex -> assertThat(((NoSuchBeanDefinitionException) ex.getCause()).getBeanName()).isEqualTo("mainControlle")); - } - - @Test - public void urlMappingWithUrlMap() throws Exception { - checkMappings("urlMapping"); - } - - @Test - public void urlMappingWithProps() throws Exception { - checkMappings("urlMappingWithProps"); + assertThatExceptionOfType(FatalBeanException.class) + .isThrownBy(wac::refresh) + .withCauseInstanceOf(NoSuchBeanDefinitionException.class) + .satisfies(ex -> { + NoSuchBeanDefinitionException cause = (NoSuchBeanDefinitionException) ex.getCause(); + assertThat(cause.getBeanName()).isEqualTo("mainControlle"); + }); } @Test public void testNewlineInRequest() throws Exception { Object controller = new Object(); - SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping( - Collections.singletonMap("/*/baz", controller)); - handlerMapping.setUrlDecode(false); - handlerMapping.setApplicationContext(new StaticApplicationContext()); + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(Collections.singletonMap("/*/baz", controller)); + mapping.setUrlDecode(false); + mapping.setApplicationContext(new StaticApplicationContext()); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo%0a%0dbar/baz"); - HandlerExecutionChain hec = handlerMapping.getHandler(request); + HandlerExecutionChain hec = mapping.getHandler(request); assertThat(hec).isNotNull(); assertThat(hec.getHandler()).isSameAs(controller); } - @SuppressWarnings("resource") - private void checkMappings(String beanName) throws Exception { + @ParameterizedTest + @ValueSource(strings = {"urlMapping", "urlMappingWithProps", "urlMappingWithPathPatterns"}) + void checkMappings(String beanName) throws Exception { MockServletContext sc = new MockServletContext(""); XmlWebApplicationContext wac = new XmlWebApplicationContext(); wac.setServletContext(sc); @@ -96,76 +94,76 @@ public class SimpleUrlHandlerMappingTests { Object defaultBean = wac.getBean("starController"); HandlerMapping hm = (HandlerMapping) wac.getBean(beanName); - MockHttpServletRequest req = new MockHttpServletRequest("GET", "/welcome.html"); - HandlerExecutionChain hec = getHandler(hm, req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); - assertThat(req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("/welcome.html"); - assertThat(req.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE)).isEqualTo(bean); + boolean usePathPatterns = (((AbstractHandlerMapping) hm).getPatternParser() != null); + MockHttpServletRequest request = PathPatternsTestUtils.initRequest("GET", "/welcome.html", usePathPatterns); + HandlerExecutionChain chain = getHandler(hm, request); + assertThat(chain.getHandler()).isSameAs(bean); + assertThat(request.getAttribute(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("/welcome.html"); + assertThat(request.getAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE)).isEqualTo(bean); - req = new MockHttpServletRequest("GET", "/welcome.x"); - hec = getHandler(hm, req); - assertThat(hec != null && hec.getHandler() == otherBean).as("Handler is correct bean").isTrue(); - assertThat(req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("welcome.x"); - assertThat(req.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE)).isEqualTo(otherBean); + request = PathPatternsTestUtils.initRequest("GET", "/welcome.x", usePathPatterns); + chain = getHandler(hm, request); + assertThat(chain.getHandler()).isSameAs(otherBean); + assertThat(request.getAttribute(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("welcome.x"); + assertThat(request.getAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE)).isEqualTo(otherBean); - req = new MockHttpServletRequest("GET", "/welcome/"); - hec = getHandler(hm, req); - assertThat(hec != null && hec.getHandler() == otherBean).as("Handler is correct bean").isTrue(); - assertThat(req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("welcome"); + request = PathPatternsTestUtils.initRequest("GET", "/welcome/", usePathPatterns); + chain = getHandler(hm, request); + assertThat(chain.getHandler()).isSameAs(otherBean); + assertThat(request.getAttribute(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("welcome"); - req = new MockHttpServletRequest("GET", "/"); - req.setServletPath("/welcome.html"); - hec = getHandler(hm, req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = PathPatternsTestUtils.initRequest("GET", "/", usePathPatterns); + request.setServletPath("/welcome.html"); + chain = getHandler(hm, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/welcome.html"); - req.setContextPath("/app"); - hec = getHandler(hm, req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = PathPatternsTestUtils.initRequest("GET", "/app", "/welcome.html", usePathPatterns); + chain = getHandler(hm, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/show.html"); - hec = getHandler(hm, req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = PathPatternsTestUtils.initRequest("GET", "/show.html", usePathPatterns); + chain = getHandler(hm, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/bookseats.html"); - hec = getHandler(hm, req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = PathPatternsTestUtils.initRequest("GET", "/bookseats.html", usePathPatterns); + chain = getHandler(hm, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/original-welcome.html"); - req.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE, "/welcome.html"); - hec = getHandler(hm, req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = PathPatternsTestUtils.initRequest("GET", null, "/original-welcome.html", usePathPatterns, + req -> req.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE, "/welcome.html")); + chain = getHandler(hm, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/original-show.html"); - req.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE, "/show.html"); - hec = getHandler(hm, req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = PathPatternsTestUtils.initRequest("GET", null, "/original-show.html", usePathPatterns, + req -> req.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE, "/show.html")); + chain = getHandler(hm, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/original-bookseats.html"); - req.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE, "/bookseats.html"); - hec = getHandler(hm, req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); + request = PathPatternsTestUtils.initRequest("GET", null, "/original-bookseats.html", usePathPatterns, + req -> req.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE, "/bookseats.html")); + chain = getHandler(hm, request); + assertThat(chain.getHandler()).isSameAs(bean); - req = new MockHttpServletRequest("GET", "/"); - hec = getHandler(hm, req); - assertThat(hec != null && hec.getHandler() == bean).as("Handler is correct bean").isTrue(); - assertThat(req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("/"); + request = PathPatternsTestUtils.initRequest("GET", "/", usePathPatterns); + chain = getHandler(hm, request); + assertThat(chain.getHandler()).isSameAs(bean); + assertThat(request.getAttribute(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("/"); - req = new MockHttpServletRequest("GET", "/somePath"); - hec = getHandler(hm, req); - assertThat(hec != null && hec.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); - assertThat(req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("/somePath"); + request = PathPatternsTestUtils.initRequest("GET", "/somePath", usePathPatterns); + chain = getHandler(hm, request); + assertThat(chain.getHandler() == defaultBean).as("Handler is correct bean").isTrue(); + assertThat(request.getAttribute(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).isEqualTo("/somePath"); } - private HandlerExecutionChain getHandler(HandlerMapping hm, MockHttpServletRequest req) throws Exception { - HandlerExecutionChain hec = hm.getHandler(req); - HandlerInterceptor[] interceptors = hec.getInterceptors(); + private HandlerExecutionChain getHandler(HandlerMapping mapping, MockHttpServletRequest request) throws Exception { + HandlerExecutionChain chain = mapping.getHandler(request); + HandlerInterceptor[] interceptors = chain.getInterceptors(); if (interceptors != null) { for (HandlerInterceptor interceptor : interceptors) { - interceptor.preHandle(req, null, hec.getHandler()); + interceptor.preHandle(request, null, chain.getHandler()); } } - return hec; + return chain; } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/UrlFilenameViewControllerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/UrlFilenameViewControllerTests.java index 7d7a8bc605..fa103a3a3a 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/UrlFilenameViewControllerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/UrlFilenameViewControllerTests.java @@ -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. @@ -16,16 +16,21 @@ package org.springframework.web.servlet.mvc; +import java.util.function.Function; +import java.util.stream.Stream; + import org.junit.jupiter.api.Test; import org.springframework.ui.ModelMap; import org.springframework.util.AntPathMatcher; -import org.springframework.util.PathMatcher; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.handler.PathPatternsParameterizedTest; +import org.springframework.web.servlet.handler.PathPatternsTestUtils; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; +import org.springframework.web.util.ServletRequestPathUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -34,162 +39,164 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Rick Evans * @since 14.09.2005 */ -public class UrlFilenameViewControllerTests { +class UrlFilenameViewControllerTests { - private PathMatcher pathMatcher = new AntPathMatcher(); + @SuppressWarnings("unused") + private static Stream> pathPatternsArguments() { + return PathPatternsTestUtils.requestArguments(); + } - @Test - public void withPlainFilename() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/index"); - MockHttpServletResponse response = new MockHttpServletResponse(); - ModelAndView mv = ctrl.handleRequest(request, response); + @PathPatternsParameterizedTest + void withPlainFilename(Function requestFactory) throws Exception { + UrlFilenameViewController controller = new UrlFilenameViewController(); + MockHttpServletRequest request = requestFactory.apply("/index"); + ModelAndView mv = controller.handleRequest(request, new MockHttpServletResponse()); assertThat(mv.getViewName()).isEqualTo("index"); assertThat(mv.getModel().isEmpty()).isTrue(); } - @Test - public void withFilenamePlusExtension() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/index.html"); - MockHttpServletResponse response = new MockHttpServletResponse(); - ModelAndView mv = ctrl.handleRequest(request, response); + @PathPatternsParameterizedTest + void withFilenamePlusExtension(Function requestFactory) throws Exception { + UrlFilenameViewController controller = new UrlFilenameViewController(); + MockHttpServletRequest request = requestFactory.apply("/index.html"); + ModelAndView mv = controller.handleRequest(request, new MockHttpServletResponse()); assertThat(mv.getViewName()).isEqualTo("index"); assertThat(mv.getModel().isEmpty()).isTrue(); } - @Test - public void withFilenameAndMatrixVariables() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/index;a=A;b=B"); - MockHttpServletResponse response = new MockHttpServletResponse(); - ModelAndView mv = ctrl.handleRequest(request, response); + @PathPatternsParameterizedTest + void withFilenameAndMatrixVariables(Function requestFactory) throws Exception { + UrlFilenameViewController controller = new UrlFilenameViewController(); + MockHttpServletRequest request = requestFactory.apply("/index;a=A;b=B"); + ModelAndView mv = controller.handleRequest(request, new MockHttpServletResponse()); assertThat(mv.getViewName()).isEqualTo("index"); assertThat(mv.getModel().isEmpty()).isTrue(); } - @Test - public void withPrefixAndSuffix() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); - ctrl.setPrefix("mypre_"); - ctrl.setSuffix("_mysuf"); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/index.html"); - MockHttpServletResponse response = new MockHttpServletResponse(); - ModelAndView mv = ctrl.handleRequest(request, response); + @PathPatternsParameterizedTest + void withPrefixAndSuffix(Function requestFactory) throws Exception { + UrlFilenameViewController controller = new UrlFilenameViewController(); + controller.setPrefix("mypre_"); + controller.setSuffix("_mysuf"); + MockHttpServletRequest request = requestFactory.apply("/index.html"); + ModelAndView mv = controller.handleRequest(request, new MockHttpServletResponse()); assertThat(mv.getViewName()).isEqualTo("mypre_index_mysuf"); assertThat(mv.getModel().isEmpty()).isTrue(); } - @Test - public void withPrefix() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); - ctrl.setPrefix("mypre_"); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/index.html"); - MockHttpServletResponse response = new MockHttpServletResponse(); - ModelAndView mv = ctrl.handleRequest(request, response); + @PathPatternsParameterizedTest + void withPrefix(Function requestFactory) throws Exception { + UrlFilenameViewController controller = new UrlFilenameViewController(); + controller.setPrefix("mypre_"); + MockHttpServletRequest request = requestFactory.apply("/index.html"); + ModelAndView mv = controller.handleRequest(request, new MockHttpServletResponse()); assertThat(mv.getViewName()).isEqualTo("mypre_index"); assertThat(mv.getModel().isEmpty()).isTrue(); } - @Test - public void withSuffix() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); - ctrl.setSuffix("_mysuf"); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/index.html"); - MockHttpServletResponse response = new MockHttpServletResponse(); - ModelAndView mv = ctrl.handleRequest(request, response); + @PathPatternsParameterizedTest + void withSuffix(Function requestFactory) throws Exception { + UrlFilenameViewController controller = new UrlFilenameViewController(); + controller.setSuffix("_mysuf"); + MockHttpServletRequest request = requestFactory.apply("/index.html"); + ModelAndView mv = controller.handleRequest(request, new MockHttpServletResponse()); assertThat(mv.getViewName()).isEqualTo("index_mysuf"); assertThat(mv.getModel().isEmpty()).isTrue(); } - @Test - public void multiLevel() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/docs/cvs/commit.html"); - MockHttpServletResponse response = new MockHttpServletResponse(); - ModelAndView mv = ctrl.handleRequest(request, response); + @PathPatternsParameterizedTest + void multiLevel(Function requestFactory) throws Exception { + UrlFilenameViewController controller = new UrlFilenameViewController(); + MockHttpServletRequest request = requestFactory.apply("/docs/cvs/commit.html"); + ModelAndView mv = controller.handleRequest(request, new MockHttpServletResponse()); assertThat(mv.getViewName()).isEqualTo("docs/cvs/commit"); assertThat(mv.getModel().isEmpty()).isTrue(); } - @Test - public void multiLevelWithMapping() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/docs/cvs/commit.html"); + @PathPatternsParameterizedTest + void multiLevelWithMapping(Function requestFactory) throws Exception { + UrlFilenameViewController controller = new UrlFilenameViewController(); + MockHttpServletRequest request = requestFactory.apply("/docs/cvs/commit.html"); exposePathInMapping(request, "/docs/**"); - MockHttpServletResponse response = new MockHttpServletResponse(); - ModelAndView mv = ctrl.handleRequest(request, response); + ModelAndView mv = controller.handleRequest(request, new MockHttpServletResponse()); assertThat(mv.getViewName()).isEqualTo("cvs/commit"); assertThat(mv.getModel().isEmpty()).isTrue(); } - @Test - public void multiLevelMappingWithFallback() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/docs/cvs/commit.html"); + @PathPatternsParameterizedTest + void multiLevelMappingWithFallback(Function requestFactory) throws Exception { + UrlFilenameViewController controller = new UrlFilenameViewController(); + MockHttpServletRequest request = requestFactory.apply("/docs/cvs/commit.html"); exposePathInMapping(request, "/docs/cvs/commit.html"); - MockHttpServletResponse response = new MockHttpServletResponse(); - ModelAndView mv = ctrl.handleRequest(request, response); + ModelAndView mv = controller.handleRequest(request, new MockHttpServletResponse()); assertThat(mv.getViewName()).isEqualTo("docs/cvs/commit"); assertThat(mv.getModel().isEmpty()).isTrue(); } - @Test - public void withContextMapping() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); + @PathPatternsParameterizedTest + void withContextMapping(Function requestFactory) throws Exception { + UrlFilenameViewController controller = new UrlFilenameViewController(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myapp/docs/cvs/commit.html"); request.setContextPath("/myapp"); - MockHttpServletResponse response = new MockHttpServletResponse(); - ModelAndView mv = ctrl.handleRequest(request, response); + ServletRequestPathUtils.parseAndCache(request); + ModelAndView mv = controller.handleRequest(request, new MockHttpServletResponse()); assertThat(mv.getViewName()).isEqualTo("docs/cvs/commit"); assertThat(mv.getModel().isEmpty()).isTrue(); } @Test - public void settingPrefixToNullCausesEmptyStringToBeUsed() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); - ctrl.setPrefix(null); - assertThat(ctrl.getPrefix()).as("For setPrefix(..) with null, the empty string must be used instead.").isNotNull(); - assertThat(ctrl.getPrefix()).as("For setPrefix(..) with null, the empty string must be used instead.").isEqualTo(""); + void settingPrefixToNullCausesEmptyStringToBeUsed() { + UrlFilenameViewController controller = new UrlFilenameViewController(); + controller.setPrefix(null); + assertThat(controller.getPrefix()) + .as("For setPrefix(..) with null, the empty string must be used instead.") + .isNotNull(); + assertThat(controller.getPrefix()) + .as("For setPrefix(..) with null, the empty string must be used instead.") + .isEqualTo(""); } @Test - public void settingSuffixToNullCausesEmptyStringToBeUsed() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); - ctrl.setSuffix(null); - assertThat(ctrl.getSuffix()).as("For setPrefix(..) with null, the empty string must be used instead.").isNotNull(); - assertThat(ctrl.getSuffix()).as("For setPrefix(..) with null, the empty string must be used instead.").isEqualTo(""); + void settingSuffixToNullCausesEmptyStringToBeUsed() { + UrlFilenameViewController controller = new UrlFilenameViewController(); + controller.setSuffix(null); + assertThat(controller.getSuffix()) + .as("For setPrefix(..) with null, the empty string must be used instead.") + .isNotNull(); + assertThat(controller.getSuffix()) + .as("For setPrefix(..) with null, the empty string must be used instead.") + .isEqualTo(""); } /** * This is the expected behavior, and it now has a test to prove it. * https://opensource.atlassian.com/projects/spring/browse/SPR-2789 */ - @Test - public void nestedPathisUsedAsViewName_InBreakingChangeFromSpring12Line() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/products/view.html"); - MockHttpServletResponse response = new MockHttpServletResponse(); - ModelAndView mv = ctrl.handleRequest(request, response); + @PathPatternsParameterizedTest + void nestedPathisUsedAsViewName_InBreakingChangeFromSpring12Line( + Function requestFactory) throws Exception { + + UrlFilenameViewController controller = new UrlFilenameViewController(); + MockHttpServletRequest request = requestFactory.apply("/products/view.html"); + ModelAndView mv = controller.handleRequest(request, new MockHttpServletResponse()); assertThat(mv.getViewName()).isEqualTo("products/view"); assertThat(mv.getModel().isEmpty()).isTrue(); } - @Test - public void withFlashAttributes() throws Exception { - UrlFilenameViewController ctrl = new UrlFilenameViewController(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/index"); + @PathPatternsParameterizedTest + void withFlashAttributes(Function requestFactory) throws Exception { + UrlFilenameViewController controller = new UrlFilenameViewController(); + MockHttpServletRequest request = requestFactory.apply("/index"); request.setAttribute(DispatcherServlet.INPUT_FLASH_MAP_ATTRIBUTE, new ModelMap("name", "value")); - MockHttpServletResponse response = new MockHttpServletResponse(); - ModelAndView mv = ctrl.handleRequest(request, response); + ModelAndView mv = controller.handleRequest(request, new MockHttpServletResponse()); assertThat(mv.getViewName()).isEqualTo("index"); assertThat(mv.getModel().size()).isEqualTo(1); assertThat(mv.getModel().get("name")).isEqualTo("value"); } private void exposePathInMapping(MockHttpServletRequest request, String mapping) { - String pathInMapping = this.pathMatcher.extractPathWithinPattern(mapping, request.getRequestURI()); + String pathInMapping = new AntPathMatcher().extractPathWithinPattern(mapping, request.getRequestURI()); request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathInMapping); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java index b1cd1b4b3b..73678e8948 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java @@ -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,80 +17,83 @@ package org.springframework.web.servlet.mvc; import java.util.Properties; +import java.util.function.Function; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.springframework.web.servlet.handler.PathPatternsParameterizedTest; +import org.springframework.web.servlet.handler.PathPatternsTestUtils; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - - /** + * Unit tests for {@link WebContentInterceptor}. * @author Rick Evans * @author Brian Clozel + * @author Rossen Stoyanchev */ -public class WebContentInterceptorTests { +class WebContentInterceptorTests { - private MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + private final MockHttpServletResponse response = new MockHttpServletResponse(); - private MockHttpServletResponse response = new MockHttpServletResponse(); + private final WebContentInterceptor interceptor = new WebContentInterceptor(); + + private final Object handler = new Object(); - @Test - public void cacheResourcesConfiguration() throws Exception { - WebContentInterceptor interceptor = new WebContentInterceptor(); + @SuppressWarnings("unused") + private static Stream> pathPatternsArguments() { + return PathPatternsTestUtils.requestArguments(); + } + + + @PathPatternsParameterizedTest + void cacheResourcesConfiguration(Function requestFactory) throws Exception { interceptor.setCacheSeconds(10); - - interceptor.preHandle(request, response, null); + interceptor.preHandle(requestFactory.apply("/"), response, handler); Iterable cacheControlHeaders = response.getHeaders("Cache-Control"); assertThat(cacheControlHeaders).contains("max-age=10"); } - @Test - public void mappedCacheConfigurationOverridesGlobal() throws Exception { + @PathPatternsParameterizedTest + void mappedCacheConfigurationOverridesGlobal(Function requestFactory) throws Exception { Properties mappings = new Properties(); - mappings.setProperty("*/*handle.vm", "-1"); // was **/*handle.vm + mappings.setProperty("/*/*handle.vm", "-1"); - WebContentInterceptor interceptor = new WebContentInterceptor(); interceptor.setCacheSeconds(10); interceptor.setCacheMappings(mappings); - // request.setRequestURI("http://localhost:7070/example/adminhandle.vm"); - request.setRequestURI("example/adminhandle.vm"); - interceptor.preHandle(request, response, null); + MockHttpServletRequest request = requestFactory.apply("/example/adminhandle.vm"); + interceptor.preHandle(request, response, handler); Iterable cacheControlHeaders = response.getHeaders("Cache-Control"); assertThat(cacheControlHeaders).isEmpty(); - // request.setRequestURI("http://localhost:7070/example/bingo.html"); - request.setRequestURI("example/bingo.html"); - interceptor.preHandle(request, response, null); + request = requestFactory.apply("/example/bingo.html"); + interceptor.preHandle(request, response, handler); cacheControlHeaders = response.getHeaders("Cache-Control"); assertThat(cacheControlHeaders).contains("max-age=10"); } - @Test - public void preventCacheConfiguration() throws Exception { - WebContentInterceptor interceptor = new WebContentInterceptor(); + @PathPatternsParameterizedTest + void preventCacheConfiguration(Function requestFactory) throws Exception { interceptor.setCacheSeconds(0); - - interceptor.preHandle(request, response, null); + interceptor.preHandle(requestFactory.apply("/"), response, handler); Iterable cacheControlHeaders = response.getHeaders("Cache-Control"); assertThat(cacheControlHeaders).contains("no-store"); } - @Test - public void emptyCacheConfiguration() throws Exception { - WebContentInterceptor interceptor = new WebContentInterceptor(); + @PathPatternsParameterizedTest + void emptyCacheConfiguration(Function requestFactory) throws Exception { interceptor.setCacheSeconds(-1); - - interceptor.preHandle(request, response, null); + interceptor.preHandle(requestFactory.apply("/"), response, handler); Iterable expiresHeaders = response.getHeaders("Expires"); assertThat(expiresHeaders).isEmpty(); @@ -98,50 +101,45 @@ public class WebContentInterceptorTests { assertThat(cacheControlHeaders).isEmpty(); } - // SPR-13252, SPR-14053 - @Test - public void cachingConfigAndPragmaHeader() throws Exception { - WebContentInterceptor interceptor = new WebContentInterceptor(); - interceptor.setCacheSeconds(10); + @PathPatternsParameterizedTest // SPR-13252, SPR-14053 + void cachingConfigAndPragmaHeader(Function requestFactory) throws Exception { response.setHeader("Pragma", "no-cache"); response.setHeader("Expires", "0"); - interceptor.preHandle(request, response, null); + interceptor.setCacheSeconds(10); + interceptor.preHandle(requestFactory.apply("/"), response, handler); assertThat(response.getHeader("Pragma")).isEqualTo(""); assertThat(response.getHeader("Expires")).isEqualTo(""); } - // SPR-13252, SPR-14053 @SuppressWarnings("deprecation") - @Test - public void http10CachingConfigAndPragmaHeader() throws Exception { - WebContentInterceptor interceptor = new WebContentInterceptor(); + @PathPatternsParameterizedTest // SPR-13252, SPR-14053 + void http10CachingConfigAndPragmaHeader(Function requestFactory) throws Exception { + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + interceptor.setCacheSeconds(10); interceptor.setAlwaysMustRevalidate(true); - response.setHeader("Pragma", "no-cache"); - response.setHeader("Expires", "0"); - - interceptor.preHandle(request, response, null); + interceptor.preHandle(requestFactory.apply("/"), response, handler); assertThat(response.getHeader("Pragma")).isEqualTo(""); assertThat(response.getHeader("Expires")).isEqualTo(""); } @SuppressWarnings("deprecation") - @Test - public void http10CachingConfigAndSpecificMapping() throws Exception { - WebContentInterceptor interceptor = new WebContentInterceptor(); + @PathPatternsParameterizedTest + void http10CachingConfigAndSpecificMapping(Function requestFactory) throws Exception { interceptor.setCacheSeconds(0); interceptor.setUseExpiresHeader(true); interceptor.setAlwaysMustRevalidate(true); Properties mappings = new Properties(); - mappings.setProperty("*/*.cache.html", "10"); // was **/*.cache.html + mappings.setProperty("/*/*.cache.html", "10"); interceptor.setCacheMappings(mappings); - // request.setRequestURI("https://example.org/foo/page.html"); - request.setRequestURI("foo/page.html"); - interceptor.preHandle(request, response, null); + MockHttpServletRequest request = requestFactory.apply("/foo/page.html"); + MockHttpServletResponse response = new MockHttpServletResponse(); + interceptor.preHandle(request, response, handler); Iterable expiresHeaders = response.getHeaders("Expires"); assertThat(expiresHeaders).hasSize(1); @@ -150,10 +148,9 @@ public class WebContentInterceptorTests { Iterable pragmaHeaders = response.getHeaders("Pragma"); assertThat(pragmaHeaders).containsExactly("no-cache"); - // request.setRequestURI("https://example.org/page.cache.html"); - request = new MockHttpServletRequest("GET", "foo/page.cache.html"); + request = requestFactory.apply("/foo/page.cache.html"); response = new MockHttpServletResponse(); - interceptor.preHandle(request, response, null); + interceptor.preHandle(request, response, handler); expiresHeaders = response.getHeaders("Expires"); assertThat(expiresHeaders).hasSize(1); @@ -162,10 +159,9 @@ public class WebContentInterceptorTests { } @Test - public void throwsExceptionWithNullPathMatcher() throws Exception { - WebContentInterceptor interceptor = new WebContentInterceptor(); - assertThatIllegalArgumentException().isThrownBy(() -> - interceptor.setPathMatcher(null)); + void throwsExceptionWithNullPathMatcher() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new WebContentInterceptor().setPathMatcher(null)); } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestConditionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestConditionTests.java new file mode 100644 index 0000000000..bc82d1f0b9 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestConditionTests.java @@ -0,0 +1,206 @@ +/* + * 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.servlet.mvc.condition; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.jupiter.api.Test; + +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.pattern.PathPatternParser; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link PathPatternsRequestCondition}. + * @author Rossen Stoyanchev + */ +public class PathPatternsRequestConditionTests { + + private static final PathPatternParser parser = new PathPatternParser(); + + + @Test + void prependSlash() { + assertThat(createCondition("foo").getPatternValues().iterator().next()) + .isEqualTo("/foo"); + } + + @Test + void prependNonEmptyPatternsOnly() { + assertThat(createCondition("").getPatternValues().iterator().next()) + .as("Do not prepend empty patterns (SPR-8255)") + .isEqualTo(""); + } + + @Test + void combineEmptySets() { + PathPatternsRequestCondition c1 = createCondition(); + PathPatternsRequestCondition c2 = createCondition(); + PathPatternsRequestCondition c3 = c1.combine(c2); + + assertThat(c3).isSameAs(c1); + assertThat(c1.getPatternValues()).isSameAs(c2.getPatternValues()).containsExactly(""); + } + + @Test + void combineOnePatternWithEmptySet() { + PathPatternsRequestCondition c1 = createCondition("/type1", "/type2"); + PathPatternsRequestCondition c2 = createCondition(); + + assertThat(c1.combine(c2)).isEqualTo(createCondition("/type1", "/type2")); + + c1 = createCondition(); + c2 = createCondition("/method1", "/method2"); + + assertThat(c1.combine(c2)).isEqualTo(createCondition("/method1", "/method2")); + } + + @Test + void combineMultiplePatterns() { + PathPatternsRequestCondition c1 = createCondition("/t1", "/t2"); + PathPatternsRequestCondition c2 = createCondition("/m1", "/m2"); + + assertThat(c1.combine(c2)).isEqualTo(createCondition("/t1/m1", "/t1/m2", "/t2/m1", "/t2/m2")); + } + + @Test + void matchDirectPath() { + MockHttpServletRequest request = createRequest("/foo"); + + PathPatternsRequestCondition condition = createCondition("/foo"); + PathPatternsRequestCondition match = condition.getMatchingCondition(request); + + assertThat(match).isNotNull(); + } + + @Test + void matchPattern() { + MockHttpServletRequest request = createRequest("/foo/bar"); + + + PathPatternsRequestCondition condition = createCondition("/foo/*"); + PathPatternsRequestCondition match = condition.getMatchingCondition(request); + + assertThat(match).isNotNull(); + } + + @Test + void matchPatternWithContextPath() { + MockHttpServletRequest request = createRequest("/app", "/app/foo/bar"); + + PathPatternsRequestCondition condition = createCondition("/foo/*"); + PathPatternsRequestCondition match = condition.getMatchingCondition(request); + + assertThat(match).isNotNull(); + } + + @Test + void matchSortPatterns() { + MockHttpServletRequest request = createRequest("/foo/bar"); + + PathPatternsRequestCondition condition = createCondition("/**", "/foo/bar", "/foo/*"); + PathPatternsRequestCondition match = condition.getMatchingCondition(request); + PathPatternsRequestCondition expected = createCondition("/foo/bar", "/foo/*", "/**"); + + assertThat(match).isEqualTo(expected); + } + + @Test + void matchTrailingSlash() { + MockHttpServletRequest request = createRequest("/foo/"); + + PathPatternsRequestCondition condition = createCondition("/foo"); + PathPatternsRequestCondition match = condition.getMatchingCondition(request); + + assertThat(match).isNotNull(); + assertThat(match.getPatternValues().iterator().next()).as("Should match by default").isEqualTo("/foo"); + + PathPatternParser strictParser = new PathPatternParser(); + strictParser.setMatchOptionalTrailingSeparator(false); + + condition = new PathPatternsRequestCondition(strictParser, "/foo"); + match = condition.getMatchingCondition(request); + + assertThat(match).isNull(); + } + + @Test + void matchPatternContainsExtension() { + MockHttpServletRequest request = createRequest("/foo.html"); + PathPatternsRequestCondition match = createCondition("/foo.jpg").getMatchingCondition(request); + + assertThat(match).isNull(); + } + + @Test // gh-22543 + void matchWithEmptyPatterns() { + PathPatternsRequestCondition condition = createCondition(); + assertThat(condition.getMatchingCondition(createRequest(""))).isNotNull(); + assertThat(condition.getMatchingCondition(createRequest("/anything"))).isNull(); + + condition = condition.combine(createCondition()); + assertThat(condition.getMatchingCondition(createRequest(""))).isNotNull(); + assertThat(condition.getMatchingCondition(createRequest("/anything"))).isNull(); + } + + @Test + void compareEqualPatterns() { + PathPatternsRequestCondition c1 = createCondition("/foo*"); + PathPatternsRequestCondition c2 = createCondition("/foo*"); + + assertThat(c1.compareTo(c2, createRequest("/foo"))).isEqualTo(0); + } + + @Test + void comparePatternSpecificity() { + PathPatternsRequestCondition c1 = createCondition("/fo*"); + PathPatternsRequestCondition c2 = createCondition("/foo"); + + assertThat(c1.compareTo(c2, createRequest("/foo"))).isEqualTo(1); + } + + @Test + void compareNumberOfMatchingPatterns() { + HttpServletRequest request = createRequest("/foo"); + + PathPatternsRequestCondition c1 = createCondition("/foo", "/bar"); + PathPatternsRequestCondition c2 = createCondition("/foo", "/f*"); + + PathPatternsRequestCondition match1 = c1.getMatchingCondition(request); + PathPatternsRequestCondition match2 = c2.getMatchingCondition(request); + + assertThat(match1.compareTo(match2, request)).isEqualTo(1); + } + + + private MockHttpServletRequest createRequest(String requestURI) { + return createRequest("", requestURI); + } + + private MockHttpServletRequest createRequest(String contextPath, String requestURI) { + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestURI); + request.setContextPath(contextPath); + ServletRequestPathUtils.parseAndCache(request); + return request; + } + + private PathPatternsRequestCondition createCondition(String... patterns) { + return new PathPatternsRequestCondition(parser, patterns); + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/PatternsRequestConditionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/PatternsRequestConditionTests.java index 4ae8551ecf..02a4aa977a 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/PatternsRequestConditionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/PatternsRequestConditionTests.java @@ -16,7 +16,6 @@ package org.springframework.web.servlet.mvc.condition; -import java.util.Arrays; import java.util.Collections; import javax.servlet.http.HttpServletRequest; @@ -24,28 +23,32 @@ import javax.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Test; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.util.UrlPathHelper; import static org.assertj.core.api.Assertions.assertThat; /** + * Unit tests for {@link PatternsRequestCondition}. + * * @author Rossen Stoyanchev */ -public class PatternsRequestConditionTests { +class PatternsRequestConditionTests { @Test - public void prependSlash() { - PatternsRequestCondition c = new PatternsRequestCondition("foo"); - assertThat(c.getPatterns().iterator().next()).isEqualTo("/foo"); + void prependSlash() { + assertThat(new PatternsRequestCondition("foo").getPatterns().iterator().next()) + .isEqualTo("/foo"); } @Test - public void prependNonEmptyPatternsOnly() { - PatternsRequestCondition c = new PatternsRequestCondition(""); - assertThat(c.getPatterns().iterator().next()).as("Do not prepend empty patterns (SPR-8255)").isEqualTo(""); + void prependNonEmptyPatternsOnly() { + assertThat(new PatternsRequestCondition("").getPatterns().iterator().next()) + .as("Do not prepend empty patterns (SPR-8255)") + .isEqualTo(""); } @Test - public void combineEmptySets() { + void combineEmptySets() { PatternsRequestCondition c1 = new PatternsRequestCondition(); PatternsRequestCondition c2 = new PatternsRequestCondition(); PatternsRequestCondition c3 = c1.combine(c2); @@ -55,7 +58,7 @@ public class PatternsRequestConditionTests { } @Test - public void combineOnePatternWithEmptySet() { + void combineOnePatternWithEmptySet() { PatternsRequestCondition c1 = new PatternsRequestCondition("/type1", "/type2"); PatternsRequestCondition c2 = new PatternsRequestCondition(); @@ -68,7 +71,7 @@ public class PatternsRequestConditionTests { } @Test - public void combineMultiplePatterns() { + void combineMultiplePatterns() { PatternsRequestCondition c1 = new PatternsRequestCondition("/t1", "/t2"); PatternsRequestCondition c2 = new PatternsRequestCondition("/m1", "/m2"); @@ -76,42 +79,49 @@ public class PatternsRequestConditionTests { } @Test - public void matchDirectPath() { + void matchDirectPath() { PatternsRequestCondition condition = new PatternsRequestCondition("/foo"); - PatternsRequestCondition match = condition.getMatchingCondition(new MockHttpServletRequest("GET", "/foo")); + PatternsRequestCondition match = condition.getMatchingCondition(initRequest("/foo")); assertThat(match).isNotNull(); } @Test - public void matchPattern() { + void matchPattern() { + MockHttpServletRequest request = initRequest("/foo/bar"); + PatternsRequestCondition condition = new PatternsRequestCondition("/foo/*"); - PatternsRequestCondition match = condition.getMatchingCondition(new MockHttpServletRequest("GET", "/foo/bar")); + PatternsRequestCondition match = condition.getMatchingCondition(request); assertThat(match).isNotNull(); } @Test - public void matchSortPatterns() { + void matchSortPatterns() { + MockHttpServletRequest request = initRequest("/foo/bar"); + PatternsRequestCondition condition = new PatternsRequestCondition("/**", "/foo/bar", "/foo/*"); - PatternsRequestCondition match = condition.getMatchingCondition(new MockHttpServletRequest("GET", "/foo/bar")); + PatternsRequestCondition match = condition.getMatchingCondition(request); PatternsRequestCondition expected = new PatternsRequestCondition("/foo/bar", "/foo/*", "/**"); assertThat(match).isEqualTo(expected); } @Test - public void matchSuffixPattern() { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.html"); + void matchSuffixPattern() { + MockHttpServletRequest request = initRequest("/foo.html"); - PatternsRequestCondition condition = new PatternsRequestCondition("/{foo}"); + boolean useSuffixPatternMatch = true; + PatternsRequestCondition condition = + new PatternsRequestCondition(new String[] {"/{foo}"}, null, null, useSuffixPatternMatch, true); PatternsRequestCondition match = condition.getMatchingCondition(request); assertThat(match).isNotNull(); assertThat(match.getPatterns().iterator().next()).isEqualTo("/{foo}.*"); - boolean useSuffixPatternMatch = false; - condition = new PatternsRequestCondition(new String[] {"/{foo}"}, null, null, useSuffixPatternMatch, false); + useSuffixPatternMatch = false; + condition = new PatternsRequestCondition( + new String[] {"/{foo}"}, null, null, useSuffixPatternMatch, false); match = condition.getMatchingCondition(request); assertThat(match).isNotNull(); @@ -119,17 +129,17 @@ public class PatternsRequestConditionTests { } @Test // SPR-8410 - public void matchSuffixPatternUsingFileExtensions() { + void matchSuffixPatternUsingFileExtensions() { PatternsRequestCondition condition = new PatternsRequestCondition( new String[] {"/jobs/{jobName}"}, null, null, true, false, Collections.singletonList("json")); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/jobs/my.job"); + MockHttpServletRequest request = initRequest("/jobs/my.job"); PatternsRequestCondition match = condition.getMatchingCondition(request); assertThat(match).isNotNull(); assertThat(match.getPatterns().iterator().next()).isEqualTo("/jobs/{jobName}"); - request = new MockHttpServletRequest("GET", "/jobs/my.job.json"); + request = initRequest("/jobs/my.job.json"); match = condition.getMatchingCondition(request); assertThat(match).isNotNull(); @@ -137,24 +147,24 @@ public class PatternsRequestConditionTests { } @Test - public void matchSuffixPatternUsingFileExtensions2() { + void matchSuffixPatternUsingFileExtensions2() { PatternsRequestCondition condition1 = new PatternsRequestCondition( - new String[] {"/prefix"}, null, null, true, false, Arrays.asList("json")); + new String[] {"/prefix"}, null, null, true, false, Collections.singletonList("json")); PatternsRequestCondition condition2 = new PatternsRequestCondition( new String[] {"/suffix"}, null, null, true, false, null); PatternsRequestCondition combined = condition1.combine(condition2); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/prefix/suffix.json"); + MockHttpServletRequest request = initRequest("/prefix/suffix.json"); PatternsRequestCondition match = combined.getMatchingCondition(request); assertThat(match).isNotNull(); } @Test - public void matchTrailingSlash() { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo/"); + void matchTrailingSlash() { + MockHttpServletRequest request = initRequest("/foo/"); PatternsRequestCondition condition = new PatternsRequestCondition("/foo"); PatternsRequestCondition match = condition.getMatchingCondition(request); @@ -162,7 +172,7 @@ public class PatternsRequestConditionTests { assertThat(match).isNotNull(); assertThat(match.getPatterns().iterator().next()).as("Should match by default").isEqualTo("/foo/"); - condition = new PatternsRequestCondition(new String[] {"/foo"}, null, null, false, true); + condition = new PatternsRequestCondition(new String[] {"/foo"}, true, null); match = condition.getMatchingCondition(request); assertThat(match).isNotNull(); @@ -170,53 +180,53 @@ public class PatternsRequestConditionTests { .as("Trailing slash should be insensitive to useSuffixPatternMatch settings (SPR-6164, SPR-5636)") .isEqualTo("/foo/"); - condition = new PatternsRequestCondition(new String[] {"/foo"}, null, null, false); + condition = new PatternsRequestCondition(new String[] {"/foo"}, false, null); match = condition.getMatchingCondition(request); assertThat(match).isNull(); } @Test - public void matchPatternContainsExtension() { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.html"); + void matchPatternContainsExtension() { + MockHttpServletRequest request = initRequest("/foo.html"); PatternsRequestCondition match = new PatternsRequestCondition("/foo.jpg").getMatchingCondition(request); assertThat(match).isNull(); } @Test // gh-22543 - public void matchWithEmptyPatterns() { + void matchWithEmptyPatterns() { PatternsRequestCondition condition = new PatternsRequestCondition(); - assertThat(condition.getMatchingCondition(new MockHttpServletRequest("GET", ""))).isNotNull(); - assertThat(condition.getMatchingCondition(new MockHttpServletRequest("GET", "/anything"))).isNull(); + assertThat(condition.getMatchingCondition(initRequest(""))).isNotNull(); + assertThat(condition.getMatchingCondition(initRequest("/anything"))).isNull(); condition = condition.combine(new PatternsRequestCondition()); - assertThat(condition.getMatchingCondition(new MockHttpServletRequest("GET", ""))).isNotNull(); - assertThat(condition.getMatchingCondition(new MockHttpServletRequest("GET", "/anything"))).isNull(); + assertThat(condition.getMatchingCondition(initRequest(""))).isNotNull(); + assertThat(condition.getMatchingCondition(initRequest("/anything"))).isNull(); } @Test - public void compareEqualPatterns() { + void compareEqualPatterns() { PatternsRequestCondition c1 = new PatternsRequestCondition("/foo*"); PatternsRequestCondition c2 = new PatternsRequestCondition("/foo*"); - assertThat(c1.compareTo(c2, new MockHttpServletRequest("GET", "/foo"))).isEqualTo(0); + assertThat(c1.compareTo(c2, initRequest("/foo"))).isEqualTo(0); } @Test - public void comparePatternSpecificity() { + void comparePatternSpecificity() { PatternsRequestCondition c1 = new PatternsRequestCondition("/fo*"); PatternsRequestCondition c2 = new PatternsRequestCondition("/foo"); - assertThat(c1.compareTo(c2, new MockHttpServletRequest("GET", "/foo"))).isEqualTo(1); + assertThat(c1.compareTo(c2, initRequest("/foo"))).isEqualTo(1); } @Test - public void compareNumberOfMatchingPatterns() { - HttpServletRequest request = new MockHttpServletRequest("GET", "/foo.html"); + void compareNumberOfMatchingPatterns() { + HttpServletRequest request = initRequest("/foo.html"); - PatternsRequestCondition c1 = new PatternsRequestCondition("/foo", "*.jpeg"); - PatternsRequestCondition c2 = new PatternsRequestCondition("/foo", "*.html"); + PatternsRequestCondition c1 = new PatternsRequestCondition("/foo.html", "*.jpeg"); + PatternsRequestCondition c2 = new PatternsRequestCondition("/foo.html", "*.html"); PatternsRequestCondition match1 = c1.getMatchingCondition(request); PatternsRequestCondition match2 = c2.getMatchingCondition(request); @@ -224,4 +234,11 @@ public class PatternsRequestConditionTests { assertThat(match1.compareTo(match2, request)).isEqualTo(1); } + + private MockHttpServletRequest initRequest(String requestUri) { + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request); + return request; + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java index f8b6f7971b..6073c8276b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import javax.servlet.http.HttpServletRequest; @@ -31,6 +32,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.http.server.RequestPath; import org.springframework.stereotype.Controller; import org.springframework.util.MultiValueMap; import org.springframework.web.HttpMediaTypeNotAcceptableException; @@ -49,14 +51,11 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.handler.MappedInterceptor; -import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; -import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition; -import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition; -import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; -import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition; -import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; +import org.springframework.web.servlet.handler.PathPatternsParameterizedTest; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -67,9 +66,25 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; * @author Arjen Poutsma * @author Rossen Stoyanchev */ -public class RequestMappingInfoHandlerMappingTests { +class RequestMappingInfoHandlerMappingTests { + + @SuppressWarnings("unused") + static Stream pathPatternsArguments() { + TestController controller = new TestController(); + + TestRequestMappingInfoHandlerMapping mapping1 = new TestRequestMappingInfoHandlerMapping(); + mapping1.setPatternParser(new PathPatternParser()); + + TestRequestMappingInfoHandlerMapping mapping2 = new TestRequestMappingInfoHandlerMapping(); + mapping2.setRemoveSemicolonContent(false); + + return Stream.of(mapping1, mapping2).peek(mapping -> { + mapping.setApplicationContext(new StaticWebApplicationContext()); + mapping.registerHandler(controller); + mapping.afterPropertiesSet(); + }); + } - private TestRequestMappingInfoHandlerMapping handlerMapping; private HandlerMethod fooMethod; @@ -81,148 +96,145 @@ public class RequestMappingInfoHandlerMappingTests { @BeforeEach - public void setup() throws Exception { - TestController testController = new TestController(); - - this.fooMethod = new HandlerMethod(testController, "foo"); - this.fooParamMethod = new HandlerMethod(testController, "fooParam"); - this.barMethod = new HandlerMethod(testController, "bar"); - this.emptyMethod = new HandlerMethod(testController, "empty"); - - this.handlerMapping = new TestRequestMappingInfoHandlerMapping(); - this.handlerMapping.registerHandler(testController); - this.handlerMapping.setRemoveSemicolonContent(false); + void setup() throws Exception { + TestController controller = new TestController(); + this.fooMethod = new HandlerMethod(controller, "foo"); + this.fooParamMethod = new HandlerMethod(controller, "fooParam"); + this.barMethod = new HandlerMethod(controller, "bar"); + this.emptyMethod = new HandlerMethod(controller, "empty"); } - @Test - public void getMappingPathPatterns() throws Exception { + @PathPatternsParameterizedTest + void getMappingPathPatterns(TestRequestMappingInfoHandlerMapping mapping) { String[] patterns = {"/foo/*", "/foo", "/bar/*", "/bar"}; - RequestMappingInfo info = RequestMappingInfo.paths(patterns).build(); - Set actual = this.handlerMapping.getMappingPathPatterns(info); + RequestMappingInfo info = mapping.createInfo(patterns); + Set actual = mapping.getMappingPathPatterns(info); assertThat(actual).isEqualTo(new HashSet<>(Arrays.asList(patterns))); } - @Test - public void getHandlerDirectMatch() throws Exception { + @PathPatternsParameterizedTest + void getHandlerDirectMatch(TestRequestMappingInfoHandlerMapping mapping) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - HandlerMethod handlerMethod = getHandler(request); + HandlerMethod handlerMethod = getHandler(mapping, request); assertThat(handlerMethod.getMethod()).isEqualTo(this.fooMethod.getMethod()); } - @Test - public void getHandlerGlobMatch() throws Exception { + @PathPatternsParameterizedTest + void getHandlerGlobMatch(TestRequestMappingInfoHandlerMapping mapping) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bar"); - HandlerMethod handlerMethod = getHandler(request); + HandlerMethod handlerMethod = getHandler(mapping, request); assertThat(handlerMethod.getMethod()).isEqualTo(this.barMethod.getMethod()); } - @Test - public void getHandlerEmptyPathMatch() throws Exception { + @PathPatternsParameterizedTest + void getHandlerEmptyPathMatch(TestRequestMappingInfoHandlerMapping mapping) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); - HandlerMethod handlerMethod = getHandler(request); + HandlerMethod handlerMethod = getHandler(mapping, request); assertThat(handlerMethod.getMethod()).isEqualTo(this.emptyMethod.getMethod()); request = new MockHttpServletRequest("GET", "/"); - handlerMethod = getHandler(request); + handlerMethod = getHandler(mapping, request); assertThat(handlerMethod.getMethod()).isEqualTo(this.emptyMethod.getMethod()); } - @Test - public void getHandlerBestMatch() throws Exception { + @PathPatternsParameterizedTest + void getHandlerBestMatch(TestRequestMappingInfoHandlerMapping mapping) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setParameter("p", "anything"); - HandlerMethod handlerMethod = getHandler(request); + HandlerMethod handlerMethod = getHandler(mapping, request); assertThat(handlerMethod.getMethod()).isEqualTo(this.fooParamMethod.getMethod()); } - @Test - public void getHandlerRequestMethodNotAllowed() throws Exception { + @PathPatternsParameterizedTest + void getHandlerRequestMethodNotAllowed(TestRequestMappingInfoHandlerMapping mapping) { MockHttpServletRequest request = new MockHttpServletRequest("POST", "/bar"); - assertThatExceptionOfType(HttpRequestMethodNotSupportedException.class).isThrownBy(() -> - this.handlerMapping.getHandler(request)) - .satisfies(ex -> assertThat(ex.getSupportedMethods()).containsExactly("GET", "HEAD")); + assertThatExceptionOfType(HttpRequestMethodNotSupportedException.class) + .isThrownBy(() -> mapping.getHandler(request)) + .satisfies(ex -> assertThat(ex.getSupportedMethods()).containsExactly("GET", "HEAD")); } - @Test // SPR-9603 - public void getHandlerRequestMethodMatchFalsePositive() throws Exception { + @PathPatternsParameterizedTest // SPR-9603 + void getHandlerRequestMethodMatchFalsePositive(TestRequestMappingInfoHandlerMapping mapping) { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/users"); request.addHeader("Accept", "application/xml"); - this.handlerMapping.registerHandler(new UserController()); - assertThatExceptionOfType(HttpMediaTypeNotAcceptableException.class).isThrownBy(() -> - this.handlerMapping.getHandler(request)); + mapping.registerHandler(new UserController()); + assertThatExceptionOfType(HttpMediaTypeNotAcceptableException.class) + .isThrownBy(() -> mapping.getHandler(request)); } - @Test // SPR-8462 - public void getHandlerMediaTypeNotSupported() throws Exception { - testHttpMediaTypeNotSupportedException("/person/1"); - testHttpMediaTypeNotSupportedException("/person/1/"); - testHttpMediaTypeNotSupportedException("/person/1.json"); + @PathPatternsParameterizedTest // SPR-8462 + void getHandlerMediaTypeNotSupported(TestRequestMappingInfoHandlerMapping mapping) { + testHttpMediaTypeNotSupportedException(mapping, "/person/1"); + testHttpMediaTypeNotSupportedException(mapping, "/person/1/"); + testHttpMediaTypeNotSupportedException(mapping, "/person/1.json"); } - @Test - public void getHandlerHttpOptions() throws Exception { - testHttpOptions("/foo", "GET,HEAD,OPTIONS"); - testHttpOptions("/person/1", "PUT,OPTIONS"); - testHttpOptions("/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS"); - testHttpOptions("/something", "PUT,POST"); + @PathPatternsParameterizedTest + void getHandlerHttpOptions(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + testHttpOptions(mapping, "/foo", "GET,HEAD,OPTIONS"); + testHttpOptions(mapping, "/person/1", "PUT,OPTIONS"); + testHttpOptions(mapping, "/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS"); + testHttpOptions(mapping, "/something", "PUT,POST"); } - @Test - public void getHandlerTestInvalidContentType() throws Exception { + @PathPatternsParameterizedTest + void getHandlerTestInvalidContentType(TestRequestMappingInfoHandlerMapping mapping) { MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/person/1"); request.setContentType("bogus"); - assertThatExceptionOfType(HttpMediaTypeNotSupportedException.class).isThrownBy(() -> - this.handlerMapping.getHandler(request)) - .withMessage("Invalid mime type \"bogus\": does not contain '/'"); + assertThatExceptionOfType(HttpMediaTypeNotSupportedException.class) + .isThrownBy(() -> mapping.getHandler(request)) + .withMessage("Invalid mime type \"bogus\": does not contain '/'"); } - @Test // SPR-8462 - public void getHandlerMediaTypeNotAccepted() throws Exception { - testHttpMediaTypeNotAcceptableException("/persons"); - testHttpMediaTypeNotAcceptableException("/persons/"); - testHttpMediaTypeNotAcceptableException("/persons.json"); + @PathPatternsParameterizedTest // SPR-8462 + void getHandlerMediaTypeNotAccepted(TestRequestMappingInfoHandlerMapping mapping) { + testHttpMediaTypeNotAcceptableException(mapping, "/persons"); + testHttpMediaTypeNotAcceptableException(mapping, "/persons/"); + if (mapping.getPatternParser() == null) { + testHttpMediaTypeNotAcceptableException(mapping, "/persons.json"); + } } - @Test // SPR-12854 - public void getHandlerUnsatisfiedServletRequestParameterException() throws Exception { + @PathPatternsParameterizedTest // SPR-12854 + void getHandlerUnsatisfiedServletRequestParameterException(TestRequestMappingInfoHandlerMapping mapping) { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/params"); - assertThatExceptionOfType(UnsatisfiedServletRequestParameterException.class).isThrownBy(() -> - this.handlerMapping.getHandler(request)) - .satisfies(ex -> assertThat(ex.getParamConditionGroups().stream().map(group -> group[0])) - .containsExactlyInAnyOrder("foo=bar", "bar=baz")); + assertThatExceptionOfType(UnsatisfiedServletRequestParameterException.class) + .isThrownBy(() -> mapping.getHandler(request)) + .satisfies(ex -> assertThat(ex.getParamConditionGroups().stream().map(group -> group[0])) + .containsExactlyInAnyOrder("foo=bar", "bar=baz")); } - @Test - public void getHandlerProducibleMediaTypesAttribute() throws Exception { + @PathPatternsParameterizedTest + void getHandlerProducibleMediaTypesAttribute(TestRequestMappingInfoHandlerMapping mapping) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/content"); request.addHeader("Accept", "application/xml"); - this.handlerMapping.getHandler(request); + mapping.getHandler(request); String name = HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; assertThat(request.getAttribute(name)).isEqualTo(Collections.singleton(MediaType.APPLICATION_XML)); request = new MockHttpServletRequest("GET", "/content"); request.addHeader("Accept", "application/json"); - this.handlerMapping.getHandler(request); + mapping.getHandler(request); assertThat(request.getAttribute(name)).as("Negated expression shouldn't be listed as producible type").isNull(); } @Test - public void getHandlerMappedInterceptors() throws Exception { + void getHandlerMappedInterceptors() throws Exception { String path = "/foo"; HandlerInterceptor interceptor = new HandlerInterceptor() {}; MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[] {path}, interceptor); TestRequestMappingInfoHandlerMapping mapping = new TestRequestMappingInfoHandlerMapping(); mapping.registerHandler(new TestController()); - mapping.setInterceptors(new Object[] { mappedInterceptor }); + mapping.setInterceptors(mappedInterceptor); mapping.setApplicationContext(new StaticWebApplicationContext()); HandlerExecutionChain chain = mapping.getHandler(new MockHttpServletRequest("GET", path)); @@ -235,12 +247,12 @@ public class RequestMappingInfoHandlerMappingTests { } @SuppressWarnings("unchecked") - @Test - public void handleMatchUriTemplateVariables() { + @PathPatternsParameterizedTest + void handleMatchUriTemplateVariables(TestRequestMappingInfoHandlerMapping mapping) { RequestMappingInfo key = RequestMappingInfo.paths("/{path1}/{path2}").build(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2"); String lookupPath = new UrlPathHelper().getLookupPathForRequest(request); - this.handlerMapping.handleMatch(key, lookupPath, request); + mapping.handleMatch(key, lookupPath, request); String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; Map uriVariables = (Map) request.getAttribute(name); @@ -251,8 +263,8 @@ public class RequestMappingInfoHandlerMappingTests { } @SuppressWarnings("unchecked") - @Test // SPR-9098 - public void handleMatchUriTemplateVariablesDecode() { + @PathPatternsParameterizedTest // SPR-9098 + void handleMatchUriTemplateVariablesDecode(TestRequestMappingInfoHandlerMapping mapping) { RequestMappingInfo key = RequestMappingInfo.paths("/{group}/{identifier}").build(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/group/a%2Fb"); @@ -260,8 +272,8 @@ public class RequestMappingInfoHandlerMappingTests { pathHelper.setUrlDecode(false); String lookupPath = pathHelper.getLookupPathForRequest(request); - this.handlerMapping.setUrlPathHelper(pathHelper); - this.handlerMapping.handleMatch(key, lookupPath, request); + mapping.setUrlPathHelper(pathHelper); + mapping.handleMatch(key, lookupPath, request); String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; Map uriVariables = (Map) request.getAttribute(name); @@ -271,32 +283,32 @@ public class RequestMappingInfoHandlerMappingTests { assertThat(uriVariables.get("identifier")).isEqualTo("a/b"); } - @Test - public void handleMatchBestMatchingPatternAttribute() { + @PathPatternsParameterizedTest + void handleMatchBestMatchingPatternAttribute(TestRequestMappingInfoHandlerMapping mapping) { RequestMappingInfo key = RequestMappingInfo.paths("/{path1}/2", "/**").build(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2"); - this.handlerMapping.handleMatch(key, "/1/2", request); + mapping.handleMatch(key, "/1/2", request); assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)).isEqualTo("/{path1}/2"); } - @Test // gh-22543 - public void handleMatchBestMatchingPatternAttributeNoPatternsDefined() { + @PathPatternsParameterizedTest // gh-22543 + void handleMatchBestMatchingPatternAttributeNoPatternsDefined(TestRequestMappingInfoHandlerMapping mapping) { String path = ""; MockHttpServletRequest request = new MockHttpServletRequest("GET", path); - this.handlerMapping.handleMatch(RequestMappingInfo.paths().build(), path, request); + mapping.handleMatch(RequestMappingInfo.paths().build(), path, request); assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)).isEqualTo(path); } - @Test - public void handleMatchMatrixVariables() { + @PathPatternsParameterizedTest + void handleMatchMatrixVariables(TestRequestMappingInfoHandlerMapping mapping) { MockHttpServletRequest request; MultiValueMap matrixVariables; Map uriVariables; // URI var parsed into path variable + matrix params.. - request = new MockHttpServletRequest(); - handleMatch(request, "/{cars}", "/cars;colors=red,blue,green;year=2012"); + request = new MockHttpServletRequest("GET", "/cars;colors=red,blue,green;year=2012"); + handleMatch(mapping, request, "/{cars}", request.getRequestURI()); matrixVariables = getMatrixVariables(request, "cars"); uriVariables = getUriTemplateVariables(request); @@ -307,8 +319,8 @@ public class RequestMappingInfoHandlerMappingTests { assertThat(uriVariables.get("cars")).isEqualTo("cars"); // URI var with regex for path variable, and URI var for matrix params.. - request = new MockHttpServletRequest(); - handleMatch(request, "/{cars:[^;]+}{params}", "/cars;colors=red,blue,green;year=2012"); + request = new MockHttpServletRequest("GET", "/cars;colors=red,blue,green;year=2012"); + handleMatch(mapping, request, "/{cars:[^;]+}{params}", request.getRequestURI()); matrixVariables = getMatrixVariables(request, "params"); uriVariables = getUriTemplateVariables(request); @@ -317,11 +329,13 @@ public class RequestMappingInfoHandlerMappingTests { assertThat(matrixVariables.get("colors")).isEqualTo(Arrays.asList("red", "blue", "green")); assertThat(matrixVariables.getFirst("year")).isEqualTo("2012"); assertThat(uriVariables.get("cars")).isEqualTo("cars"); - assertThat(uriVariables.get("params")).isEqualTo(";colors=red,blue,green;year=2012"); + if (mapping.getPatternParser() == null) { + assertThat(uriVariables.get("params")).isEqualTo(";colors=red,blue,green;year=2012"); + } // URI var with regex for path variable, and (empty) URI var for matrix params.. - request = new MockHttpServletRequest(); - handleMatch(request, "/{cars:[^;]+}{params}", "/cars"); + request = new MockHttpServletRequest("GET", "/cars"); + handleMatch(mapping, request, "/{cars:[^;]+}{params}", request.getRequestURI()); matrixVariables = getMatrixVariables(request, "params"); uriVariables = getUriTemplateVariables(request); @@ -331,32 +345,38 @@ public class RequestMappingInfoHandlerMappingTests { assertThat(uriVariables.get("params")).isEqualTo(""); // SPR-11897 - request = new MockHttpServletRequest(); - handleMatch(request, "/{foo}", "/a=42;b=c"); + request = new MockHttpServletRequest("GET", "/a=42;b=c"); + handleMatch(mapping, request, "/{foo}", request.getRequestURI()); matrixVariables = getMatrixVariables(request, "foo"); uriVariables = getUriTemplateVariables(request); assertThat(matrixVariables).isNotNull(); - assertThat(matrixVariables.size()).isEqualTo(2); - assertThat(matrixVariables.getFirst("a")).isEqualTo("42"); - assertThat(matrixVariables.getFirst("b")).isEqualTo("c"); - assertThat(uriVariables.get("foo")).isEqualTo("a=42"); + if (mapping.getPatternParser() != null) { + assertThat(matrixVariables.size()).isEqualTo(1); + assertThat(matrixVariables.getFirst("b")).isEqualTo("c"); + assertThat(uriVariables.get("foo")).isEqualTo("a=42"); + } + else { + assertThat(matrixVariables.size()).isEqualTo(2); + assertThat(matrixVariables.getFirst("a")).isEqualTo("42"); + assertThat(matrixVariables.getFirst("b")).isEqualTo("c"); + assertThat(uriVariables.get("foo")).isEqualTo("a=42"); + } } - @Test // SPR-10140, SPR-16867 - public void handleMatchMatrixVariablesDecoding() { + @PathPatternsParameterizedTest // SPR-10140, SPR-16867 + void handleMatchMatrixVariablesDecoding(TestRequestMappingInfoHandlerMapping mapping) { - MockHttpServletRequest request; + if (mapping.getPatternParser() == null) { + UrlPathHelper urlPathHelper = new UrlPathHelper(); + urlPathHelper.setUrlDecode(false); + urlPathHelper.setRemoveSemicolonContent(false); + mapping.setUrlPathHelper(urlPathHelper); + } - UrlPathHelper urlPathHelper = new UrlPathHelper(); - urlPathHelper.setUrlDecode(false); - urlPathHelper.setRemoveSemicolonContent(false); - - this.handlerMapping.setUrlPathHelper(urlPathHelper); - - request = new MockHttpServletRequest(); - handleMatch(request, "/{cars}", "/cars;mvar=a%2Fb"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/cars;mvar=a%2Fb"); + handleMatch(mapping, request, "/{cars}", request.getRequestURI()); MultiValueMap matrixVariables = getMatrixVariables(request, "cars"); Map uriVariables = getUriTemplateVariables(request); @@ -366,24 +386,27 @@ public class RequestMappingInfoHandlerMappingTests { assertThat(uriVariables.get("cars")).isEqualTo("cars"); } + private HandlerMethod getHandler( + TestRequestMappingInfoHandlerMapping mapping, MockHttpServletRequest request) throws Exception { - private HandlerMethod getHandler(MockHttpServletRequest request) throws Exception { - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); assertThat(chain).isNotNull(); return (HandlerMethod) chain.getHandler(); } - private void testHttpMediaTypeNotSupportedException(String url) throws Exception { + private void testHttpMediaTypeNotSupportedException(TestRequestMappingInfoHandlerMapping mapping, String url) { MockHttpServletRequest request = new MockHttpServletRequest("PUT", url); request.setContentType("application/json"); - assertThatExceptionOfType(HttpMediaTypeNotSupportedException.class).isThrownBy(() -> - this.handlerMapping.getHandler(request)) - .satisfies(ex -> assertThat(ex.getSupportedMediaTypes()).containsExactly(MediaType.APPLICATION_XML)); + assertThatExceptionOfType(HttpMediaTypeNotSupportedException.class) + .isThrownBy(() -> mapping.getHandler(request)) + .satisfies(ex -> assertThat(ex.getSupportedMediaTypes()).containsExactly(MediaType.APPLICATION_XML)); } - private void testHttpOptions(String requestURI, String allowHeader) throws Exception { + private void testHttpOptions( + TestRequestMappingInfoHandlerMapping mapping, String requestURI, String allowHeader) throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", requestURI); - HandlerMethod handlerMethod = getHandler(request); + HandlerMethod handlerMethod = getHandler(mapping, request); ServletWebRequest webRequest = new ServletWebRequest(request); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); @@ -394,17 +417,22 @@ public class RequestMappingInfoHandlerMappingTests { assertThat(((HttpHeaders) result).getFirst("Allow")).isEqualTo(allowHeader); } - private void testHttpMediaTypeNotAcceptableException(String url) throws Exception { + private void testHttpMediaTypeNotAcceptableException(TestRequestMappingInfoHandlerMapping mapping, String url) { MockHttpServletRequest request = new MockHttpServletRequest("GET", url); request.addHeader("Accept", "application/json"); assertThatExceptionOfType(HttpMediaTypeNotAcceptableException.class).isThrownBy(() -> - this.handlerMapping.getHandler(request)) + mapping.getHandler(request)) .satisfies(ex -> assertThat(ex.getSupportedMediaTypes()).containsExactly(MediaType.APPLICATION_XML)); } - private void handleMatch(MockHttpServletRequest request, String pattern, String lookupPath) { - RequestMappingInfo info = RequestMappingInfo.paths(pattern).build(); - this.handlerMapping.handleMatch(info, lookupPath, request); + private void handleMatch(TestRequestMappingInfoHandlerMapping mapping, + MockHttpServletRequest request, String pattern, String lookupPath) { + + if (mapping.getPatternParser() != null) { + ServletRequestPathUtils.parseAndCache(request); + } + + mapping.handleMatch(mapping.createInfo(pattern), lookupPath, request); } @SuppressWarnings("unchecked") @@ -494,7 +522,8 @@ public class RequestMappingInfoHandlerMappingTests { private static class TestRequestMappingInfoHandlerMapping extends RequestMappingInfoHandlerMapping { - public void registerHandler(Object handler) { + + void registerHandler(Object handler) { super.detectHandlerMethods(handler); } @@ -507,18 +536,46 @@ public class RequestMappingInfoHandlerMappingTests { protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { RequestMapping annot = AnnotationUtils.findAnnotation(method, RequestMapping.class); if (annot != null) { - return new RequestMappingInfo( - new PatternsRequestCondition(annot.value(), getUrlPathHelper(), getPathMatcher(), true, true), - new RequestMethodsRequestCondition(annot.method()), - new ParamsRequestCondition(annot.params()), - new HeadersRequestCondition(annot.headers()), - new ConsumesRequestCondition(annot.consumes(), annot.headers()), - new ProducesRequestCondition(annot.produces(), annot.headers()), null); + return RequestMappingInfo.paths(annot.value()) + .methods(annot.method()) + .params(annot.params()) + .headers(annot.headers()) + .consumes(annot.consumes()) + .produces(annot.produces()) + .options(getBuilderConfig()) + .build(); } else { return null; } } + + private RequestMappingInfo.BuilderConfiguration getBuilderConfig() { + RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); + if (getPatternParser() != null) { + config.setPatternParser(getPatternParser()); + } + else { + config.setSuffixPatternMatch(true); + config.setRegisteredSuffixPatternMatch(true); + config.setPathMatcher(getPathMatcher()); + } + return config; + } + + RequestMappingInfo createInfo(String... patterns) { + return RequestMappingInfo.paths(patterns).options(getBuilderConfig()).build(); + } + + @Override + protected String initLookupPath(HttpServletRequest request) { + // At runtime this is done by the DispatcherServlet + if (getPatternParser() != null) { + RequestPath requestPath = ServletRequestPathUtils.parseAndCache(request); + return requestPath.pathWithinApplication().value(); + } + return super.initLookupPath(request); + } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoTests.java index 2f83723124..a6c0187c49 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoTests.java @@ -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. @@ -19,6 +19,7 @@ package org.springframework.web.servlet.mvc.method; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.stream.Stream; import javax.servlet.http.HttpServletRequest; @@ -26,13 +27,16 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.handler.PathPatternsParameterizedTest; +import org.springframework.web.servlet.handler.PathPatternsTestUtils; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.pattern.PathPatternParser; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.HEAD; -import static org.springframework.web.servlet.mvc.method.RequestMappingInfo.paths; /** * Test fixture for {@link RequestMappingInfo} tests. @@ -40,15 +44,22 @@ import static org.springframework.web.servlet.mvc.method.RequestMappingInfo.path * @author Arjen Poutsma * @author Rossen Stoyanchev */ -public class RequestMappingInfoTests { +class RequestMappingInfoTests { - @Test - public void createEmpty() { + @SuppressWarnings("unused") + static Stream pathPatternsArguments() { + RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); + config.setPatternParser(new PathPatternParser()); + return Stream.of(RequestMappingInfo.paths().options(config), RequestMappingInfo.paths()); + } - RequestMappingInfo info = paths().build(); + + @PathPatternsParameterizedTest + void createEmpty(RequestMappingInfo.Builder infoBuilder) { // gh-22543 - assertThat(info.getPatternsCondition().getPatterns()).isEqualTo(Collections.singleton("")); + RequestMappingInfo info = infoBuilder.build(); + assertThat(info.getPatternValues()).isEqualTo(Collections.singleton("")); assertThat(info.getMethodsCondition().getMethods().size()).isEqualTo(0); assertThat(info.getParamsCondition()).isNotNull(); assertThat(info.getHeadersCondition()).isNotNull(); @@ -56,7 +67,8 @@ public class RequestMappingInfoTests { assertThat(info.getProducesCondition().isEmpty()).isEqualTo(true); assertThat(info.getCustomCondition()).isNull(); - RequestMappingInfo anotherInfo = paths().build(); + RequestMappingInfo anotherInfo = infoBuilder.build(); + assertThat(info.getActivePatternsCondition()).isSameAs(anotherInfo.getActivePatternsCondition()); assertThat(info.getPatternsCondition()).isSameAs(anotherInfo.getPatternsCondition()); assertThat(info.getMethodsCondition()).isSameAs(anotherInfo.getMethodsCondition()); assertThat(info.getParamsCondition()).isSameAs(anotherInfo.getParamsCondition()); @@ -66,7 +78,7 @@ public class RequestMappingInfoTests { assertThat(info.getCustomCondition()).isSameAs(anotherInfo.getCustomCondition()); RequestMappingInfo result = info.combine(anotherInfo); - assertThat(info.getPatternsCondition()).isSameAs(result.getPatternsCondition()); + assertThat(info.getActivePatternsCondition()).isSameAs(result.getActivePatternsCondition()); assertThat(info.getMethodsCondition()).isSameAs(result.getMethodsCondition()); assertThat(info.getParamsCondition()).isSameAs(result.getParamsCondition()); assertThat(info.getHeadersCondition()).isSameAs(result.getHeadersCondition()); @@ -75,148 +87,157 @@ public class RequestMappingInfoTests { assertThat(info.getCustomCondition()).isSameAs(result.getCustomCondition()); } - @Test - public void matchPatternsCondition() { - HttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + @PathPatternsParameterizedTest + void matchPatternsCondition(RequestMappingInfo.Builder builder) { - RequestMappingInfo info = paths("/foo*", "/bar").build(); - RequestMappingInfo expected = paths("/foo*").build(); + boolean useParsedPatterns = builder.build().getPathPatternsCondition() != null; + HttpServletRequest request = PathPatternsTestUtils.initRequest("GET", "/foo", useParsedPatterns); + + RequestMappingInfo info = builder.paths("/foo*", "/bar").build(); + RequestMappingInfo expected = builder.paths("/foo*").build(); assertThat(info.getMatchingCondition(request)).isEqualTo(expected); - info = paths("/**", "/foo*", "/foo").build(); - expected = paths("/foo", "/foo*", "/**").build(); + info = builder.paths("/**", "/foo*", "/foo").build(); + expected = builder.paths("/foo", "/foo*", "/**").build(); assertThat(info.getMatchingCondition(request)).isEqualTo(expected); } @Test - public void matchParamsCondition() { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + void matchParamsCondition() { + MockHttpServletRequest request = PathPatternsTestUtils.initRequest("GET", "/foo", false); request.setParameter("foo", "bar"); + UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request); - RequestMappingInfo info = paths("/foo").params("foo=bar").build(); + RequestMappingInfo info = RequestMappingInfo.paths("/foo").params("foo=bar").build(); RequestMappingInfo match = info.getMatchingCondition(request); assertThat(match).isNotNull(); - info = paths("/foo").params("foo!=bar").build(); + info = RequestMappingInfo.paths("/foo").params("foo!=bar").build(); match = info.getMatchingCondition(request); assertThat(match).isNull(); } @Test - public void matchHeadersCondition() { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + void matchHeadersCondition() { + MockHttpServletRequest request = PathPatternsTestUtils.initRequest("GET", "/foo", false); request.addHeader("foo", "bar"); + UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request); - RequestMappingInfo info = paths("/foo").headers("foo=bar").build(); + RequestMappingInfo info = RequestMappingInfo.paths("/foo").headers("foo=bar").build(); RequestMappingInfo match = info.getMatchingCondition(request); assertThat(match).isNotNull(); - info = paths("/foo").headers("foo!=bar").build(); + info = RequestMappingInfo.paths("/foo").headers("foo!=bar").build(); match = info.getMatchingCondition(request); assertThat(match).isNull(); } @Test - public void matchConsumesCondition() { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + void matchConsumesCondition() { + MockHttpServletRequest request = PathPatternsTestUtils.initRequest("GET", "/foo", false); request.setContentType("text/plain"); + UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request); - RequestMappingInfo info = paths("/foo").consumes("text/plain").build(); + RequestMappingInfo info = RequestMappingInfo.paths("/foo").consumes("text/plain").build(); RequestMappingInfo match = info.getMatchingCondition(request); assertThat(match).isNotNull(); - info = paths("/foo").consumes("application/xml").build(); + info = RequestMappingInfo.paths("/foo").consumes("application/xml").build(); match = info.getMatchingCondition(request); assertThat(match).isNull(); } @Test - public void matchProducesCondition() { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + void matchProducesCondition() { + MockHttpServletRequest request = PathPatternsTestUtils.initRequest("GET", "/foo", false); request.addHeader("Accept", "text/plain"); + UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request); - RequestMappingInfo info = paths("/foo").produces("text/plain").build(); + RequestMappingInfo info = RequestMappingInfo.paths("/foo").produces("text/plain").build(); RequestMappingInfo match = info.getMatchingCondition(request); assertThat(match).isNotNull(); - info = paths("/foo").produces("application/xml").build(); + info = RequestMappingInfo.paths("/foo").produces("application/xml").build(); match = info.getMatchingCondition(request); assertThat(match).isNull(); } @Test - public void matchCustomCondition() { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + void matchCustomCondition() { + MockHttpServletRequest request = PathPatternsTestUtils.initRequest("GET", "/foo", false); request.setParameter("foo", "bar"); + UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request); - RequestMappingInfo info = paths("/foo").params("foo=bar").build(); + RequestMappingInfo info = RequestMappingInfo.paths("/foo").params("foo=bar").build(); RequestMappingInfo match = info.getMatchingCondition(request); assertThat(match).isNotNull(); - info = paths("/foo").params("foo!=bar").params("foo!=bar").build(); + info = RequestMappingInfo.paths("/foo").params("foo!=bar").params("foo!=bar").build(); match = info.getMatchingCondition(request); assertThat(match).isNull(); } @Test - public void compareToWithImpicitVsExplicitHttpMethodDeclaration() { - RequestMappingInfo noMethods = paths().build(); - RequestMappingInfo oneMethod = paths().methods(GET).build(); - RequestMappingInfo oneMethodOneParam = paths().methods(GET).params("foo").build(); + void compareToWithImpicitVsExplicitHttpMethodDeclaration() { + RequestMappingInfo noMethods = RequestMappingInfo.paths().build(); + RequestMappingInfo oneMethod = RequestMappingInfo.paths().methods(GET).build(); + RequestMappingInfo oneMethodOneParam = RequestMappingInfo.paths().methods(GET).params("foo").build(); - Comparator comparator = - (info, otherInfo) -> info.compareTo(otherInfo, new MockHttpServletRequest()); + MockHttpServletRequest request = PathPatternsTestUtils.initRequest("GET", "/", false); + UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request); + Comparator comparator = (info, otherInfo) -> info.compareTo(otherInfo, request); List list = asList(noMethods, oneMethod, oneMethodOneParam); Collections.shuffle(list); - Collections.sort(list, comparator); + list.sort(comparator); assertThat(list.get(0)).isEqualTo(oneMethodOneParam); assertThat(list.get(1)).isEqualTo(oneMethod); assertThat(list.get(2)).isEqualTo(noMethods); } - @Test // SPR-14383 - public void compareToWithHttpHeadMapping() { - MockHttpServletRequest request = new MockHttpServletRequest(); + @Test + // SPR-14383 + void compareToWithHttpHeadMapping() { + MockHttpServletRequest request = PathPatternsTestUtils.initRequest("GET", "/", false); request.setMethod("HEAD"); request.addHeader("Accept", "application/json"); - RequestMappingInfo noMethods = paths().build(); - RequestMappingInfo getMethod = paths().methods(GET).produces("application/json").build(); - RequestMappingInfo headMethod = paths().methods(HEAD).build(); + RequestMappingInfo noMethods = RequestMappingInfo.paths().build(); + RequestMappingInfo getMethod = RequestMappingInfo.paths().methods(GET).produces("application/json").build(); + RequestMappingInfo headMethod = RequestMappingInfo.paths().methods(HEAD).build(); Comparator comparator = (info, otherInfo) -> info.compareTo(otherInfo, request); List list = asList(noMethods, getMethod, headMethod); Collections.shuffle(list); - Collections.sort(list, comparator); + list.sort(comparator); assertThat(list.get(0)).isEqualTo(headMethod); assertThat(list.get(1)).isEqualTo(getMethod); assertThat(list.get(2)).isEqualTo(noMethods); } - @Test - public void equals() { - RequestMappingInfo info1 = paths("/foo").methods(GET) + @PathPatternsParameterizedTest + void equalsMethod(RequestMappingInfo.Builder infoBuilder) { + RequestMappingInfo info1 = infoBuilder.paths("/foo").methods(GET) .params("foo=bar", "customFoo=customBar").headers("foo=bar") .consumes("text/plain").produces("text/plain") .build(); - RequestMappingInfo info2 = paths("/foo").methods(GET) + RequestMappingInfo info2 = infoBuilder.paths("/foo").methods(GET) .params("foo=bar", "customFoo=customBar").headers("foo=bar") .consumes("text/plain").produces("text/plain") .build(); @@ -224,7 +245,7 @@ public class RequestMappingInfoTests { assertThat(info2).isEqualTo(info1); assertThat(info2.hashCode()).isEqualTo(info1.hashCode()); - info2 = paths("/foo", "/NOOOOOO").methods(GET) + info2 = infoBuilder.paths("/foo", "/NOOOOOO").methods(GET) .params("foo=bar", "customFoo=customBar").headers("foo=bar") .consumes("text/plain").produces("text/plain") .build(); @@ -232,7 +253,7 @@ public class RequestMappingInfoTests { assertThat(info1.equals(info2)).isFalse(); assertThat(info2.hashCode()).isNotEqualTo((long) info1.hashCode()); - info2 = paths("/foo").methods(GET, RequestMethod.POST) + info2 = infoBuilder.paths("/foo").methods(GET, RequestMethod.POST) .params("foo=bar", "customFoo=customBar").headers("foo=bar") .consumes("text/plain").produces("text/plain") .build(); @@ -240,7 +261,7 @@ public class RequestMappingInfoTests { assertThat(info1.equals(info2)).isFalse(); assertThat(info2.hashCode()).isNotEqualTo((long) info1.hashCode()); - info2 = paths("/foo").methods(GET) + info2 = infoBuilder.paths("/foo").methods(GET) .params("/NOOOOOO", "customFoo=customBar").headers("foo=bar") .consumes("text/plain").produces("text/plain") .build(); @@ -248,7 +269,7 @@ public class RequestMappingInfoTests { assertThat(info1.equals(info2)).isFalse(); assertThat(info2.hashCode()).isNotEqualTo((long) info1.hashCode()); - info2 = paths("/foo").methods(GET) + info2 = infoBuilder.paths("/foo").methods(GET) .params("foo=bar", "customFoo=customBar").headers("/NOOOOOO") .consumes("text/plain").produces("text/plain") .build(); @@ -256,7 +277,7 @@ public class RequestMappingInfoTests { assertThat(info1.equals(info2)).isFalse(); assertThat(info2.hashCode()).isNotEqualTo((long) info1.hashCode()); - info2 = paths("/foo").methods(GET) + info2 = infoBuilder.paths("/foo").methods(GET) .params("foo=bar", "customFoo=customBar").headers("foo=bar") .consumes("text/NOOOOOO").produces("text/plain") .build(); @@ -264,7 +285,7 @@ public class RequestMappingInfoTests { assertThat(info1.equals(info2)).isFalse(); assertThat(info2.hashCode()).isNotEqualTo((long) info1.hashCode()); - info2 = paths("/foo").methods(GET) + info2 = infoBuilder.paths("/foo").methods(GET) .params("foo=bar", "customFoo=customBar").headers("foo=bar") .consumes("text/plain").produces("text/NOOOOOO") .build(); @@ -272,7 +293,7 @@ public class RequestMappingInfoTests { assertThat(info1.equals(info2)).isFalse(); assertThat(info2.hashCode()).isNotEqualTo((long) info1.hashCode()); - info2 = paths("/foo").methods(GET) + info2 = infoBuilder.paths("/foo").methods(GET) .params("foo=bar", "customFoo=NOOOOOO").headers("foo=bar") .consumes("text/plain").produces("text/plain") .build(); @@ -282,16 +303,16 @@ public class RequestMappingInfoTests { } @Test - public void preFlightRequest() { - MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/foo"); + void preFlightRequest() { + MockHttpServletRequest request = PathPatternsTestUtils.initRequest("OPTIONS", "/foo", false); request.addHeader(HttpHeaders.ORIGIN, "https://domain.com"); request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); - RequestMappingInfo info = paths("/foo").methods(RequestMethod.POST).build(); + RequestMappingInfo info = RequestMappingInfo.paths("/foo").methods(RequestMethod.POST).build(); RequestMappingInfo match = info.getMatchingCondition(request); assertThat(match).isNotNull(); - info = paths("/foo").methods(RequestMethod.OPTIONS).build(); + info = RequestMappingInfo.paths("/foo").methods(RequestMethod.OPTIONS).build(); match = info.getMatchingCondition(request); assertThat(match).as("Pre-flight should match the ACCESS_CONTROL_REQUEST_METHOD").isNull(); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractServletHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractServletHandlerMethodTests.java index 21b0adb3b2..c9cfaf7ec4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractServletHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractServletHandlerMethodTests.java @@ -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. @@ -20,6 +20,7 @@ import javax.servlet.ServletException; import org.junit.jupiter.api.AfterEach; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContextInitializer; import org.springframework.lang.Nullable; @@ -29,6 +30,7 @@ import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; import org.springframework.web.testfixture.servlet.MockServletConfig; +import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; @@ -44,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat; */ public abstract class AbstractServletHandlerMethodTests { + @Nullable private DispatcherServlet servlet; @@ -57,49 +60,54 @@ public abstract class AbstractServletHandlerMethodTests { this.servlet = null; } + /** * Initialize a DispatcherServlet instance registering zero or more controller classes. */ - protected WebApplicationContext initServletWithControllers(final Class... controllerClasses) - throws ServletException { + protected WebApplicationContext initDispatcherServlet( + Class controllerClass, boolean usePathPatterns) throws ServletException { - return initServlet(null, controllerClasses); + return initDispatcherServlet(controllerClass, usePathPatterns, null); } - /** - * Initialize a DispatcherServlet instance registering zero or more controller classes - * and also providing additional bean definitions through a callback. - */ @SuppressWarnings("serial") - protected WebApplicationContext initServlet( - final ApplicationContextInitializer initializer, - final Class... controllerClasses) throws ServletException { + WebApplicationContext initDispatcherServlet( + @Nullable Class controllerClass, boolean usePathPatterns, + @Nullable ApplicationContextInitializer initializer) + throws ServletException { final GenericWebApplicationContext wac = new GenericWebApplicationContext(); servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(@Nullable WebApplicationContext parent) { - for (Class clazz : controllerClasses) { - wac.registerBeanDefinition(clazz.getSimpleName(), new RootBeanDefinition(clazz)); - } - RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); - mappingDef.getPropertyValues().add("removeSemicolonContent", "false"); - wac.registerBeanDefinition("handlerMapping", mappingDef); - wac.registerBeanDefinition("handlerAdapter", - new RootBeanDefinition(RequestMappingHandlerAdapter.class)); - wac.registerBeanDefinition("requestMappingResolver", - new RootBeanDefinition(ExceptionHandlerExceptionResolver.class)); - wac.registerBeanDefinition("responseStatusResolver", - new RootBeanDefinition(ResponseStatusExceptionResolver.class)); - wac.registerBeanDefinition("defaultResolver", - new RootBeanDefinition(DefaultHandlerExceptionResolver.class)); + if (controllerClass != null) { + wac.registerBeanDefinition( + controllerClass.getSimpleName(), new RootBeanDefinition(controllerClass)); + } if (initializer != null) { initializer.initialize(wac); } + if (!wac.containsBeanDefinition("handlerMapping")) { + BeanDefinition def = register("handlerMapping", RequestMappingHandlerMapping.class, wac); + def.getPropertyValues().add("removeSemicolonContent", "false"); + } + + BeanDefinition mappingDef = wac.getBeanDefinition("handlerMapping"); + if (usePathPatterns && !mappingDef.hasAttribute("patternParser")) { + BeanDefinition parserDef = register("parser", PathPatternParser.class, wac); + mappingDef.getPropertyValues().add("patternParser", parserDef); + } + + register("handlerAdapter", RequestMappingHandlerAdapter.class, wac); + register("requestMappingResolver", ExceptionHandlerExceptionResolver.class, wac); + register("responseStatusResolver", ResponseStatusExceptionResolver.class, wac); + register("defaultResolver", DefaultHandlerExceptionResolver.class, wac); + wac.refresh(); return wac; } @@ -110,4 +118,13 @@ public abstract class AbstractServletHandlerMethodTests { return wac; } + private BeanDefinition register(String beanName, Class beanType, GenericWebApplicationContext wac) { + if (wac.containsBeanDefinition(beanName)) { + return wac.getBeanDefinition(beanName); + } + RootBeanDefinition beanDef = new RootBeanDefinition(beanType); + wac.registerBeanDefinition(beanName, beanDef); + return beanDef; + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index e135d7f76f..9948c24338 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -21,11 +21,13 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; -import java.util.Arrays; +import java.util.Collections; import java.util.Properties; +import java.util.stream.Stream; + +import javax.servlet.http.HttpServletRequest; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.springframework.beans.DirectFieldAccessor; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; @@ -33,6 +35,8 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.PropertiesPropertySource; import org.springframework.http.HttpHeaders; +import org.springframework.http.server.RequestPath; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Controller; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.CrossOrigin; @@ -44,14 +48,11 @@ import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; -import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition; -import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition; -import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; -import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition; -import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; +import org.springframework.web.servlet.handler.PathPatternsParameterizedTest; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -62,21 +63,12 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; * @author Sebastien Deleuze * @author Sam Brannen * @author Nicolas Labrot + * @author Rossen Stoyanchev */ -public class CrossOriginTests { +class CrossOriginTests { - private final TestRequestMappingInfoHandlerMapping handlerMapping = new TestRequestMappingInfoHandlerMapping(); - - private final MockHttpServletRequest request = new MockHttpServletRequest(); - - private final String optionsHandler = "org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping$HttpOptionsHandler#handle()"; - - private final String corsPreflightHandler = "org.springframework.web.servlet.handler.AbstractHandlerMapping$PreFlightHandler"; - - - @BeforeEach - @SuppressWarnings("resource") - public void setup() { + @SuppressWarnings("unused") + static Stream pathPatternsArguments() { StaticWebApplicationContext wac = new StaticWebApplicationContext(); Properties props = new Properties(); props.setProperty("myOrigin", "https://example.com"); @@ -84,234 +76,252 @@ public class CrossOriginTests { wac.registerSingleton("ppc", PropertySourcesPlaceholderConfigurer.class); wac.refresh(); - this.handlerMapping.setRemoveSemicolonContent(false); - wac.getAutowireCapableBeanFactory().initializeBean(this.handlerMapping, "hm"); + TestRequestMappingInfoHandlerMapping mapping1 = new TestRequestMappingInfoHandlerMapping(); + mapping1.setPatternParser(new PathPatternParser()); + wac.getAutowireCapableBeanFactory().initializeBean(mapping1, "mapping1"); + TestRequestMappingInfoHandlerMapping mapping2 = new TestRequestMappingInfoHandlerMapping(); + wac.getAutowireCapableBeanFactory().initializeBean(mapping2, "mapping2"); + + return Stream.of(mapping1, mapping2); + } + + + private final MockHttpServletRequest request = new MockHttpServletRequest(); + + + @BeforeEach + void setup() { this.request.setMethod("GET"); this.request.addHeader(HttpHeaders.ORIGIN, "https://domain.com/"); } - @Test - public void noAnnotationWithoutOrigin() throws Exception { - this.handlerMapping.registerHandler(new MethodLevelController()); + @PathPatternsParameterizedTest + void noAnnotationWithoutOrigin(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new MethodLevelController()); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/no"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); assertThat(getCorsConfiguration(chain, false)).isNull(); } - @Test - public void noAnnotationWithAccessControlRequestMethod() throws Exception { - this.handlerMapping.registerHandler(new MethodLevelController()); + @PathPatternsParameterizedTest + void noAnnotationWithAccessControlRequestMethod(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new MethodLevelController()); MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/no"); request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); - assertThat(chain.getHandler().toString()).isEqualTo(optionsHandler); + HandlerExecutionChain chain = mapping.getHandler(request); + assertThat(chain).isNotNull(); + assertThat(chain.getHandler().toString()) + .endsWith("RequestMappingInfoHandlerMapping$HttpOptionsHandler#handle()"); } - @Test - public void noAnnotationWithPreflightRequest() throws Exception { - this.handlerMapping.registerHandler(new MethodLevelController()); + @PathPatternsParameterizedTest + void noAnnotationWithPreflightRequest(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new MethodLevelController()); MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/no"); request.addHeader(HttpHeaders.ORIGIN, "https://domain.com/"); request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); - assertThat(chain.getHandler().getClass().getName()).isEqualTo(corsPreflightHandler); + HandlerExecutionChain chain = mapping.getHandler(request); + assertThat(chain).isNotNull(); + assertThat(chain.getHandler().getClass().getName()).endsWith("AbstractHandlerMapping$PreFlightHandler"); } - @Test // SPR-12931 - public void noAnnotationWithOrigin() throws Exception { - this.handlerMapping.registerHandler(new MethodLevelController()); + @PathPatternsParameterizedTest // SPR-12931 + void noAnnotationWithOrigin(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new MethodLevelController()); this.request.setRequestURI("/no"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); assertThat(getCorsConfiguration(chain, false)).isNull(); } - @Test // SPR-12931 - public void noAnnotationPostWithOrigin() throws Exception { - this.handlerMapping.registerHandler(new MethodLevelController()); + @PathPatternsParameterizedTest // SPR-12931 + void noAnnotationPostWithOrigin(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new MethodLevelController()); this.request.setMethod("POST"); this.request.setRequestURI("/no"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); assertThat(getCorsConfiguration(chain, false)).isNull(); } - @Test - public void defaultAnnotation() throws Exception { - this.handlerMapping.registerHandler(new MethodLevelController()); + @PathPatternsParameterizedTest + void defaultAnnotation(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new MethodLevelController()); this.request.setRequestURI("/default"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[] {"GET"}); - assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[] {"*"}); + assertThat(config.getAllowedMethods()).containsExactly("GET"); + assertThat(config.getAllowedOrigins()).containsExactly("*"); assertThat(config.getAllowCredentials()).isNull(); - assertThat(config.getAllowedHeaders().toArray()).isEqualTo(new String[] {"*"}); + assertThat(config.getAllowedHeaders()).containsExactly("*"); assertThat(CollectionUtils.isEmpty(config.getExposedHeaders())).isTrue(); assertThat(config.getMaxAge()).isEqualTo(new Long(1800)); } - @Test - public void customized() throws Exception { - this.handlerMapping.registerHandler(new MethodLevelController()); + @PathPatternsParameterizedTest + void customized(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new MethodLevelController()); this.request.setRequestURI("/customized"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[] {"DELETE"}); - assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[] {"https://site1.com", "https://site2.com"}); - assertThat(config.getAllowedHeaders().toArray()).isEqualTo(new String[] {"header1", "header2"}); - assertThat(config.getExposedHeaders().toArray()).isEqualTo(new String[] {"header3", "header4"}); + assertThat(config.getAllowedMethods()).containsExactly("DELETE"); + assertThat(config.getAllowedOrigins()).containsExactly("https://site1.com", "https://site2.com"); + assertThat(config.getAllowedHeaders()).containsExactly("header1", "header2"); + assertThat(config.getExposedHeaders()).containsExactly("header3", "header4"); assertThat(config.getMaxAge()).isEqualTo(new Long(123)); - assertThat((boolean) config.getAllowCredentials()).isFalse(); + assertThat(config.getAllowCredentials()).isFalse(); } - @Test - public void customOriginDefinedViaValueAttribute() throws Exception { - this.handlerMapping.registerHandler(new MethodLevelController()); + @PathPatternsParameterizedTest + void customOriginDefinedViaValueAttribute(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new MethodLevelController()); this.request.setRequestURI("/customOrigin"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); - assertThat(config.getAllowedOrigins()).isEqualTo(Arrays.asList("https://example.com")); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("https://example.com")); assertThat(config.getAllowCredentials()).isNull(); } - @Test - public void customOriginDefinedViaPlaceholder() throws Exception { - this.handlerMapping.registerHandler(new MethodLevelController()); + @PathPatternsParameterizedTest + void customOriginDefinedViaPlaceholder(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new MethodLevelController()); this.request.setRequestURI("/someOrigin"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); - assertThat(config.getAllowedOrigins()).isEqualTo(Arrays.asList("https://example.com")); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("https://example.com")); assertThat(config.getAllowCredentials()).isNull(); } - @Test - public void bogusAllowCredentialsValue() throws Exception { + @PathPatternsParameterizedTest + void bogusAllowCredentialsValue(TestRequestMappingInfoHandlerMapping mapping) { assertThatIllegalStateException().isThrownBy(() -> - this.handlerMapping.registerHandler(new MethodLevelControllerWithBogusAllowCredentialsValue())) + mapping.registerHandler(new MethodLevelControllerWithBogusAllowCredentialsValue())) .withMessageContaining("@CrossOrigin's allowCredentials") .withMessageContaining("current value is [bogus]"); } - @Test - public void classLevel() throws Exception { - this.handlerMapping.registerHandler(new ClassLevelController()); + @PathPatternsParameterizedTest + void classLevel(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new ClassLevelController()); this.request.setRequestURI("/foo"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[] {"GET"}); - assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[] {"*"}); - assertThat((boolean) config.getAllowCredentials()).isFalse(); + assertThat(config.getAllowedMethods()).containsExactly("GET"); + assertThat(config.getAllowedOrigins()).containsExactly("*"); + assertThat(config.getAllowCredentials()).isFalse(); this.request.setRequestURI("/bar"); - chain = this.handlerMapping.getHandler(request); + chain = mapping.getHandler(request); config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[] {"GET"}); - assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[] {"*"}); - assertThat((boolean) config.getAllowCredentials()).isFalse(); + assertThat(config.getAllowedMethods()).containsExactly("GET"); + assertThat(config.getAllowedOrigins()).containsExactly("*"); + assertThat(config.getAllowCredentials()).isFalse(); this.request.setRequestURI("/baz"); - chain = this.handlerMapping.getHandler(request); + chain = mapping.getHandler(request); config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[] {"GET"}); - assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[] {"*"}); - assertThat((boolean) config.getAllowCredentials()).isTrue(); + assertThat(config.getAllowedMethods()).containsExactly("GET"); + assertThat(config.getAllowedOrigins()).containsExactly("*"); + assertThat(config.getAllowCredentials()).isTrue(); } - @Test // SPR-13468 - public void classLevelComposedAnnotation() throws Exception { - this.handlerMapping.registerHandler(new ClassLevelMappingWithComposedAnnotation()); + @PathPatternsParameterizedTest // SPR-13468 + void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new ClassLevelMappingWithComposedAnnotation()); this.request.setRequestURI("/foo"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[] {"GET"}); - assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[] {"http://www.foo.example/"}); - assertThat((boolean) config.getAllowCredentials()).isTrue(); + assertThat(config.getAllowedMethods()).containsExactly("GET"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowCredentials()).isTrue(); } - @Test // SPR-13468 - public void methodLevelComposedAnnotation() throws Exception { - this.handlerMapping.registerHandler(new MethodLevelMappingWithComposedAnnotation()); + @PathPatternsParameterizedTest // SPR-13468 + void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new MethodLevelMappingWithComposedAnnotation()); this.request.setRequestURI("/foo"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[] {"GET"}); - assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[] {"http://www.foo.example/"}); - assertThat((boolean) config.getAllowCredentials()).isTrue(); + assertThat(config.getAllowedMethods()).containsExactly("GET"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowCredentials()).isTrue(); } - @Test - public void preFlightRequest() throws Exception { - this.handlerMapping.registerHandler(new MethodLevelController()); + @PathPatternsParameterizedTest + void preFlightRequest(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new MethodLevelController()); this.request.setMethod("OPTIONS"); this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); this.request.setRequestURI("/default"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); CorsConfiguration config = getCorsConfiguration(chain, true); assertThat(config).isNotNull(); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[] {"GET"}); - assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[] {"*"}); + assertThat(config.getAllowedMethods()).containsExactly("GET"); + assertThat(config.getAllowedOrigins()).containsExactly("*"); assertThat(config.getAllowCredentials()).isNull(); - assertThat(config.getAllowedHeaders().toArray()).isEqualTo(new String[] {"*"}); + assertThat(config.getAllowedHeaders()).containsExactly("*"); assertThat(CollectionUtils.isEmpty(config.getExposedHeaders())).isTrue(); assertThat(config.getMaxAge()).isEqualTo(new Long(1800)); } - @Test - public void ambiguousHeaderPreFlightRequest() throws Exception { - this.handlerMapping.registerHandler(new MethodLevelController()); + @PathPatternsParameterizedTest + void ambiguousHeaderPreFlightRequest(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new MethodLevelController()); this.request.setMethod("OPTIONS"); this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "header1"); this.request.setRequestURI("/ambiguous-header"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); CorsConfiguration config = getCorsConfiguration(chain, true); assertThat(config).isNotNull(); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[] {"*"}); - assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[] {"*"}); - assertThat(config.getAllowedHeaders().toArray()).isEqualTo(new String[] {"*"}); - assertThat((boolean) config.getAllowCredentials()).isTrue(); + assertThat(config.getAllowedMethods()).containsExactly("*"); + assertThat(config.getAllowedOrigins()).containsExactly("*"); + assertThat(config.getAllowedHeaders()).containsExactly("*"); + assertThat(config.getAllowCredentials()).isTrue(); assertThat(CollectionUtils.isEmpty(config.getExposedHeaders())).isTrue(); assertThat(config.getMaxAge()).isNull(); } - @Test - public void ambiguousProducesPreFlightRequest() throws Exception { - this.handlerMapping.registerHandler(new MethodLevelController()); + @PathPatternsParameterizedTest + void ambiguousProducesPreFlightRequest(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + mapping.registerHandler(new MethodLevelController()); this.request.setMethod("OPTIONS"); this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); this.request.setRequestURI("/ambiguous-produces"); - HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + HandlerExecutionChain chain = mapping.getHandler(request); CorsConfiguration config = getCorsConfiguration(chain, true); assertThat(config).isNotNull(); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[] {"*"}); - assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[] {"*"}); - assertThat(config.getAllowedHeaders().toArray()).isEqualTo(new String[] {"*"}); - assertThat((boolean) config.getAllowCredentials()).isTrue(); + assertThat(config.getAllowedMethods()).containsExactly("*"); + assertThat(config.getAllowedOrigins()).containsExactly("*"); + assertThat(config.getAllowedHeaders()).containsExactly("*"); + assertThat(config.getAllowCredentials()).isTrue(); assertThat(CollectionUtils.isEmpty(config.getExposedHeaders())).isTrue(); assertThat(config.getMaxAge()).isNull(); } - @Test - public void preFlightRequestWithoutRequestMethodHeader() throws Exception { + @PathPatternsParameterizedTest + void preFlightRequestWithoutRequestMethodHeader(TestRequestMappingInfoHandlerMapping mapping) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/default"); request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); - assertThat(this.handlerMapping.getHandler(request)).isNull(); + assertThat(mapping.getHandler(request)).isNull(); } - private CorsConfiguration getCorsConfiguration(HandlerExecutionChain chain, boolean isPreFlightRequest) { + @Nullable + private CorsConfiguration getCorsConfiguration(@Nullable HandlerExecutionChain chain, boolean isPreFlightRequest) { + assertThat(chain).isNotNull(); if (isPreFlightRequest) { Object handler = chain.getHandler(); assertThat(handler.getClass().getSimpleName().equals("PreFlightHandler")).isTrue(); @@ -334,6 +344,7 @@ public class CrossOriginTests { @Controller + @SuppressWarnings("unused") private static class MethodLevelController { @GetMapping("/no") @@ -399,6 +410,7 @@ public class CrossOriginTests { @Controller + @SuppressWarnings("unused") private static class MethodLevelControllerWithBogusAllowCredentialsValue { @CrossOrigin(allowCredentials = "bogus") @@ -462,7 +474,7 @@ public class CrossOriginTests { private static class TestRequestMappingInfoHandlerMapping extends RequestMappingHandlerMapping { - public void registerHandler(Object handler) { + void registerHandler(Object handler) { super.detectHandlerMethods(handler); } @@ -475,18 +487,33 @@ public class CrossOriginTests { protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { RequestMapping annotation = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class); if (annotation != null) { - return new RequestMappingInfo( - new PatternsRequestCondition(annotation.value(), getUrlPathHelper(), getPathMatcher(), true, true), - new RequestMethodsRequestCondition(annotation.method()), - new ParamsRequestCondition(annotation.params()), - new HeadersRequestCondition(annotation.headers()), - new ConsumesRequestCondition(annotation.consumes(), annotation.headers()), - new ProducesRequestCondition(annotation.produces(), annotation.headers()), null); + RequestMappingInfo.BuilderConfiguration options = new RequestMappingInfo.BuilderConfiguration(); + if (getPatternParser() != null) { + options.setPatternParser(getPatternParser()); + } + return RequestMappingInfo.paths(annotation.value()) + .methods(annotation.method()) + .params(annotation.params()) + .headers(annotation.headers()) + .consumes(annotation.consumes()) + .produces(annotation.produces()) + .options(options) + .build(); } else { return null; } } + + @Override + protected String initLookupPath(HttpServletRequest request) { + // At runtime this is done by the DispatcherServlet + if (getPatternParser() != null) { + RequestPath requestPath = ServletRequestPathUtils.parseAndCache(request); + return requestPath.pathWithinApplication().value(); + } + return super.initLookupPath(request); + } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java index 356828e0b0..2238b54469 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java @@ -26,8 +26,10 @@ import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.Arguments; import org.springframework.core.annotation.AliasFor; import org.springframework.http.MediaType; @@ -46,9 +48,12 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.HandlerTypePredicate; +import org.springframework.web.servlet.handler.PathPatternsParameterizedTest; import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -61,41 +66,49 @@ import static org.mockito.Mockito.mock; */ public class RequestMappingHandlerMappingTests { - private final StaticWebApplicationContext wac = new StaticWebApplicationContext(); + @SuppressWarnings("unused") + static Stream pathPatternsArguments() { + RequestMappingHandlerMapping mapping1 = new RequestMappingHandlerMapping(); + StaticWebApplicationContext wac1 = new StaticWebApplicationContext(); + mapping1.setPatternParser(new PathPatternParser()); + mapping1.setApplicationContext(wac1); - private final RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping(); - { - this.handlerMapping.setApplicationContext(wac); + RequestMappingHandlerMapping mapping2 = new RequestMappingHandlerMapping(); + StaticWebApplicationContext wac2 = new StaticWebApplicationContext(); + mapping2.setApplicationContext(wac2); + + return Stream.of(Arguments.of(mapping1, wac1), Arguments.of(mapping2, wac2)); } @Test - public void useRegisteredSuffixPatternMatch() { - assertThat(this.handlerMapping.useSuffixPatternMatch()).isFalse(); - assertThat(this.handlerMapping.useRegisteredSuffixPatternMatch()).isFalse(); + void useRegisteredSuffixPatternMatch() { + + RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping(); + handlerMapping.setApplicationContext(new StaticWebApplicationContext()); Map fileExtensions = Collections.singletonMap("json", MediaType.APPLICATION_JSON); PathExtensionContentNegotiationStrategy strategy = new PathExtensionContentNegotiationStrategy(fileExtensions); ContentNegotiationManager manager = new ContentNegotiationManager(strategy); - this.handlerMapping.setContentNegotiationManager(manager); - this.handlerMapping.setUseRegisteredSuffixPatternMatch(true); - this.handlerMapping.afterPropertiesSet(); + handlerMapping.setContentNegotiationManager(manager); + handlerMapping.setUseRegisteredSuffixPatternMatch(true); + handlerMapping.afterPropertiesSet(); - assertThat(this.handlerMapping.useSuffixPatternMatch()).isTrue(); - assertThat(this.handlerMapping.useRegisteredSuffixPatternMatch()).isTrue(); - assertThat(this.handlerMapping.getFileExtensions()).isEqualTo(Collections.singletonList("json")); + assertThat(handlerMapping.useSuffixPatternMatch()).isTrue(); + assertThat(handlerMapping.useRegisteredSuffixPatternMatch()).isTrue(); + assertThat(handlerMapping.getFileExtensions()).isEqualTo(Collections.singletonList("json")); } @Test - public void useRegisteredSuffixPatternMatchInitialization() { + void useRegisteredSuffixPatternMatchInitialization() { Map fileExtensions = Collections.singletonMap("json", MediaType.APPLICATION_JSON); PathExtensionContentNegotiationStrategy strategy = new PathExtensionContentNegotiationStrategy(fileExtensions); ContentNegotiationManager manager = new ContentNegotiationManager(strategy); final Set extensions = new HashSet<>(); - RequestMappingHandlerMapping hm = new RequestMappingHandlerMapping() { + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping() { @Override protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { extensions.addAll(getFileExtensions()); @@ -103,76 +116,99 @@ public class RequestMappingHandlerMappingTests { } }; + StaticWebApplicationContext wac = new StaticWebApplicationContext(); wac.registerSingleton("testController", ComposedAnnotationController.class); wac.refresh(); - hm.setContentNegotiationManager(manager); - hm.setUseRegisteredSuffixPatternMatch(true); - hm.setApplicationContext(wac); - hm.afterPropertiesSet(); + mapping.setContentNegotiationManager(manager); + mapping.setUseRegisteredSuffixPatternMatch(true); + mapping.setApplicationContext(wac); + mapping.afterPropertiesSet(); assertThat(extensions).isEqualTo(Collections.singleton("json")); } @Test - public void useSuffixPatternMatch() { - assertThat(this.handlerMapping.useSuffixPatternMatch()).isFalse(); + void suffixPatternMatchSettings() { + RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping(); - this.handlerMapping.setUseRegisteredSuffixPatternMatch(false); - assertThat(this.handlerMapping.useSuffixPatternMatch()) - .as("'false' registeredSuffixPatternMatch shouldn't impact suffixPatternMatch").isFalse(); + assertThat(handlerMapping.useSuffixPatternMatch()).isFalse(); + assertThat(handlerMapping.useRegisteredSuffixPatternMatch()).isFalse(); - this.handlerMapping.setUseRegisteredSuffixPatternMatch(true); - assertThat(this.handlerMapping.useSuffixPatternMatch()) - .as("'true' registeredSuffixPatternMatch should enable suffixPatternMatch").isTrue(); + handlerMapping.setUseRegisteredSuffixPatternMatch(false); + assertThat(handlerMapping.useSuffixPatternMatch()) + .as("'false' registeredSuffixPatternMatch shouldn't impact suffixPatternMatch") + .isFalse(); + + handlerMapping.setUseRegisteredSuffixPatternMatch(true); + assertThat(handlerMapping.useSuffixPatternMatch()) + .as("'true' registeredSuffixPatternMatch should enable suffixPatternMatch") + .isTrue(); } - @Test - public void resolveEmbeddedValuesInPatterns() { - this.handlerMapping.setEmbeddedValueResolver( + @PathPatternsParameterizedTest + void resolveEmbeddedValuesInPatterns(RequestMappingHandlerMapping mapping) { + + mapping.setEmbeddedValueResolver( value -> "/${pattern}/bar".equals(value) ? "/foo/bar" : value ); String[] patterns = new String[] { "/foo", "/${pattern}/bar" }; - String[] result = this.handlerMapping.resolveEmbeddedValuesInPatterns(patterns); + String[] result = mapping.resolveEmbeddedValuesInPatterns(patterns); assertThat(result).isEqualTo(new String[] { "/foo", "/foo/bar" }); } - @Test - public void pathPrefix() throws NoSuchMethodException { - this.handlerMapping.setEmbeddedValueResolver(value -> "/${prefix}".equals(value) ? "/api" : value); - this.handlerMapping.setPathPrefixes(Collections.singletonMap( + @PathPatternsParameterizedTest + void pathPrefix(RequestMappingHandlerMapping mapping) throws Exception { + + mapping.setEmbeddedValueResolver(value -> "/${prefix}".equals(value) ? "/api" : value); + mapping.setPathPrefixes(Collections.singletonMap( "/${prefix}", HandlerTypePredicate.forAnnotation(RestController.class))); + mapping.afterPropertiesSet(); Method method = UserController.class.getMethod("getUser"); - RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, UserController.class); + RequestMappingInfo info = mapping.getMappingForMethod(method, UserController.class); assertThat(info).isNotNull(); - assertThat(info.getPatternsCondition().getPatterns()).isEqualTo(Collections.singleton("/api/user/{id}")); + assertThat(info.getPatternValues()).isEqualTo(Collections.singleton("/api/user/{id}")); } - @Test // gh-23907 - public void pathPrefixPreservesPathMatchingSettings() throws NoSuchMethodException { - this.handlerMapping.setUseSuffixPatternMatch(false); - this.handlerMapping.setPathPrefixes(Collections.singletonMap("/api", HandlerTypePredicate.forAnyHandlerType())); - this.handlerMapping.afterPropertiesSet(); + @PathPatternsParameterizedTest // gh-23907 + void pathPrefixPreservesPathMatchingSettings(RequestMappingHandlerMapping mapping) throws Exception { + mapping.setPathPrefixes(Collections.singletonMap("/api", HandlerTypePredicate.forAnyHandlerType())); + mapping.afterPropertiesSet(); Method method = ComposedAnnotationController.class.getMethod("get"); - RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, ComposedAnnotationController.class); + RequestMappingInfo info = mapping.getMappingForMethod(method, ComposedAnnotationController.class); assertThat(info).isNotNull(); + assertThat(info.getActivePatternsCondition()).isNotNull(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/get"); - assertThat(info.getPatternsCondition().getMatchingCondition(request)).isNotNull(); + initRequestPath(mapping, request); + assertThat(info.getActivePatternsCondition().getMatchingCondition(request)).isNotNull(); request = new MockHttpServletRequest("GET", "/api/get.pdf"); - assertThat(info.getPatternsCondition().getMatchingCondition(request)).isNull(); + initRequestPath(mapping, request); + assertThat(info.getActivePatternsCondition().getMatchingCondition(request)).isNull(); } - @Test - public void resolveRequestMappingViaComposedAnnotation() throws Exception { - RequestMappingInfo info = assertComposedAnnotationMapping("postJson", "/postJson", RequestMethod.POST); + private void initRequestPath(RequestMappingHandlerMapping mapping, MockHttpServletRequest request) { + PathPatternParser parser = mapping.getPatternParser(); + if (parser != null) { + ServletRequestPathUtils.parseAndCache(request); + } + else { + mapping.getUrlPathHelper().resolveAndCacheLookupPath(request); + } + } + + @PathPatternsParameterizedTest + void resolveRequestMappingViaComposedAnnotation(RequestMappingHandlerMapping mapping) { + + RequestMappingInfo info = assertComposedAnnotationMapping( + mapping, "postJson", "/postJson", RequestMethod.POST); assertThat(info.getConsumesCondition().getConsumableMediaTypes().iterator().next().toString()) .isEqualTo(MediaType.APPLICATION_JSON_VALUE); @@ -181,68 +217,72 @@ public class RequestMappingHandlerMappingTests { } @Test // SPR-14988 - public void getMappingOverridesConsumesFromTypeLevelAnnotation() throws Exception { + void getMappingOverridesConsumesFromTypeLevelAnnotation() throws Exception { RequestMappingInfo requestMappingInfo = assertComposedAnnotationMapping(RequestMethod.POST); ConsumesRequestCondition condition = requestMappingInfo.getConsumesCondition(); assertThat(condition.getConsumableMediaTypes()).isEqualTo(Collections.singleton(MediaType.APPLICATION_XML)); } - @Test // gh-22010 - public void consumesWithOptionalRequestBody() { - this.wac.registerSingleton("testController", ComposedAnnotationController.class); - this.wac.refresh(); - this.handlerMapping.afterPropertiesSet(); - RequestMappingInfo info = this.handlerMapping.getHandlerMethods().keySet().stream() - .filter(i -> i.getPatternsCondition().getPatterns().equals(Collections.singleton("/post"))) + @PathPatternsParameterizedTest // gh-22010 + void consumesWithOptionalRequestBody(RequestMappingHandlerMapping mapping, StaticWebApplicationContext wac) { + wac.registerSingleton("testController", ComposedAnnotationController.class); + wac.refresh(); + mapping.afterPropertiesSet(); + RequestMappingInfo result = mapping.getHandlerMethods().keySet().stream() + .filter(info -> info.getPatternValues().equals(Collections.singleton("/post"))) .findFirst() .orElseThrow(() -> new AssertionError("No /post")); - assertThat(info.getConsumesCondition().isBodyRequired()).isFalse(); + assertThat(result.getConsumesCondition().isBodyRequired()).isFalse(); } @Test - public void getMapping() throws Exception { + void getMapping() throws Exception { assertComposedAnnotationMapping(RequestMethod.GET); } @Test - public void postMapping() throws Exception { + void postMapping() throws Exception { assertComposedAnnotationMapping(RequestMethod.POST); } @Test - public void putMapping() throws Exception { + void putMapping() throws Exception { assertComposedAnnotationMapping(RequestMethod.PUT); } @Test - public void deleteMapping() throws Exception { + void deleteMapping() throws Exception { assertComposedAnnotationMapping(RequestMethod.DELETE); } @Test - public void patchMapping() throws Exception { + void patchMapping() throws Exception { assertComposedAnnotationMapping(RequestMethod.PATCH); } private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) throws Exception { + + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setApplicationContext(new StaticWebApplicationContext()); + String methodName = requestMethod.name().toLowerCase(); String path = "/" + methodName; - return assertComposedAnnotationMapping(methodName, path, requestMethod); + return assertComposedAnnotationMapping(mapping, methodName, path, requestMethod); } - private RequestMappingInfo assertComposedAnnotationMapping(String methodName, String path, - RequestMethod requestMethod) throws Exception { + private RequestMappingInfo assertComposedAnnotationMapping( + RequestMappingHandlerMapping mapping, String methodName, String path, RequestMethod requestMethod) { Class clazz = ComposedAnnotationController.class; Method method = ClassUtils.getMethod(clazz, methodName, (Class[]) null); - RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, clazz); + RequestMappingInfo info = mapping.getMappingForMethod(method, clazz); assertThat(info).isNotNull(); - Set paths = info.getPatternsCondition().getPatterns(); + Set paths = info.getPatternValues(); assertThat(paths.size()).isEqualTo(1); assertThat(paths.iterator().next()).isEqualTo(path); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index cdbdc47037..7d28de7254 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -48,6 +48,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.stream.Stream; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; @@ -59,8 +60,6 @@ import javax.validation.Valid; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlRootElement; -import org.junit.jupiter.api.Test; - import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.aop.interceptor.SimpleTraceInterceptor; import org.springframework.aop.support.DefaultPointcutAdvisor; @@ -140,6 +139,7 @@ import org.springframework.web.multipart.support.StringMultipartFileEditor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; +import org.springframework.web.servlet.handler.PathPatternsParameterizedTest; import org.springframework.web.servlet.mvc.annotation.ModelAndViewResolver; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.springframework.web.servlet.support.RequestContext; @@ -163,9 +163,15 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; */ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServletHandlerMethodTests { - @Test - public void emptyValueMapping() throws Exception { - initServletWithControllers(ControllerWithEmptyValueMapping.class); + @SuppressWarnings("unused") + static Stream pathPatternsArguments() { + return Stream.of(true, false); + } + + + @PathPatternsParameterizedTest + void emptyValueMapping(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ControllerWithEmptyValueMapping.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setContextPath("/foo"); @@ -175,9 +181,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("test"); } - @Test - public void errorThrownFromHandlerMethod() throws Exception { - initServletWithControllers(ControllerWithErrorThrown.class); + @PathPatternsParameterizedTest + void errorThrownFromHandlerMethod(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ControllerWithErrorThrown.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setContextPath("/foo"); @@ -187,9 +193,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("test"); } - @Test - public void customAnnotationController() throws Exception { - initServletWithControllers(CustomAnnotationController.class); + @PathPatternsParameterizedTest + void customAnnotationController(boolean usePathPatterns) throws Exception { + initDispatcherServlet(CustomAnnotationController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -197,9 +203,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getStatus()).as("Invalid response status code").isEqualTo(HttpServletResponse.SC_OK); } - @Test - public void requiredParamMissing() throws Exception { - WebApplicationContext webAppContext = initServletWithControllers(RequiredParamController.class); + @PathPatternsParameterizedTest + void requiredParamMissing(boolean usePathPatterns) throws Exception { + WebApplicationContext webAppContext = initDispatcherServlet(RequiredParamController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -208,9 +214,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(webAppContext.isSingleton(RequiredParamController.class.getSimpleName())).isTrue(); } - @Test - public void typeConversionError() throws Exception { - initServletWithControllers(RequiredParamController.class); + @PathPatternsParameterizedTest + void typeConversionError(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RequiredParamController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); request.addParameter("id", "foo"); @@ -219,9 +225,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getStatus()).as("Invalid response status code").isEqualTo(HttpServletResponse.SC_BAD_REQUEST); } - @Test - public void optionalParamPresent() throws Exception { - initServletWithControllers(OptionalParamController.class); + @PathPatternsParameterizedTest + void optionalParamPresent(boolean usePathPatterns) throws Exception { + initDispatcherServlet(OptionalParamController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); request.addParameter("id", "val"); @@ -232,9 +238,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("val-true-otherVal"); } - @Test - public void optionalParamMissing() throws Exception { - initServletWithControllers(OptionalParamController.class); + @PathPatternsParameterizedTest + void optionalParamMissing(boolean usePathPatterns) throws Exception { + initDispatcherServlet(OptionalParamController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -242,9 +248,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("null-false-null"); } - @Test - public void defaultParameters() throws Exception { - initServletWithControllers(DefaultValueParamController.class); + @PathPatternsParameterizedTest + void defaultParameters(boolean usePathPatterns) throws Exception { + initDispatcherServlet(DefaultValueParamController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -252,13 +258,13 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("foo--bar"); } - @Test - public void defaultExpressionParameters() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void defaultExpressionParameters(boolean usePathPatterns) throws Exception { + initDispatcherServlet(DefaultExpressionValueParamController.class, usePathPatterns, wac -> { RootBeanDefinition ppc = new RootBeanDefinition(PropertySourcesPlaceholderConfigurer.class); ppc.getPropertyValues().add("properties", "myKey=foo"); wac.registerBeanDefinition("ppc", ppc); - }, DefaultExpressionValueParamController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myApp/myPath.do"); request.setContextPath("/myApp"); @@ -273,9 +279,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("foo-bar-/myApp"); } - @Test - public void typeNestedSetBinding() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void typeNestedSetBinding(boolean usePathPatterns) throws Exception { + initDispatcherServlet(NestedSetController.class, usePathPatterns, wac -> { RootBeanDefinition csDef = new RootBeanDefinition(FormattingConversionServiceFactoryBean.class); csDef.getPropertyValues().add("converters", new TestBeanConverter()); RootBeanDefinition wbiDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class); @@ -283,7 +289,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("webBindingInitializer", wbiDef); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, NestedSetController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); request.addParameter("testBeanSet", "1", "2"); @@ -292,9 +298,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("[1, 2]-org.springframework.beans.testfixture.beans.TestBean"); } - @Test // SPR-12903 - public void pathVariableWithCustomConverter() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest // SPR-12903 + void pathVariableWithCustomConverter(boolean usePathPatterns) throws Exception { + initDispatcherServlet(PathVariableWithCustomConverterController.class, usePathPatterns, wac -> { RootBeanDefinition csDef = new RootBeanDefinition(FormattingConversionServiceFactoryBean.class); csDef.getPropertyValues().add("converters", new AnnotatedExceptionRaisingConverter()); RootBeanDefinition wbiDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class); @@ -302,7 +308,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("webBindingInitializer", wbiDef); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, PathVariableWithCustomConverterController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath/1"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -310,9 +316,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getStatus()).isEqualTo(404); } - @Test - public void methodNotAllowed() throws Exception { - initServletWithControllers(MethodNotAllowedController.class); + @PathPatternsParameterizedTest + void methodNotAllowed(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MethodNotAllowedController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -330,13 +336,13 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(allowedMethods.contains("POST")).as("POST not allowed").isTrue(); } - @Test - public void emptyParameterListHandleMethod() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void emptyParameterListHandleMethod(boolean usePathPatterns) throws Exception { + initDispatcherServlet(EmptyParameterListHandlerMethodController.class, usePathPatterns, wac -> { RootBeanDefinition vrDef = new RootBeanDefinition(InternalResourceViewResolver.class); vrDef.getPropertyValues().add("suffix", ".jsp"); wac.registerBeanDefinition("viewResolver", vrDef); - }, EmptyParameterListHandlerMethodController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/emptyParameterListHandler"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -348,17 +354,19 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl } @SuppressWarnings("rawtypes") - @Test - public void sessionAttributeExposure() throws Exception { - initServlet( - wac -> wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(ModelExposingViewResolver.class)), - MySessionAttributesController.class); + @PathPatternsParameterizedTest + void sessionAttributeExposure(boolean usePathPatterns) throws Exception { + initDispatcherServlet( + MySessionAttributesController.class, usePathPatterns, + wac -> wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(ModelExposingViewResolver.class)) + ); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPage"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); assertThat(request.getAttribute("viewName")).isEqualTo("page1"); HttpSession session = request.getSession(); + assertThat(session).isNotNull(); assertThat(session.getAttribute("object1") != null).isTrue(); assertThat(session.getAttribute("object2") != null).isTrue(); assertThat(((Map) session.getAttribute("model")).containsKey("object1")).isTrue(); @@ -376,21 +384,22 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl } @SuppressWarnings("rawtypes") - @Test - public void sessionAttributeExposureWithInterface() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void sessionAttributeExposureWithInterface(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MySessionAttributesControllerImpl.class, usePathPatterns, wac -> { wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(ModelExposingViewResolver.class)); DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); autoProxyCreator.setBeanFactory(wac.getBeanFactory()); wac.getBeanFactory().addBeanPostProcessor(autoProxyCreator); wac.getBeanFactory().registerSingleton("advisor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); - }, MySessionAttributesControllerImpl.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPage"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); assertThat(request.getAttribute("viewName")).isEqualTo("page1"); HttpSession session = request.getSession(); + assertThat(session).isNotNull(); assertThat(session.getAttribute("object1") != null).isTrue(); assertThat(session.getAttribute("object2") != null).isTrue(); assertThat(((Map) session.getAttribute("model")).containsKey("object1")).isTrue(); @@ -408,17 +417,19 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl } @SuppressWarnings("rawtypes") - @Test - public void parameterizedAnnotatedInterface() throws Exception { - initServlet( - wac -> wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(ModelExposingViewResolver.class)), - MyParameterizedControllerImpl.class); + @PathPatternsParameterizedTest + void parameterizedAnnotatedInterface(boolean usePathPatterns) throws Exception { + initDispatcherServlet( + MyParameterizedControllerImpl.class, usePathPatterns, + wac -> wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(ModelExposingViewResolver.class)) + ); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPage"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); assertThat(request.getAttribute("viewName")).isEqualTo("page1"); HttpSession session = request.getSession(); + assertThat(session).isNotNull(); assertThat(session.getAttribute("object1") != null).isTrue(); assertThat(session.getAttribute("object2") != null).isTrue(); assertThat(((Map) session.getAttribute("model")).containsKey("object1")).isTrue(); @@ -438,17 +449,19 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl } @SuppressWarnings("rawtypes") - @Test - public void parameterizedAnnotatedInterfaceWithOverriddenMappingsInImpl() throws Exception { - initServlet( - wac -> wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(ModelExposingViewResolver.class)), - MyParameterizedControllerImplWithOverriddenMappings.class); + @PathPatternsParameterizedTest + void parameterizedAnnotatedInterfaceWithOverriddenMappingsInImpl(boolean usePathPatterns) throws Exception { + initDispatcherServlet( + MyParameterizedControllerImplWithOverriddenMappings.class, usePathPatterns, + wac -> wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(ModelExposingViewResolver.class)) + ); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPage"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); assertThat(request.getAttribute("viewName")).isEqualTo("page1"); HttpSession session = request.getSession(); + assertThat(session).isNotNull(); assertThat(session.getAttribute("object1") != null).isTrue(); assertThat(session.getAttribute("object2") != null).isTrue(); assertThat(((Map) session.getAttribute("model")).containsKey("object1")).isTrue(); @@ -467,28 +480,30 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(((Map) session.getAttribute("model")).containsKey("testBeanList")).isTrue(); } - @Test - public void adaptedHandleMethods() throws Exception { - initServlet(wac -> { - RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); - mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); - wac.registerBeanDefinition("handlerMapping", mappingDef); - }, MyAdaptedController.class); - doTestAdaptedHandleMethods(); + @PathPatternsParameterizedTest + void adaptedHandleMethods(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MyAdaptedController.class, usePathPatterns, wac -> { + if (!usePathPatterns) { + RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); + mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); + wac.registerBeanDefinition("handlerMapping", mappingDef); + } + }); + doTestAdaptedHandleMethods(usePathPatterns); } - @Test - public void adaptedHandleMethods2() throws Exception { - initServletWithControllers(MyAdaptedController2.class); + @PathPatternsParameterizedTest + void adaptedHandleMethods2(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MyAdaptedController2.class, usePathPatterns); } - @Test - public void adaptedHandleMethods3() throws Exception { - initServletWithControllers(MyAdaptedController3.class); - doTestAdaptedHandleMethods(); + @PathPatternsParameterizedTest + void adaptedHandleMethods3(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MyAdaptedController3.class, usePathPatterns); + doTestAdaptedHandleMethods(usePathPatterns); } - private void doTestAdaptedHandleMethods() throws Exception { + private void doTestAdaptedHandleMethods(boolean usePathPatterns) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath1.do"); MockHttpServletResponse response = new MockHttpServletResponse(); request.addParameter("param1", "value1"); @@ -512,7 +527,12 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl request.addParameter("age", "2"); response = new MockHttpServletResponse(); getServlet().service(request, response); - assertThat(response.getContentAsString()).isEqualTo("test-name1-2"); + + if (!usePathPatterns) { + // This depends on suffix pattern matching and has different outcomes otherwise, + // e.g. 404 vs another method matching, depending on the test case. + assertThat(response.getContentAsString()).isEqualTo("test-name1-2"); + } request = new MockHttpServletRequest("GET", "/myPath4.do"); request.addParameter("param1", "value1"); @@ -524,11 +544,12 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("test-name1-typeMismatch"); } - @Test - public void formController() throws Exception { - initServlet( - wac -> wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(TestViewResolver.class)), - MyFormController.class); + @PathPatternsParameterizedTest + void formController(boolean usePathPatterns) throws Exception { + initDispatcherServlet( + MyFormController.class, usePathPatterns, + wac -> wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(TestViewResolver.class)) + ); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); request.addParameter("name", "name1"); @@ -538,11 +559,12 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("myView-name1-typeMismatch-tb1-myValue"); } - @Test - public void modelFormController() throws Exception { - initServlet( - wac -> wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(TestViewResolver.class)), - MyModelFormController.class); + @PathPatternsParameterizedTest + void modelFormController(boolean usePathPatterns) throws Exception { + initDispatcherServlet( + MyModelFormController.class, usePathPatterns, + wac -> wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(TestViewResolver.class)) + ); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); request.addParameter("name", "name1"); @@ -552,11 +574,12 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("myPath-name1-typeMismatch-tb1-myValue-yourValue"); } - @Test - public void lateBindingFormController() throws Exception { - initServlet( - wac -> wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(TestViewResolver.class)), - LateBindingFormController.class); + @PathPatternsParameterizedTest + void lateBindingFormController(boolean usePathPatterns) throws Exception { + initDispatcherServlet( + LateBindingFormController.class, usePathPatterns, + wac -> wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(TestViewResolver.class)) + ); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); request.addParameter("name", "name1"); @@ -566,16 +589,15 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("myView-name1-typeMismatch-tb1-myValue"); } - @Test - public void proxiedFormController() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void proxiedFormController(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MyFormController.class, usePathPatterns, wac -> { wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(TestViewResolver.class)); DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); autoProxyCreator.setBeanFactory(wac.getBeanFactory()); wac.getBeanFactory().addBeanPostProcessor(autoProxyCreator); - wac.getBeanFactory() - .registerSingleton("advisor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); - }, MyFormController.class); + wac.getBeanFactory().registerSingleton("advisor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); request.addParameter("name", "name1"); @@ -585,14 +607,14 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("myView-name1-typeMismatch-tb1-myValue"); } - @Test - public void commandProvidingFormControllerWithCustomEditor() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void commandProvidingFormControllerWithCustomEditor(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MyCommandProvidingFormController.class, usePathPatterns, wac -> { wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(TestViewResolver.class)); RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("webBindingInitializer", new MyWebBindingInitializer()); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, MyCommandProvidingFormController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); request.addParameter("defaultName", "myDefaultName"); @@ -603,9 +625,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("myView-String:myDefaultName-typeMismatch-tb1-myOriginalValue"); } - @Test - public void typedCommandProvidingFormController() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void typedCommandProvidingFormController(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MyTypedCommandProvidingFormController.class, usePathPatterns, wac -> { wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(TestViewResolver.class)); RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("webBindingInitializer", new MyWebBindingInitializer()); @@ -613,7 +635,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl argumentResolvers.add(new ServletWebArgumentResolverAdapter(new MySpecialArgumentResolver())); adapterDef.getPropertyValues().add("customArgumentResolvers", argumentResolvers); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, MyTypedCommandProvidingFormController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); request.addParameter("defaultName", "10"); @@ -640,11 +662,11 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("myView-special-99-special-99"); } - @Test - public void binderInitializingCommandProvidingFormController() throws Exception { - initServlet(wac -> wac.registerBeanDefinition("viewResolver", - new RootBeanDefinition(TestViewResolver.class)), - MyBinderInitializingCommandProvidingFormController.class); + @PathPatternsParameterizedTest + void binderInitializingCommandProvidingFormController(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MyBinderInitializingCommandProvidingFormController.class, usePathPatterns, wac -> wac.registerBeanDefinition("viewResolver", + new RootBeanDefinition(TestViewResolver.class)) + ); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); request.addParameter("defaultName", "myDefaultName"); @@ -655,11 +677,11 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("myView-String:myDefaultName-typeMismatch-tb1-myOriginalValue"); } - @Test - public void specificBinderInitializingCommandProvidingFormController() throws Exception { - initServlet(wac -> wac.registerBeanDefinition("viewResolver", - new RootBeanDefinition(TestViewResolver.class)), - MySpecificBinderInitializingCommandProvidingFormController.class); + @PathPatternsParameterizedTest + void specificBinderInitializingCommandProvidingFormController(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MySpecificBinderInitializingCommandProvidingFormController.class, usePathPatterns, wac -> wac.registerBeanDefinition("viewResolver", + new RootBeanDefinition(TestViewResolver.class)) + ); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); request.addParameter("defaultName", "myDefaultName"); @@ -670,21 +692,22 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("myView-String:myDefaultName-typeMismatch-tb1-myOriginalValue"); } - @Test - public void parameterDispatchingController() throws Exception { + @PathPatternsParameterizedTest + void parameterDispatchingController(boolean usePathPatterns) throws Exception { final MockServletContext servletContext = new MockServletContext(); final MockServletConfig servletConfig = new MockServletConfig(servletContext); WebApplicationContext webAppContext = - initServlet(wac -> { + initDispatcherServlet(MyParameterDispatchingController.class, usePathPatterns, wac -> { wac.setServletContext(servletContext); AnnotationConfigUtils.registerAnnotationConfigProcessors(wac); wac.getBeanFactory().registerResolvableDependency(ServletConfig.class, servletConfig); - }, MyParameterDispatchingController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest(servletContext, "GET", "/myPath.do"); MockHttpServletResponse response = new MockHttpServletResponse(); HttpSession session = request.getSession(); + assertThat(session).isNotNull(); getServlet().service(request, response); assertThat(response.getContentAsString()).isEqualTo("myView"); assertThat(request.getAttribute("servletContext")).isSameAs(servletContext); @@ -696,6 +719,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl request = new MockHttpServletRequest(servletContext, "GET", "/myPath.do"); response = new MockHttpServletResponse(); session = request.getSession(); + assertThat(session).isNotNull(); getServlet().service(request, response); assertThat(response.getContentAsString()).isEqualTo("myView"); assertThat(request.getAttribute("servletContext")).isSameAs(servletContext); @@ -729,13 +753,15 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(deserialized.session).isNotNull(); } - @Test - public void relativePathDispatchingController() throws Exception { - initServlet(wac -> { - RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); - mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); - wac.registerBeanDefinition("handlerMapping", mappingDef); - }, MyRelativePathDispatchingController.class); + @PathPatternsParameterizedTest + void relativePathDispatchingController(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MyRelativePathDispatchingController.class, usePathPatterns, wac -> { + if (!usePathPatterns) { + RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); + mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); + wac.registerBeanDefinition("handlerMapping", mappingDef); + } + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myApp/myHandle"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -755,16 +781,18 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl request = new MockHttpServletRequest("GET", "/myApp/surprise.do"); response = new MockHttpServletResponse(); getServlet().service(request, response); - assertThat(response.getContentAsString()).isEqualTo("mySurpriseView"); + assertThat(response.getContentAsString()).isEqualTo(!usePathPatterns ? "mySurpriseView" : "myView"); } - @Test - public void relativeMethodPathDispatchingController() throws Exception { - initServlet(wac -> { - RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); - mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); - wac.registerBeanDefinition("handlerMapping", mappingDef); - }, MyRelativeMethodPathDispatchingController.class); + @PathPatternsParameterizedTest + void relativeMethodPathDispatchingController(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MyRelativeMethodPathDispatchingController.class, usePathPatterns, wac -> { + if (!usePathPatterns) { + RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); + mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); + wac.registerBeanDefinition("handlerMapping", mappingDef); + } + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myApp/myHandle"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -784,12 +812,21 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl request = new MockHttpServletRequest("GET", "/herApp/surprise.do"); response = new MockHttpServletResponse(); getServlet().service(request, response); - assertThat(response.getContentAsString()).isEqualTo("mySurpriseView"); + + if (!usePathPatterns) { + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getContentAsString()).isEqualTo("mySurpriseView"); + } + else { + assertThat(response.getStatus()) + .as("Suffixes pattern matching should not work with PathPattern's") + .isEqualTo(404); + } } - @Test - public void nullCommandController() throws Exception { - initServletWithControllers(MyNullCommandController.class); + @PathPatternsParameterizedTest + void nullCommandController(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MyNullCommandController.class, usePathPatterns); getServlet().init(new MockServletConfig()); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath"); @@ -799,17 +836,17 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("myView"); } - @Test - public void equivalentMappingsWithSameMethodName() { - assertThatThrownBy(() -> initServletWithControllers(ChildController.class)) + @PathPatternsParameterizedTest + void equivalentMappingsWithSameMethodName(boolean usePathPatterns) { + assertThatThrownBy(() -> initDispatcherServlet(ChildController.class, usePathPatterns)) .isInstanceOf(BeanCreationException.class) .hasCauseInstanceOf(IllegalStateException.class) .hasMessageContaining("Ambiguous mapping"); } - @Test // gh-22543 - public void unmappedPathMapping() throws Exception { - initServletWithControllers(UnmappedPathController.class); + @PathPatternsParameterizedTest // gh-22543 + void unmappedPathMapping(boolean usePathPatterns) throws Exception { + initDispatcherServlet(UnmappedPathController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bogus-unmapped"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -823,9 +860,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("get"); } - @Test - public void explicitAndEmptyPathsControllerMapping() throws Exception { - initServletWithControllers(ExplicitAndEmptyPathsController.class); + @PathPatternsParameterizedTest + void explicitAndEmptyPathsControllerMapping(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ExplicitAndEmptyPathsController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -838,9 +875,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("get"); } - @Test - public void pathOrdering() throws Exception { - initServletWithControllers(PathOrderingController.class); + @PathPatternsParameterizedTest + void pathOrdering(boolean usePathPatterns) throws Exception { + initDispatcherServlet(PathOrderingController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/dir/myPath1.do"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -848,13 +885,13 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("method1"); } - @Test - public void requestBodyResponseBody() throws Exception { - initServletWithControllers(RequestResponseBodyController.class); + @PathPatternsParameterizedTest + void requestBodyResponseBody(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RequestResponseBodyController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something"); String requestBody = "Hello World"; - request.setContent(requestBody.getBytes("UTF-8")); + request.setContent(requestBody.getBytes(StandardCharsets.UTF_8)); request.addHeader("Content-Type", "text/plain; charset=utf-8"); request.addHeader("Accept", "text/*, */*"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -863,13 +900,13 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo(requestBody); } - @Test - public void httpPatch() throws Exception { - initServletWithControllers(RequestResponseBodyController.class); + @PathPatternsParameterizedTest + void httpPatch(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RequestResponseBodyController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("PATCH", "/something"); String requestBody = "Hello world!"; - request.setContent(requestBody.getBytes("UTF-8")); + request.setContent(requestBody.getBytes(StandardCharsets.UTF_8)); request.addHeader("Content-Type", "text/plain; charset=utf-8"); request.addHeader("Accept", "text/*, */*"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -878,18 +915,18 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo(requestBody); } - @Test - public void responseBodyNoAcceptableMediaType() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void responseBodyNoAcceptableMediaType(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RequestResponseBodyProducesController.class, usePathPatterns, wac -> { RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); StringHttpMessageConverter converter = new StringHttpMessageConverter(); adapterDef.getPropertyValues().add("messageConverters", converter); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, RequestResponseBodyProducesController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something"); String requestBody = "Hello World"; - request.setContent(requestBody.getBytes("UTF-8")); + request.setContent(requestBody.getBytes(StandardCharsets.UTF_8)); request.addHeader("Content-Type", "text/plain; charset=utf-8"); request.addHeader("Accept", "application/pdf, application/msword"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -897,13 +934,13 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getStatus()).isEqualTo(406); } - @Test - public void responseBodyWildCardMediaType() throws Exception { - initServletWithControllers(RequestResponseBodyController.class); + @PathPatternsParameterizedTest + void responseBodyWildCardMediaType(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RequestResponseBodyController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something"); String requestBody = "Hello World"; - request.setContent(requestBody.getBytes("UTF-8")); + request.setContent(requestBody.getBytes(StandardCharsets.UTF_8)); request.addHeader("Content-Type", "text/plain; charset=utf-8"); request.addHeader("Accept", "*/*"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -911,17 +948,17 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo(requestBody); } - @Test - public void unsupportedRequestBody() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void unsupportedRequestBody(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RequestResponseBodyController.class, usePathPatterns, wac -> { RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("messageConverters", new ByteArrayHttpMessageConverter()); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, RequestResponseBodyController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something"); String requestBody = "Hello World"; - request.setContent(requestBody.getBytes("UTF-8")); + request.setContent(requestBody.getBytes(StandardCharsets.UTF_8)); request.addHeader("Content-Type", "application/pdf"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); @@ -929,13 +966,13 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getHeader("Accept")).as("No Accept response header set").isNotNull(); } - @Test - public void responseBodyNoAcceptHeader() throws Exception { - initServletWithControllers(RequestResponseBodyController.class); + @PathPatternsParameterizedTest + void responseBodyNoAcceptHeader(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RequestResponseBodyController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something"); String requestBody = "Hello World"; - request.setContent(requestBody.getBytes("UTF-8")); + request.setContent(requestBody.getBytes(StandardCharsets.UTF_8)); request.addHeader("Content-Type", "text/plain; charset=utf-8"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); @@ -943,30 +980,30 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo(requestBody); } - @Test - public void badRequestRequestBody() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void badRequestRequestBody(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RequestResponseBodyController.class, usePathPatterns, wac -> { RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("messageConverters", new NotReadableMessageConverter()); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, RequestResponseBodyController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something"); String requestBody = "Hello World"; - request.setContent(requestBody.getBytes("UTF-8")); + request.setContent(requestBody.getBytes(StandardCharsets.UTF_8)); request.addHeader("Content-Type", "application/pdf"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); assertThat(response.getStatus()).as("Invalid response status code").isEqualTo(HttpServletResponse.SC_BAD_REQUEST); } - @Test - public void httpEntity() throws Exception { - initServletWithControllers(ResponseEntityController.class); + @PathPatternsParameterizedTest + void httpEntity(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ResponseEntityController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); String requestBody = "Hello World"; - request.setContent(requestBody.getBytes("UTF-8")); + request.setContent(requestBody.getBytes(StandardCharsets.UTF_8)); request.addHeader("Content-Type", "text/plain; charset=utf-8"); request.addHeader("Accept", "text/*, */*"); request.addHeader("MyRequestHeader", "MyValue"); @@ -983,29 +1020,30 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getStatus()).isEqualTo(404); } - @Test // SPR-16172 - public void httpEntityWithContentType() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest // SPR-16172 + void httpEntityWithContentType(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ResponseEntityController.class, usePathPatterns, wac -> { RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); List> messageConverters = new ArrayList<>(); messageConverters.add(new MappingJackson2HttpMessageConverter()); messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); adapterDef.getPropertyValues().add("messageConverters", messageConverters); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, ResponseEntityController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test-entity"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getHeader("Content-Type")).isEqualTo("application/xml"); - assertThat(response.getContentAsString()).isEqualTo(("" + - "Foo Bar")); + assertThat(response.getContentAsString()).isEqualTo( + "" + + "Foo Bar"); } - @Test // SPR-6877 - public void overlappingMessageConvertersRequestBody() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest // SPR-6877 + void overlappingMessageConvertersRequestBody(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RequestResponseBodyController.class, usePathPatterns, wac -> { RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); List> messageConverters = new ArrayList<>(); messageConverters.add(new StringHttpMessageConverter()); @@ -1013,10 +1051,10 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl .add(new SimpleMessageConverter(new MediaType("application","json"), MediaType.ALL)); adapterDef.getPropertyValues().add("messageConverters", messageConverters); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, RequestResponseBodyController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something"); - request.setContent("Hello World".getBytes("UTF-8")); + request.setContent("Hello World".getBytes(StandardCharsets.UTF_8)); request.addHeader("Content-Type", "text/plain; charset=utf-8"); request.addHeader("Accept", "application/json, text/javascript, */*"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1024,9 +1062,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getHeader("Content-Type")).as("Invalid content-type").isEqualTo("application/json"); } - @Test - public void responseBodyVoid() throws Exception { - initServletWithControllers(ResponseBodyVoidController.class); + @PathPatternsParameterizedTest + void responseBodyVoid(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ResponseBodyVoidController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something"); request.addHeader("Accept", "text/*, */*"); @@ -1035,9 +1073,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getStatus()).isEqualTo(200); } - @Test - public void responseBodyArgMismatch() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void responseBodyArgMismatch(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RequestBodyArgMismatchController.class, usePathPatterns, wac -> { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(A.class, B.class); try { @@ -1051,11 +1089,11 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("messageConverters", messageConverter); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, RequestBodyArgMismatchController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something"); String requestBody = ""; - request.setContent(requestBody.getBytes("UTF-8")); + request.setContent(requestBody.getBytes(StandardCharsets.UTF_8)); request.addHeader("Content-Type", "application/xml; charset=utf-8"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); @@ -1063,9 +1101,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl } - @Test - public void contentTypeHeaders() throws Exception { - initServletWithControllers(ContentTypeHeadersController.class); + @PathPatternsParameterizedTest + void contentTypeHeaders(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ContentTypeHeadersController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("POST", "/something"); request.setContentType("application/pdf"); @@ -1086,9 +1124,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getStatus()).isEqualTo(415); } - @Test - public void consumes() throws Exception { - initServletWithControllers(ConsumesController.class); + @PathPatternsParameterizedTest + void consumes(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ConsumesController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("POST", "/something"); request.setContentType("application/pdf"); @@ -1109,9 +1147,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getStatus()).isEqualTo(415); } - @Test - public void negatedContentTypeHeaders() throws Exception { - initServletWithControllers(NegatedContentTypeHeadersController.class); + @PathPatternsParameterizedTest + void negatedContentTypeHeaders(boolean usePathPatterns) throws Exception { + initDispatcherServlet(NegatedContentTypeHeadersController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("POST", "/something"); request.setContentType("application/pdf"); @@ -1126,9 +1164,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("non-pdf"); } - @Test - public void acceptHeaders() throws Exception { - initServletWithControllers(AcceptHeadersController.class); + @PathPatternsParameterizedTest + void acceptHeaders(boolean usePathPatterns) throws Exception { + initDispatcherServlet(AcceptHeadersController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something"); request.addHeader("Accept", "text/html"); @@ -1161,9 +1199,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getStatus()).isEqualTo(406); } - @Test - public void produces() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void produces(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ProducesController.class, usePathPatterns, wac -> { List> converters = new ArrayList<>(); converters.add(new MappingJackson2HttpMessageConverter()); converters.add(new Jaxb2RootElementHttpMessageConverter()); @@ -1178,7 +1216,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl beanDef.getPropertyValues().add("messageConverters", converters); wac.registerBeanDefinition("requestMappingResolver", beanDef); - }, ProducesController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something"); request.addHeader("Accept", "text/html"); @@ -1220,9 +1258,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("{\"reason\":\"error\"}"); } - @Test - public void responseStatus() throws Exception { - initServletWithControllers(ResponseStatusController.class); + @PathPatternsParameterizedTest + void responseStatus(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ResponseStatusController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1232,14 +1270,14 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getErrorMessage()).isEqualTo("It's alive!"); } - @Test - public void mavResolver() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void mavResolver(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ModelAndViewResolverController.class, usePathPatterns, wac -> { RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); ModelAndViewResolver[] mavResolvers = new ModelAndViewResolver[] {new MyModelAndViewResolver()}; adapterDef.getPropertyValues().add("modelAndViewResolvers", mavResolvers); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, ModelAndViewResolverController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1248,9 +1286,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl } - @Test - public void bindingCookieValue() throws Exception { - initServletWithControllers(BindingCookieValueController.class); + @PathPatternsParameterizedTest + void bindingCookieValue(boolean usePathPatterns) throws Exception { + initDispatcherServlet(BindingCookieValueController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test"); request.setCookies(new Cookie("date", "2008-11-18")); @@ -1259,9 +1297,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("test-2008"); } - @Test - public void ambiguousParams() throws Exception { - initServletWithControllers(AmbiguousParamsController.class); + @PathPatternsParameterizedTest + void ambiguousParams(boolean usePathPatterns) throws Exception { + initDispatcherServlet(AmbiguousParamsController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1275,9 +1313,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("myParam-42"); } - @Test // SPR-9062 - public void ambiguousPathAndRequestMethod() throws Exception { - initServletWithControllers(AmbiguousPathAndRequestMethodController.class); + @PathPatternsParameterizedTest // SPR-9062 + void ambiguousPathAndRequestMethod(boolean usePathPatterns) throws Exception { + initDispatcherServlet(AmbiguousPathAndRequestMethodController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bug/EXISTING"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1286,31 +1324,31 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("Pattern"); } - @Test - public void bridgeMethods() throws Exception { - initServletWithControllers(TestControllerImpl.class); + @PathPatternsParameterizedTest + void bridgeMethods(boolean usePathPatterns) throws Exception { + initDispatcherServlet(TestControllerImpl.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/method"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); } - @Test - public void bridgeMethodsWithMultipleInterfaces() throws Exception { - initServletWithControllers(ArticleController.class); + @PathPatternsParameterizedTest + void bridgeMethodsWithMultipleInterfaces(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ArticleController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/method"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); } - @Test - public void requestParamMap() throws Exception { - initServletWithControllers(RequestParamMapController.class); + @PathPatternsParameterizedTest + void requestParamMap(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RequestParamMapController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/map"); request.addParameter("key1", "value1"); - request.addParameter("key2", new String[] {"value21", "value22"}); + request.addParameter("key2", "value21", "value22"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); @@ -1323,9 +1361,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("key1=[value1],key2=[value21,value22]"); } - @Test - public void requestHeaderMap() throws Exception { - initServletWithControllers(RequestHeaderMapController.class); + @PathPatternsParameterizedTest + void requestHeaderMap(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RequestHeaderMapController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/map"); request.addHeader("Content-Type", "text/html"); @@ -1333,25 +1371,28 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); - assertThat(response.getContentAsString()).isEqualTo("Content-Type=text/html,Custom-Header=value21"); + assertThat(response.getContentAsString()) + .isEqualTo("Content-Type=text/html,Custom-Header=value21"); request.setRequestURI("/multiValueMap"); response = new MockHttpServletResponse(); getServlet().service(request, response); - assertThat(response.getContentAsString()).isEqualTo("Content-Type=[text/html],Custom-Header=[value21,value22]"); + assertThat(response.getContentAsString()) + .isEqualTo("Content-Type=[text/html],Custom-Header=[value21,value22]"); request.setRequestURI("/httpHeaders"); response = new MockHttpServletResponse(); getServlet().service(request, response); - assertThat(response.getContentAsString()).isEqualTo("Content-Type=[text/html],Custom-Header=[value21,value22]"); + assertThat(response.getContentAsString()) + .isEqualTo("Content-Type=[text/html],Custom-Header=[value21,value22]"); } - @Test - public void requestMappingInterface() throws Exception { - initServletWithControllers(IMyControllerImpl.class); + @PathPatternsParameterizedTest + void requestMappingInterface(boolean usePathPatterns) throws Exception { + initDispatcherServlet(IMyControllerImpl.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/handle"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1365,14 +1406,14 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("handle value"); } - @Test - public void requestMappingInterfaceWithProxy() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void requestMappingInterfaceWithProxy(boolean usePathPatterns) throws Exception { + initDispatcherServlet(IMyControllerImpl.class, usePathPatterns, wac -> { DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); autoProxyCreator.setBeanFactory(wac.getBeanFactory()); wac.getBeanFactory().addBeanPostProcessor(autoProxyCreator); wac.getBeanFactory().registerSingleton("advisor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); - }, IMyControllerImpl.class); + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/handle"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1386,9 +1427,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("handle value"); } - @Test - public void requestMappingBaseClass() throws Exception { - initServletWithControllers(MyAbstractControllerImpl.class); + @PathPatternsParameterizedTest + void requestMappingBaseClass(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MyAbstractControllerImpl.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/handle"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1397,9 +1438,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl } - @Test - public void trailingSlash() throws Exception { - initServletWithControllers(TrailingSlashController.class); + @PathPatternsParameterizedTest + void trailingSlash(boolean usePathPatterns) throws Exception { + initDispatcherServlet(TrailingSlashController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo/"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1410,9 +1451,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl /* * See SPR-6021 */ - @Test - public void customMapEditor() throws Exception { - initServletWithControllers(CustomMapEditorController.class); + @PathPatternsParameterizedTest + void customMapEditor(boolean usePathPatterns) throws Exception { + initDispatcherServlet(CustomMapEditorController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/handle"); request.addParameter("map", "bar"); @@ -1423,9 +1464,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("test-{foo=bar}"); } - @Test - public void multipartFileAsSingleString() throws Exception { - initServletWithControllers(MultipartController.class); + @PathPatternsParameterizedTest + void multipartFileAsSingleString(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MultipartController.class, usePathPatterns); MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); request.setRequestURI("/singleString"); @@ -1435,9 +1476,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("Juergen"); } - @Test - public void regularParameterAsSingleString() throws Exception { - initServletWithControllers(MultipartController.class); + @PathPatternsParameterizedTest + void regularParameterAsSingleString(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MultipartController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest(); request.setRequestURI("/singleString"); @@ -1448,9 +1489,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("Juergen"); } - @Test - public void multipartFileAsStringArray() throws Exception { - initServletWithControllers(MultipartController.class); + @PathPatternsParameterizedTest + void multipartFileAsStringArray(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MultipartController.class, usePathPatterns); MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); request.setRequestURI("/stringArray"); @@ -1460,9 +1501,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("Juergen"); } - @Test - public void regularParameterAsStringArray() throws Exception { - initServletWithControllers(MultipartController.class); + @PathPatternsParameterizedTest + void regularParameterAsStringArray(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MultipartController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest(); request.setRequestURI("/stringArray"); @@ -1473,9 +1514,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("Juergen"); } - @Test - public void multipartFilesAsStringArray() throws Exception { - initServletWithControllers(MultipartController.class); + @PathPatternsParameterizedTest + void multipartFilesAsStringArray(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MultipartController.class, usePathPatterns); MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); request.setRequestURI("/stringArray"); @@ -1486,9 +1527,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("Juergen-Eva"); } - @Test - public void regularParametersAsStringArray() throws Exception { - initServletWithControllers(MultipartController.class); + @PathPatternsParameterizedTest + void regularParametersAsStringArray(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MultipartController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest(); request.setRequestURI("/stringArray"); @@ -1500,16 +1541,16 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("Juergen-Eva"); } - @Test - public void parameterCsvAsStringArray() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void parameterCsvAsStringArray(boolean usePathPatterns) throws Exception { + initDispatcherServlet(CsvController.class, usePathPatterns, wac -> { RootBeanDefinition csDef = new RootBeanDefinition(FormattingConversionServiceFactoryBean.class); RootBeanDefinition wbiDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class); wbiDef.getPropertyValues().add("conversionService", csDef); RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("webBindingInitializer", wbiDef); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, CsvController.class); + }); MockHttpServletRequest request = new MockHttpServletRequest(); request.setRequestURI("/integerArray"); @@ -1520,9 +1561,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("1-2"); } - @Test - public void testMatchWithoutMethodLevelPath() throws Exception { - initServletWithControllers(NoPathGetAndM2PostController.class); + @PathPatternsParameterizedTest + void testMatchWithoutMethodLevelPath(boolean usePathPatterns) throws Exception { + initDispatcherServlet(NoPathGetAndM2PostController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/t1/m2"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1530,9 +1571,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getStatus()).isEqualTo(405); } - @Test // SPR-8536 - public void testHeadersCondition() throws Exception { - initServletWithControllers(HeadersConditionController.class); + @PathPatternsParameterizedTest // SPR-8536 + void testHeadersCondition(boolean usePathPatterns) throws Exception { + initDispatcherServlet(HeadersConditionController.class, usePathPatterns); // No "Accept" header MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); @@ -1562,9 +1603,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("homeJson"); } - @Test - public void redirectAttribute() throws Exception { - initServletWithControllers(RedirectAttributesController.class); + @PathPatternsParameterizedTest + void redirectAttribute(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RedirectAttributesController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("POST", "/messages"); HttpSession session = request.getSession(); @@ -1601,9 +1642,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(RequestContextUtils.getOutputFlashMap(request).isEmpty()).isTrue(); } - @Test // SPR-15176 - public void flashAttributesWithResponseEntity() throws Exception { - initServletWithControllers(RedirectAttributesController.class); + @PathPatternsParameterizedTest // SPR-15176 + void flashAttributesWithResponseEntity(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RedirectAttributesController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("POST", "/messages-response-entity"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1627,9 +1668,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(RequestContextUtils.getOutputFlashMap(request).isEmpty()).isTrue(); } - @Test - public void prototypeController() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void prototypeController(boolean usePathPatterns) throws Exception { + initDispatcherServlet(null, usePathPatterns, wac -> { RootBeanDefinition beanDef = new RootBeanDefinition(PrototypeController.class); beanDef.setScope(BeanDefinition.SCOPE_PROTOTYPE); wac.registerBeanDefinition("controller", beanDef); @@ -1648,9 +1689,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("count:3"); } - @Test - public void restController() throws Exception { - initServletWithControllers(ThisWillActuallyRun.class); + @PathPatternsParameterizedTest + void restController(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ThisWillActuallyRun.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1658,9 +1699,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("Hello World!"); } - @Test - public void responseAsHttpHeaders() throws Exception { - initServletWithControllers(HttpHeadersResponseController.class); + @PathPatternsParameterizedTest + void responseAsHttpHeaders(boolean usePathPatterns) throws Exception { + initDispatcherServlet(HttpHeadersResponseController.class, usePathPatterns); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(new MockHttpServletRequest("POST", "/"), response); @@ -1670,9 +1711,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentLength()).as("Expected an empty content").isEqualTo(0); } - @Test - public void responseAsHttpHeadersNoHeader() throws Exception { - initServletWithControllers(HttpHeadersResponseController.class); + @PathPatternsParameterizedTest + void responseAsHttpHeadersNoHeader(boolean usePathPatterns) throws Exception { + initDispatcherServlet(HttpHeadersResponseController.class, usePathPatterns); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(new MockHttpServletRequest("POST", "/empty"), response); @@ -1681,12 +1722,15 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentLength()).as("Expected an empty content").isEqualTo(0); } - @Test - public void responseBodyAsHtml() throws Exception { - initServlet(wac -> { - RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); - mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); - wac.registerBeanDefinition("handlerMapping", mappingDef); + @PathPatternsParameterizedTest + void responseBodyAsHtml(boolean usePathPatterns) throws Exception { + initDispatcherServlet(TextRestController.class, usePathPatterns, wac -> { + if (!usePathPatterns) { + // `useSuffixPatternMatch` is not allowed with PathPattern's + RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); + mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); + wac.registerBeanDefinition("handlerMapping", mappingDef); + } ContentNegotiationManagerFactoryBean factoryBean = new ContentNegotiationManagerFactoryBean(); factoryBean.afterPropertiesSet(); @@ -1694,7 +1738,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("contentNegotiationManager", factoryBean.getObject()); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, TextRestController.class); + }); byte[] content = "alert('boo')".getBytes(StandardCharsets.ISO_8859_1); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a1.html"); @@ -1703,21 +1747,28 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl getServlet().service(request, response); - assertThat(response.getStatus()).isEqualTo(200); - assertThat(response.getContentType()).isEqualTo("text/html;charset=ISO-8859-1"); - assertThat(response.getHeader("Content-Disposition")).isEqualTo("inline;filename=f.txt"); - assertThat(response.getContentAsByteArray()).isEqualTo(content); + if (!usePathPatterns) { + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getContentType()).isEqualTo("text/html;charset=ISO-8859-1"); + assertThat(response.getHeader("Content-Disposition")).isEqualTo("inline;filename=f.txt"); + assertThat(response.getContentAsByteArray()).isEqualTo(content); + } + else { + assertThat(response.getStatus()) + .as("Suffixes pattern matching should not work with PathPattern's") + .isEqualTo(404); + } } - @Test - public void responseBodyAsHtmlWithSuffixPresent() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void responseBodyAsHtmlWithSuffixPresent(boolean usePathPatterns) throws Exception { + initDispatcherServlet(TextRestController.class, usePathPatterns, wac -> { ContentNegotiationManagerFactoryBean factoryBean = new ContentNegotiationManagerFactoryBean(); factoryBean.afterPropertiesSet(); RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("contentNegotiationManager", factoryBean.getObject()); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, TextRestController.class); + }); byte[] content = "alert('boo')".getBytes(StandardCharsets.ISO_8859_1); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a2.html"); @@ -1732,12 +1783,14 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsByteArray()).isEqualTo(content); } - @Test - public void responseBodyAsHtmlWithProducesCondition() throws Exception { - initServlet(wac -> { - RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); - mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); - wac.registerBeanDefinition("handlerMapping", mappingDef); + @PathPatternsParameterizedTest + void responseBodyAsHtmlWithProducesCondition(boolean usePathPatterns) throws Exception { + initDispatcherServlet(TextRestController.class, usePathPatterns, wac -> { + if (!usePathPatterns) { + RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); + mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); + wac.registerBeanDefinition("handlerMapping", mappingDef); + } ContentNegotiationManagerFactoryBean factoryBean = new ContentNegotiationManagerFactoryBean(); factoryBean.afterPropertiesSet(); @@ -1745,7 +1798,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("contentNegotiationManager", factoryBean.getObject()); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, TextRestController.class); + }); byte[] content = "alert('boo')".getBytes(StandardCharsets.ISO_8859_1); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a3.html"); @@ -1754,21 +1807,28 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl getServlet().service(request, response); - assertThat(response.getStatus()).isEqualTo(200); - assertThat(response.getContentType()).isEqualTo("text/html;charset=ISO-8859-1"); - assertThat(response.getHeader("Content-Disposition")).isNull(); - assertThat(response.getContentAsByteArray()).isEqualTo(content); + if (!usePathPatterns) { + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getContentType()).isEqualTo("text/html;charset=ISO-8859-1"); + assertThat(response.getHeader("Content-Disposition")).isNull(); + assertThat(response.getContentAsByteArray()).isEqualTo(content); + } + else { + assertThat(response.getStatus()) + .as("Suffixes pattern matching should not work with PathPattern's") + .isEqualTo(404); + } } - @Test - public void responseBodyAsTextWithCssExtension() throws Exception { - initServlet(wac -> { + @PathPatternsParameterizedTest + void responseBodyAsTextWithCssExtension(boolean usePathPatterns) throws Exception { + initDispatcherServlet(TextRestController.class, usePathPatterns, wac -> { ContentNegotiationManagerFactoryBean factoryBean = new ContentNegotiationManagerFactoryBean(); factoryBean.afterPropertiesSet(); RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("contentNegotiationManager", factoryBean.getObject()); wac.registerBeanDefinition("handlerAdapter", adapterDef); - }, TextRestController.class); + }); byte[] content = "body".getBytes(StandardCharsets.ISO_8859_1); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a4.css"); @@ -1783,9 +1843,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsByteArray()).isEqualTo(content); } - @Test - public void modelAndViewWithStatus() throws Exception { - initServletWithControllers(ModelAndViewController.class); + @PathPatternsParameterizedTest + void modelAndViewWithStatus(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ModelAndViewController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1795,9 +1855,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getForwardedUrl()).isEqualTo("view"); } - @Test // SPR-14796 - public void modelAndViewWithStatusInExceptionHandler() throws Exception { - initServletWithControllers(ModelAndViewController.class); + @PathPatternsParameterizedTest // SPR-14796 + void modelAndViewWithStatusInExceptionHandler(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ModelAndViewController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/exception"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1807,9 +1867,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getForwardedUrl()).isEqualTo("view"); } - @Test - public void httpHead() throws Exception { - initServletWithControllers(ResponseEntityController.class); + @PathPatternsParameterizedTest + void httpHead(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ResponseEntityController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("HEAD", "/baz"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1831,9 +1891,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("body"); } - @Test - public void httpHeadExplicit() throws Exception { - initServletWithControllers(ResponseEntityController.class); + @PathPatternsParameterizedTest + void httpHeadExplicit(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ResponseEntityController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("HEAD", "/stores"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1843,9 +1903,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getHeader("h1")).isEqualTo("v1"); } - @Test - public void httpOptions() throws Exception { - initServletWithControllers(ResponseEntityController.class); + @PathPatternsParameterizedTest + void httpOptions(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ResponseEntityController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/baz"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -1856,9 +1916,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsByteArray().length == 0).isTrue(); } - @Test - public void dataClassBinding() throws Exception { - initServletWithControllers(DataClassController.class); + @PathPatternsParameterizedTest + void dataClassBinding(boolean usePathPatterns) throws Exception { + initDispatcherServlet(DataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); @@ -1868,9 +1928,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("value1-true-0"); } - @Test - public void dataClassBindingWithAdditionalSetter() throws Exception { - initServletWithControllers(DataClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithAdditionalSetter(boolean usePathPatterns) throws Exception { + initDispatcherServlet(DataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); @@ -1881,9 +1941,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("value1-true-3"); } - @Test - public void dataClassBindingWithResult() throws Exception { - initServletWithControllers(ValidatedDataClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithResult(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ValidatedDataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); @@ -1894,9 +1954,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("value1-true-3"); } - @Test - public void dataClassBindingWithOptionalParameter() throws Exception { - initServletWithControllers(ValidatedDataClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithOptionalParameter(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ValidatedDataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); @@ -1907,9 +1967,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("value1-true-8"); } - @Test - public void dataClassBindingWithMissingParameter() throws Exception { - initServletWithControllers(ValidatedDataClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithMissingParameter(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ValidatedDataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); @@ -1918,9 +1978,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("1:value1-null-null"); } - @Test - public void dataClassBindingWithConversionError() throws Exception { - initServletWithControllers(ValidatedDataClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithConversionError(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ValidatedDataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); @@ -1930,9 +1990,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("1:value1-x-null"); } - @Test - public void dataClassBindingWithValidationError() throws Exception { - initServletWithControllers(ValidatedDataClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithValidationError(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ValidatedDataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param2", "true"); @@ -1942,9 +2002,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("1:null-true-0"); } - @Test - public void dataClassBindingWithValidationErrorAndConversionError() throws Exception { - initServletWithControllers(ValidatedDataClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithValidationErrorAndConversionError(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ValidatedDataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param2", "x"); @@ -1953,9 +2013,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("2:null-x-null"); } - @Test - public void dataClassBindingWithOptional() throws Exception { - initServletWithControllers(OptionalDataClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithOptional(boolean usePathPatterns) throws Exception { + initDispatcherServlet(OptionalDataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); @@ -1966,9 +2026,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("value1-true-3"); } - @Test - public void dataClassBindingWithOptionalAndConversionError() throws Exception { - initServletWithControllers(OptionalDataClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithOptionalAndConversionError(boolean usePathPatterns) throws Exception { + initDispatcherServlet(OptionalDataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); @@ -1978,9 +2038,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("value1-x-null"); } - @Test - public void dataClassBindingWithFieldMarker() throws Exception { - initServletWithControllers(DataClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithFieldMarker(boolean usePathPatterns) throws Exception { + initDispatcherServlet(DataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); @@ -1991,9 +2051,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("value1-true-0"); } - @Test - public void dataClassBindingWithFieldMarkerFallback() throws Exception { - initServletWithControllers(DataClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithFieldMarkerFallback(boolean usePathPatterns) throws Exception { + initDispatcherServlet(DataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); @@ -2003,9 +2063,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("value1-false-0"); } - @Test - public void dataClassBindingWithFieldDefault() throws Exception { - initServletWithControllers(DataClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithFieldDefault(boolean usePathPatterns) throws Exception { + initDispatcherServlet(DataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); @@ -2016,9 +2076,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("value1-true-0"); } - @Test - public void dataClassBindingWithFieldDefaultFallback() throws Exception { - initServletWithControllers(DataClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithFieldDefaultFallback(boolean usePathPatterns) throws Exception { + initDispatcherServlet(DataClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); @@ -2028,9 +2088,9 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("value1-false-0"); } - @Test - public void dataClassBindingWithLocalDate() throws Exception { - initServletWithControllers(DateClassController.class); + @PathPatternsParameterizedTest + void dataClassBindingWithLocalDate(boolean usePathPatterns) throws Exception { + initDispatcherServlet(DateClassController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("date", "2010-01-01"); @@ -2044,7 +2104,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl static class ControllerWithEmptyValueMapping { @RequestMapping("") - public void myPath2(HttpServletResponse response) throws IOException { + public void myPath2(HttpServletResponse response) { throw new IllegalStateException("test"); } @@ -2063,7 +2123,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl private static class ControllerWithErrorThrown { @RequestMapping("") - public void myPath2(HttpServletResponse response) throws IOException { + public void myPath2(HttpServletResponse response) { throw new AssertionError("test"); } @@ -2460,12 +2520,12 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl static class MySpecialArg { - public MySpecialArg(String value) { + MySpecialArg(String value) { } } @Controller - static class MyTypedCommandProvidingFormController + private static class MyTypedCommandProvidingFormController extends MyCommandProvidingFormController { } @@ -3771,7 +3831,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl private final String content; - public BindStatusView(String content) { + BindStatusView(String content) { this.content = content; } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java index ca6bdcf44e..fdd2342105 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java @@ -19,19 +19,17 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; import java.io.Writer; import java.text.SimpleDateFormat; -import java.util.Arrays; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.stream.Stream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.Test; - import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.propertyeditors.CustomDateEditor; import org.springframework.stereotype.Controller; @@ -45,6 +43,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; +import org.springframework.web.servlet.handler.PathPatternsParameterizedTest; import org.springframework.web.servlet.view.AbstractView; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; @@ -57,9 +56,15 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends AbstractServletHandlerMethodTests { - @Test - public void simple() throws Exception { - initServletWithControllers(SimpleUriTemplateController.class); + @SuppressWarnings("unused") + static Stream pathPatternsArguments() { + return Stream.of(true, false); + } + + + @PathPatternsParameterizedTest + void simple(boolean usePathPatterns) throws Exception { + initDispatcherServlet(SimpleUriTemplateController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/42"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -67,9 +72,9 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab assertThat(response.getContentAsString()).isEqualTo("test-42-7"); } - @Test - public void multiple() throws Exception { - initServletWithControllers(MultipleUriTemplateController.class); + @PathPatternsParameterizedTest + void multiple(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MultipleUriTemplateController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/42;q=24/bookings/21-other;q=12"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -78,29 +83,29 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab assertThat(response.getContentAsString()).isEqualTo("test-42-q24-21-other-q12"); } - @Test - public void pathVarsInModel() throws Exception { + @PathPatternsParameterizedTest + void pathVarsInModel(boolean usePathPatterns) throws Exception { final Map pathVars = new HashMap<>(); pathVars.put("hotel", "42"); pathVars.put("booking", 21); pathVars.put("other", "other"); - WebApplicationContext wac = initServlet(context -> { + WebApplicationContext wac = initDispatcherServlet(ViewRenderingController.class, usePathPatterns, context -> { RootBeanDefinition beanDef = new RootBeanDefinition(ModelValidatingViewResolver.class); beanDef.getConstructorArgumentValues().addGenericArgumentValue(pathVars); context.registerBeanDefinition("viewResolver", beanDef); - }, ViewRenderingController.class); + }); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/42;q=1,2/bookings/21-other;q=3;r=R"); + HttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/42;q=1,2/bookings/21-other;q=3;r=R"); getServlet().service(request, new MockHttpServletResponse()); ModelValidatingViewResolver resolver = wac.getBean(ModelValidatingViewResolver.class); assertThat(resolver.validatedAttrCount).isEqualTo(3); } - @Test - public void binding() throws Exception { - initServletWithControllers(BindingUriTemplateController.class); + @PathPatternsParameterizedTest + void binding(boolean usePathPatterns) throws Exception { + initDispatcherServlet(BindingUriTemplateController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/42/dates/2008-11-18"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -112,16 +117,16 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab getServlet().service(request, response); assertThat(response.getStatus()).isEqualTo(400); - initServletWithControllers(NonBindingUriTemplateController.class); + initDispatcherServlet(NonBindingUriTemplateController.class, usePathPatterns); request = new MockHttpServletRequest("GET", "/hotels/42/dates/2008-foo-bar"); response = new MockHttpServletResponse(); getServlet().service(request, response); assertThat(response.getStatus()).isEqualTo(500); } - @Test - public void ambiguous() throws Exception { - initServletWithControllers(AmbiguousUriTemplateController.class); + @PathPatternsParameterizedTest + void ambiguous(boolean usePathPatterns) throws Exception { + initDispatcherServlet(AmbiguousUriTemplateController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/new"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -129,9 +134,9 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab assertThat(response.getContentAsString()).isEqualTo("specific"); } - @Test - public void relative() throws Exception { - initServletWithControllers(RelativePathUriTemplateController.class); + @PathPatternsParameterizedTest + void relative(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RelativePathUriTemplateController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/42/bookings/21"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -139,24 +144,27 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab assertThat(response.getContentAsString()).isEqualTo("test-42-21"); } - @Test - public void extension() throws Exception { - initServlet(wac -> { - RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); - mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); - mappingDef.getPropertyValues().add("removeSemicolonContent", "false"); - wac.registerBeanDefinition("handlerMapping", mappingDef); - }, SimpleUriTemplateController.class); + @PathPatternsParameterizedTest + void extension(boolean usePathPatterns) throws Exception { + initDispatcherServlet(SimpleUriTemplateController.class, usePathPatterns, wac -> { + if (!usePathPatterns) { + RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); + mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); + mappingDef.getPropertyValues().add("removeSemicolonContent", "false"); + wac.registerBeanDefinition("handlerMapping", mappingDef); + } + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/42;jsessionid=c0o7fszeb1;q=24.xml"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); - assertThat(response.getContentAsString()).isEqualTo("test-42-24"); + assertThat(response.getContentAsString()) + .isEqualTo(!usePathPatterns ? "test-42-24" : "test-42-24.xml"); } - @Test - public void typeConversionError() throws Exception { - initServletWithControllers(SimpleUriTemplateController.class); + @PathPatternsParameterizedTest + void typeConversionError(boolean usePathPatterns) throws Exception { + initDispatcherServlet(SimpleUriTemplateController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.xml"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -164,9 +172,9 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab assertThat(response.getStatus()).as("Invalid response status code").isEqualTo(HttpServletResponse.SC_BAD_REQUEST); } - @Test - public void explicitSubPath() throws Exception { - initServletWithControllers(ExplicitSubPathController.class); + @PathPatternsParameterizedTest + void explicitSubPath(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ExplicitSubPathController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/42"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -174,9 +182,9 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab assertThat(response.getContentAsString()).isEqualTo("test-42"); } - @Test - public void implicitSubPath() throws Exception { - initServletWithControllers(ImplicitSubPathController.class); + @PathPatternsParameterizedTest + void implicitSubPath(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ImplicitSubPathController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/42"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -184,9 +192,9 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab assertThat(response.getContentAsString()).isEqualTo("test-42"); } - @Test - public void crud() throws Exception { - initServletWithControllers(CrudController.class); + @PathPatternsParameterizedTest + void crud(boolean usePathPatterns) throws Exception { + initDispatcherServlet(CrudController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -224,9 +232,9 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab assertThat(response.getContentAsString()).isEqualTo("remove-42"); } - @Test - public void methodNotSupported() throws Exception { - initServletWithControllers(MethodNotAllowedController.class); + @PathPatternsParameterizedTest + void methodNotSupported(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MethodNotAllowedController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/1"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -249,9 +257,9 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab assertThat(response.getStatus()).isEqualTo(405); } - @Test - public void multiPaths() throws Exception { - initServletWithControllers(MultiPathController.class); + @PathPatternsParameterizedTest + void multiPaths(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MultiPathController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/category/page/5"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -259,20 +267,21 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab assertThat(response.getContentAsString()).isEqualTo("handle4-page-5"); } - @Test - public void customRegex() throws Exception { - initServletWithControllers(CustomRegexController.class); + @PathPatternsParameterizedTest + void customRegex(boolean usePathPatterns) throws Exception { + initDispatcherServlet(CustomRegexController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/42;q=1;q=2"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); assertThat(response.getStatus()).isEqualTo(200); - assertThat(response.getContentAsString()).isEqualTo("test-42-;q=1;q=2-[1, 2]"); + assertThat(response.getContentAsString()) + .isEqualTo(!usePathPatterns ? "test-42-;q=1;q=2-[1, 2]" : "test-42--[1, 2]"); } - @Test // gh-11306 - public void menuTree() throws Exception { - initServletWithControllers(MenuTreeController.class); + @PathPatternsParameterizedTest // gh-11306 + void menuTree(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MenuTreeController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/book/menu/type/M5"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -280,9 +289,9 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab assertThat(response.getContentAsString()).isEqualTo("M5"); } - @Test // gh-11542 - public void variableNames() throws Exception { - initServletWithControllers(VariableNamesController.class); + @PathPatternsParameterizedTest // gh-11542 + void variableNames(boolean usePathPatterns) throws Exception { + initDispatcherServlet(VariableNamesController.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test/foo"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -295,23 +304,25 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab assertThat(response.getContentAsString()).isEqualTo("bar-bar"); } - @Test // gh-13187 - public void variableNamesWithUrlExtension() throws Exception { - initServlet(wac -> { - RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); - mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); - wac.registerBeanDefinition("handlerMapping", mappingDef); - }, VariableNamesController.class); + @PathPatternsParameterizedTest // gh-13187 + void variableNamesWithUrlExtension(boolean usePathPatterns) throws Exception { + initDispatcherServlet(VariableNamesController.class, usePathPatterns, wac -> { + if (!usePathPatterns) { + RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); + mappingDef.getPropertyValues().add("useSuffixPatternMatch", true); + wac.registerBeanDefinition("handlerMapping", mappingDef); + } + }); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test/foo.json"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); - assertThat(response.getContentAsString()).isEqualTo("foo-foo"); + assertThat(response.getContentAsString()).isEqualTo(!usePathPatterns ? "foo-foo" : "foo-foo.json"); } - @Test // gh-11643 - public void doIt() throws Exception { - initServletWithControllers(Spr6978Controller.class); + @PathPatternsParameterizedTest // gh-11643 + void doIt(boolean usePathPatterns) throws Exception { + initDispatcherServlet(Spr6978Controller.class, usePathPatterns); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo/100"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -344,7 +355,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class SimpleUriTemplateController { @RequestMapping("/{root}") - public void handle(@PathVariable("root") int root, @MatrixVariable(required=false, defaultValue="7") int q, + void handle(@PathVariable("root") int root, @MatrixVariable(required=false, defaultValue="7") String q, Writer writer) throws IOException { assertThat(root).as("Invalid path variable value").isEqualTo(42); @@ -357,7 +368,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class MultipleUriTemplateController { @RequestMapping("/hotels/{hotel}/bookings/{booking}-{other}") - public void handle(@PathVariable("hotel") String hotel, + void handle(@PathVariable("hotel") String hotel, @PathVariable int booking, @PathVariable String other, @MatrixVariable(name = "q", pathVar = "hotel") int qHotel, @@ -374,12 +385,12 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class ViewRenderingController { @RequestMapping("/hotels/{hotel}/bookings/{booking}-{other}") - public void handle(@PathVariable("hotel") String hotel, @PathVariable int booking, + void handle(@PathVariable("hotel") String hotel, @PathVariable int booking, @PathVariable String other, @MatrixVariable MultiValueMap params) { assertThat(hotel).as("Invalid path variable value").isEqualTo("42"); assertThat(booking).as("Invalid path variable value").isEqualTo(21); - assertThat(params.get("q")).isEqualTo(Arrays.asList("1", "2", "3")); + assertThat(params.get("q")).containsExactlyInAnyOrder("1", "2", "3"); assertThat(params.getFirst("r")).isEqualTo("R"); } @@ -389,7 +400,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class BindingUriTemplateController { @InitBinder - public void initBinder(WebDataBinder binder, @PathVariable("hotel") String hotel) { + void initBinder(WebDataBinder binder, @PathVariable("hotel") String hotel) { assertThat(hotel).as("Invalid path variable value").isEqualTo("42"); binder.initBeanPropertyAccess(); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); @@ -398,7 +409,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab } @RequestMapping("/hotels/{hotel}/dates/{date}") - public void handle(@PathVariable("hotel") String hotel, @PathVariable Date date, Writer writer) + void handle(@PathVariable("hotel") String hotel, @PathVariable Date date, Writer writer) throws IOException { assertThat(hotel).as("Invalid path variable value").isEqualTo("42"); assertThat(date).as("Invalid path variable value").isEqualTo(new GregorianCalendar(2008, 10, 18).getTime()); @@ -410,7 +421,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class NonBindingUriTemplateController { @RequestMapping("/hotels/{hotel}/dates/{date}") - public void handle(@PathVariable("hotel") String hotel, @PathVariable Date date, Writer writer) + void handle(@PathVariable("hotel") String hotel, @PathVariable Date date, Writer writer) throws IOException { } @@ -421,7 +432,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class RelativePathUriTemplateController { @RequestMapping("bookings/{booking}") - public void handle(@PathVariable("hotel") String hotel, @PathVariable int booking, Writer writer) + void handle(@PathVariable("hotel") String hotel, @PathVariable int booking, Writer writer) throws IOException { assertThat(hotel).as("Invalid path variable value").isEqualTo("42"); assertThat(booking).as("Invalid path variable value").isEqualTo(21); @@ -435,18 +446,18 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class AmbiguousUriTemplateController { @RequestMapping("/{hotel}") - public void handleVars(@PathVariable("hotel") String hotel, Writer writer) throws IOException { + void handleVars(@PathVariable("hotel") String hotel, Writer writer) throws IOException { assertThat(hotel).as("Invalid path variable value").isEqualTo("42"); writer.write("variables"); } @RequestMapping("/new") - public void handleSpecific(Writer writer) throws IOException { + void handleSpecific(Writer writer) throws IOException { writer.write("specific"); } @RequestMapping("/*") - public void handleWildCard(Writer writer) throws IOException { + void handleWildCard(Writer writer) throws IOException { writer.write("wildcard"); } @@ -457,7 +468,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class ExplicitSubPathController { @RequestMapping("{hotel}") - public void handleHotel(@PathVariable String hotel, Writer writer) throws IOException { + void handleHotel(@PathVariable String hotel, Writer writer) throws IOException { writer.write("test-" + hotel); } @@ -468,7 +479,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class ImplicitSubPathController { @RequestMapping("{hotel}") - public void handleHotel(@PathVariable String hotel, Writer writer) throws IOException { + void handleHotel(@PathVariable String hotel, Writer writer) throws IOException { writer.write("test-" + hotel); } } @@ -477,7 +488,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class CustomRegexController { @RequestMapping("/{root:\\d+}{params}") - public void handle(@PathVariable("root") int root, @PathVariable("params") String paramString, + void handle(@PathVariable("root") int root, @PathVariable("params") String paramString, @MatrixVariable List q, Writer writer) throws IOException { assertThat(root).as("Invalid path variable value").isEqualTo(42); @@ -489,7 +500,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class DoubleController { @RequestMapping("/lat/{latitude}/long/{longitude}") - public void testLatLong(@PathVariable Double latitude, @PathVariable Double longitude, Writer writer) + void testLatLong(@PathVariable Double latitude, @PathVariable Double longitude, Writer writer) throws IOException { writer.write("latitude-" + latitude + "-longitude-" + longitude); } @@ -501,27 +512,27 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class CrudController { @RequestMapping(method = RequestMethod.GET) - public void list(Writer writer) throws IOException { + void list(Writer writer) throws IOException { writer.write("list"); } @RequestMapping(method = RequestMethod.POST) - public void create(Writer writer) throws IOException { + void create(Writer writer) throws IOException { writer.write("create"); } @RequestMapping(value = "/{hotel}", method = RequestMethod.GET) - public void show(@PathVariable String hotel, Writer writer) throws IOException { + void show(@PathVariable String hotel, Writer writer) throws IOException { writer.write("show-" + hotel); } @RequestMapping(value = "{hotel}", method = RequestMethod.PUT) - public void createOrUpdate(@PathVariable String hotel, Writer writer) throws IOException { + void createOrUpdate(@PathVariable String hotel, Writer writer) throws IOException { writer.write("createOrUpdate-" + hotel); } @RequestMapping(value = "{hotel}", method = RequestMethod.DELETE) - public void remove(@PathVariable String hotel, Writer writer) throws IOException { + void remove(@PathVariable String hotel, Writer writer) throws IOException { writer.write("remove-" + hotel); } @@ -532,19 +543,19 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class MethodNotAllowedController { @RequestMapping(method = RequestMethod.GET) - public void list(Writer writer) { + void list(Writer writer) { } @RequestMapping(method = RequestMethod.GET, value = "{hotelId}") - public void show(@PathVariable long hotelId, Writer writer) { + void show(@PathVariable long hotelId, Writer writer) { } @RequestMapping(method = RequestMethod.PUT, value = "{hotelId}") - public void createOrUpdate(@PathVariable long hotelId, Writer writer) { + void createOrUpdate(@PathVariable long hotelId, Writer writer) { } @RequestMapping(method = RequestMethod.DELETE, value = "/{hotelId}") - public void remove(@PathVariable long hotelId, Writer writer) { + void remove(@PathVariable long hotelId, Writer writer) { } } @@ -553,25 +564,25 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class MultiPathController { @RequestMapping(value = {"/{category}/page/{page}", "/*/{category}/page/{page}"}) - public void category(@PathVariable String category, @PathVariable int page, Writer writer) throws IOException { + void category(@PathVariable String category, @PathVariable int page, Writer writer) throws IOException { writer.write("handle1-"); writer.write("category-" + category); writer.write("page-" + page); } @RequestMapping(value = {"/{category}", "/*/{category}"}) - public void category(@PathVariable String category, Writer writer) throws IOException { + void category(@PathVariable String category, Writer writer) throws IOException { writer.write("handle2-"); writer.write("category-" + category); } @RequestMapping(value = {""}) - public void category(Writer writer) throws IOException { + void category(Writer writer) throws IOException { writer.write("handle3"); } @RequestMapping(value = {"/page/{page}"}) - public void category(@PathVariable int page, Writer writer) throws IOException { + void category(@PathVariable int page, Writer writer) throws IOException { writer.write("handle4-"); writer.write("page-" + page); } @@ -583,7 +594,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class MenuTreeController { @RequestMapping("type/{var}") - public void getFirstLevelFunctionNodes(@PathVariable("var") String var, Writer writer) throws IOException { + void getFirstLevelFunctionNodes(@PathVariable("var") String var, Writer writer) throws IOException { writer.write(var); } } @@ -593,12 +604,12 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class VariableNamesController { @RequestMapping(value = "/{foo}", method=RequestMethod.GET) - public void foo(@PathVariable String foo, Writer writer) throws IOException { + void foo(@PathVariable String foo, Writer writer) throws IOException { writer.write("foo-" + foo); } @RequestMapping(value = "/{bar}", method=RequestMethod.DELETE) - public void bar(@PathVariable String bar, Writer writer) throws IOException { + void bar(@PathVariable String bar, Writer writer) throws IOException { writer.write("bar-" + bar); } } @@ -607,18 +618,18 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class Spr6978Controller { @RequestMapping(value = "/{type}/{id}", method = RequestMethod.GET) - public void loadEntity(@PathVariable final String type, @PathVariable final long id, Writer writer) + void loadEntity(@PathVariable final String type, @PathVariable final long id, Writer writer) throws IOException { writer.write("loadEntity:" + type + ":" + id); } @RequestMapping(value = "/module/{id}", method = RequestMethod.GET) - public void loadModule(@PathVariable final long id, Writer writer) throws IOException { + void loadModule(@PathVariable final long id, Writer writer) throws IOException { writer.write("loadModule:" + id); } @RequestMapping(value = "/{type}/{id}", method = RequestMethod.POST) - public void publish(@PathVariable final String type, @PathVariable final long id, Writer writer) + void publish(@PathVariable final String type, @PathVariable final long id, Writer writer) throws IOException { writer.write("publish:" + type + ":" + id); } @@ -655,12 +666,12 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab } // @Disabled("ControllerClassNameHandlerMapping") -// public void controllerClassName() throws Exception { +// void controllerClassName() throws Exception { // @Disabled("useDefaultSuffixPattern property not supported") -// public void doubles() throws Exception { +// void doubles() throws Exception { // @Disabled("useDefaultSuffixPattern property not supported") -// public void noDefaultSuffixPattern() throws Exception { +// void noDefaultSuffixPattern() throws Exception { } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslatorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslatorTests.java index aabb3500d1..c1ed9b9c2d 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslatorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslatorTests.java @@ -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. @@ -16,9 +16,11 @@ package org.springframework.web.servlet.view; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import java.util.function.Function; +import java.util.stream.Stream; +import org.springframework.web.servlet.handler.PathPatternsParameterizedTest; +import org.springframework.web.servlet.handler.PathPatternsTestUtils; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; @@ -31,110 +33,99 @@ public class DefaultRequestToViewNameTranslatorTests { private static final String VIEW_NAME = "apple"; private static final String EXTENSION = ".html"; - private static final String CONTEXT_PATH = "/sundays"; - private DefaultRequestToViewNameTranslator translator; - private MockHttpServletRequest request; + private final DefaultRequestToViewNameTranslator translator = new DefaultRequestToViewNameTranslator(); - @BeforeEach - public void setUp() { - this.translator = new DefaultRequestToViewNameTranslator(); - this.request = new MockHttpServletRequest(); - this.request.setContextPath(CONTEXT_PATH); + @SuppressWarnings("unused") + private static Stream> pathPatternsArguments() { + return PathPatternsTestUtils.requestArguments("/sundays"); } - @Test - public void testGetViewNameLeavesLeadingSlashIfSoConfigured() { - request.setRequestURI(CONTEXT_PATH + "/" + VIEW_NAME + "/"); + @PathPatternsParameterizedTest + void testGetViewNameLeavesLeadingSlashIfSoConfigured(Function requestFactory) { + MockHttpServletRequest request = requestFactory.apply(VIEW_NAME + "/"); this.translator.setStripLeadingSlash(false); - assertViewName("/" + VIEW_NAME); + assertViewName(request, "/" + VIEW_NAME); } - @Test - public void testGetViewNameLeavesTrailingSlashIfSoConfigured() { - request.setRequestURI(CONTEXT_PATH + "/" + VIEW_NAME + "/"); + @PathPatternsParameterizedTest + void testGetViewNameLeavesTrailingSlashIfSoConfigured(Function requestFactory) { + MockHttpServletRequest request = requestFactory.apply(VIEW_NAME + "/"); this.translator.setStripTrailingSlash(false); - assertViewName(VIEW_NAME + "/"); + assertViewName(request, VIEW_NAME + "/"); } - @Test - public void testGetViewNameLeavesExtensionIfSoConfigured() { - request.setRequestURI(CONTEXT_PATH + "/" + VIEW_NAME + EXTENSION); + @PathPatternsParameterizedTest + void testGetViewNameLeavesExtensionIfSoConfigured(Function requestFactory) { + MockHttpServletRequest request = requestFactory.apply(VIEW_NAME + EXTENSION); this.translator.setStripExtension(false); - assertViewName(VIEW_NAME + EXTENSION); + assertViewName(request, VIEW_NAME + EXTENSION); } - @Test - public void testGetViewNameWithDefaultConfiguration() { - request.setRequestURI(CONTEXT_PATH + VIEW_NAME + EXTENSION); - assertViewName(VIEW_NAME); + @PathPatternsParameterizedTest + void testGetViewNameWithDefaultConfiguration(Function requestFactory) { + MockHttpServletRequest request = requestFactory.apply(VIEW_NAME + EXTENSION); + assertViewName(request, VIEW_NAME); } - @Test - public void testGetViewNameWithCustomSeparator() { - request.setRequestURI(CONTEXT_PATH + VIEW_NAME + "/fiona" + EXTENSION); + @PathPatternsParameterizedTest + void testGetViewNameWithCustomSeparator(Function requestFactory) { + MockHttpServletRequest request = requestFactory.apply(VIEW_NAME + "/fiona" + EXTENSION); this.translator.setSeparator("_"); - assertViewName(VIEW_NAME + "_fiona"); + assertViewName(request, VIEW_NAME + "_fiona"); } - @Test - public void testGetViewNameWithNoExtension() { - request.setRequestURI(CONTEXT_PATH + VIEW_NAME); - assertViewName(VIEW_NAME); + @PathPatternsParameterizedTest + void testGetViewNameWithNoExtension(Function requestFactory) { + MockHttpServletRequest request = requestFactory.apply(VIEW_NAME); + assertViewName(request, VIEW_NAME); } - @Test - public void testGetViewNameWithSemicolonContent() { - request.setRequestURI(CONTEXT_PATH + VIEW_NAME + ";a=A;b=B"); - assertViewName(VIEW_NAME); + @PathPatternsParameterizedTest + void testGetViewNameWithSemicolonContent(Function requestFactory) { + MockHttpServletRequest request = requestFactory.apply(VIEW_NAME + ";a=A;b=B"); + assertViewName(request, VIEW_NAME); } - @Test - public void testGetViewNameWithPrefix() { + @PathPatternsParameterizedTest + void testGetViewNameWithPrefix(Function requestFactory) { final String prefix = "fiona_"; - request.setRequestURI(CONTEXT_PATH + VIEW_NAME); + MockHttpServletRequest request = requestFactory.apply(VIEW_NAME); this.translator.setPrefix(prefix); - assertViewName(prefix + VIEW_NAME); + assertViewName(request, prefix + VIEW_NAME); } - @Test - public void testGetViewNameWithNullPrefix() { - request.setRequestURI(CONTEXT_PATH + VIEW_NAME); + @PathPatternsParameterizedTest + void testGetViewNameWithNullPrefix(Function requestFactory) { + MockHttpServletRequest request = requestFactory.apply(VIEW_NAME); this.translator.setPrefix(null); - assertViewName(VIEW_NAME); + assertViewName(request, VIEW_NAME); } - @Test - public void testGetViewNameWithSuffix() { + @PathPatternsParameterizedTest + void testGetViewNameWithSuffix(Function requestFactory) { final String suffix = ".fiona"; - request.setRequestURI(CONTEXT_PATH + VIEW_NAME); + MockHttpServletRequest request = requestFactory.apply(VIEW_NAME); this.translator.setSuffix(suffix); - assertViewName(VIEW_NAME + suffix); + assertViewName(request, VIEW_NAME + suffix); } - @Test - public void testGetViewNameWithNullSuffix() { - request.setRequestURI(CONTEXT_PATH + VIEW_NAME); + @PathPatternsParameterizedTest + void testGetViewNameWithNullSuffix(Function requestFactory) { + MockHttpServletRequest request = requestFactory.apply(VIEW_NAME); this.translator.setSuffix(null); - assertViewName(VIEW_NAME); - } - - @Test - public void testTrySetUrlPathHelperToNull() { - try { - this.translator.setUrlPathHelper(null); - } - catch (IllegalArgumentException expected) { - } + assertViewName(request, VIEW_NAME); } - private void assertViewName(String expectedViewName) { - String actualViewName = this.translator.getViewName(this.request); + private void assertViewName(MockHttpServletRequest request, String expectedViewName) { + String actualViewName = this.translator.getViewName(request); assertThat(actualViewName).isNotNull(); - assertThat(actualViewName).as("Did not get the expected viewName from the DefaultRequestToViewNameTranslator.getViewName(..)").isEqualTo(expectedViewName); + assertThat(actualViewName) + .as("Did not get the expected viewName from the DefaultRequestToViewNameTranslator.getViewName(..)") + .isEqualTo(expectedViewName); } } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 9a4b942109..7898ded3ed 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -24,7 +24,7 @@ import org.springframework.http.HttpHeaders.* import org.springframework.http.HttpMethod.* import org.springframework.http.HttpStatus import org.springframework.http.MediaType.* -import org.springframework.web.testfixture.servlet.MockHttpServletRequest +import org.springframework.web.servlet.handler.PathPatternsTestUtils /** * Tests for WebMvc.fn [RouterFunctionDsl]. @@ -35,7 +35,7 @@ class RouterFunctionDslTests { @Test fun header() { - val servletRequest = MockHttpServletRequest() + val servletRequest = PathPatternsTestUtils.initRequest("GET", "", true) servletRequest.addHeader("bar", "bar") val request = DefaultServerRequest(servletRequest, emptyList()) assertThat(sampleRouter().route(request).isPresent).isTrue() @@ -43,7 +43,7 @@ class RouterFunctionDslTests { @Test fun accept() { - val servletRequest = MockHttpServletRequest("GET", "/content") + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/content", true) servletRequest.addHeader(ACCEPT, APPLICATION_ATOM_XML_VALUE) val request = DefaultServerRequest(servletRequest, emptyList()) assertThat(sampleRouter().route(request).isPresent).isTrue() @@ -51,7 +51,7 @@ class RouterFunctionDslTests { @Test fun acceptAndPOST() { - val servletRequest = MockHttpServletRequest("POST", "/api/foo/") + val servletRequest = PathPatternsTestUtils.initRequest("POST", "/api/foo/", true) servletRequest.addHeader(ACCEPT, APPLICATION_JSON_VALUE) val request = DefaultServerRequest(servletRequest, emptyList()) assertThat(sampleRouter().route(request).isPresent).isTrue() @@ -59,7 +59,7 @@ class RouterFunctionDslTests { @Test fun acceptAndPOSTWithRequestPredicate() { - val servletRequest = MockHttpServletRequest("POST", "/api/bar/") + val servletRequest = PathPatternsTestUtils.initRequest("POST", "/api/bar/", true) servletRequest.addHeader(ACCEPT, APPLICATION_JSON_VALUE) servletRequest.addHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) val request = DefaultServerRequest(servletRequest, emptyList()) @@ -68,7 +68,7 @@ class RouterFunctionDslTests { @Test fun contentType() { - val servletRequest = MockHttpServletRequest("GET", "/content") + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/content", true) servletRequest.addHeader(CONTENT_TYPE, APPLICATION_OCTET_STREAM_VALUE) val request = DefaultServerRequest(servletRequest, emptyList()) assertThat(sampleRouter().route(request).isPresent).isTrue() @@ -76,28 +76,29 @@ class RouterFunctionDslTests { @Test fun resourceByPath() { - val servletRequest = MockHttpServletRequest("GET", "/org/springframework/web/servlet/function/response.txt") + val servletRequest = PathPatternsTestUtils.initRequest( + "GET", "/org/springframework/web/servlet/function/response.txt", true) val request = DefaultServerRequest(servletRequest, emptyList()) assertThat(sampleRouter().route(request).isPresent).isTrue() } @Test fun method() { - val servletRequest = MockHttpServletRequest("PATCH", "/") + val servletRequest = PathPatternsTestUtils.initRequest("PATCH", "/", true) val request = DefaultServerRequest(servletRequest, emptyList()) assertThat(sampleRouter().route(request).isPresent).isTrue() } @Test fun path() { - val servletRequest = MockHttpServletRequest("GET", "/baz") + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/baz", true) val request = DefaultServerRequest(servletRequest, emptyList()) assertThat(sampleRouter().route(request).isPresent).isTrue() } @Test fun resource() { - val servletRequest = MockHttpServletRequest("GET", "/response.txt") + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/response.txt", true) val request = DefaultServerRequest(servletRequest, emptyList()) assertThat(sampleRouter().route(request).isPresent).isTrue() } @@ -105,7 +106,7 @@ class RouterFunctionDslTests { @Test fun noRoute() { - val servletRequest = MockHttpServletRequest("GET", "/bar") + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/bar", true) servletRequest.addHeader(ACCEPT, APPLICATION_PDF_VALUE) servletRequest.addHeader(CONTENT_TYPE, APPLICATION_PDF_VALUE) val request = DefaultServerRequest(servletRequest, emptyList()) @@ -114,7 +115,7 @@ class RouterFunctionDslTests { @Test fun rendering() { - val servletRequest = MockHttpServletRequest("GET", "/rendering") + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/rendering", true) val request = DefaultServerRequest(servletRequest, emptyList()) assertThat(sampleRouter().route(request).get().handle(request) is RenderingResponse).isTrue() } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodKotlinTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodKotlinTests.kt index cb3c36529f..aa44f91597 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodKotlinTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodKotlinTests.kt @@ -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,20 +17,28 @@ package org.springframework.web.servlet.mvc.method.annotation import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +import org.springframework.web.servlet.handler.PathPatternsParameterizedTest import org.springframework.web.testfixture.servlet.MockHttpServletRequest import org.springframework.web.testfixture.servlet.MockHttpServletResponse +import java.util.stream.Stream /** * @author Sebastien Deleuze */ class ServletAnnotationControllerHandlerMethodKotlinTests : AbstractServletHandlerMethodTests() { - @Test - fun dataClassBinding() { - initServletWithControllers(DataClassController::class.java) + companion object { + @JvmStatic + fun pathPatternsArguments(): Stream { + return Stream.of(true, false) + } + } + + @PathPatternsParameterizedTest + fun dataClassBinding(usePathPatterns: Boolean) { + initDispatcherServlet(DataClassController::class.java, usePathPatterns) val request = MockHttpServletRequest("GET", "/bind") request.addParameter("param1", "value1") @@ -40,9 +48,9 @@ class ServletAnnotationControllerHandlerMethodKotlinTests : AbstractServletHandl assertThat(response.contentAsString).isEqualTo("value1-2") } - @Test - fun dataClassBindingWithOptionalParameterAndAllParameters() { - initServletWithControllers(DataClassController::class.java) + @PathPatternsParameterizedTest + fun dataClassBindingWithOptionalParameterAndAllParameters(usePathPatterns: Boolean) { + initDispatcherServlet(DataClassController::class.java, usePathPatterns) val request = MockHttpServletRequest("GET", "/bind-optional-parameter") request.addParameter("param1", "value1") @@ -52,9 +60,9 @@ class ServletAnnotationControllerHandlerMethodKotlinTests : AbstractServletHandl assertThat(response.contentAsString).isEqualTo("value1-2") } - @Test - fun dataClassBindingWithOptionalParameterAndOnlyMissingParameters() { - initServletWithControllers(DataClassController::class.java) + @PathPatternsParameterizedTest + fun dataClassBindingWithOptionalParameterAndOnlyMissingParameters(usePathPatterns: Boolean) { + initDispatcherServlet(DataClassController::class.java, usePathPatterns) val request = MockHttpServletRequest("GET", "/bind-optional-parameter") request.addParameter("param1", "value1") diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map1.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map1.xml index 04fc0c064b..f11859211b 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map1.xml +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map1.xml @@ -1,7 +1,7 @@ - - - + diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map2.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map2.xml index fcf44c3d2e..fa3ae8f0ad 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map2.xml +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map2.xml @@ -1,7 +1,7 @@ - - - + @@ -24,10 +24,19 @@ - + - + + + + + + + + + + /org/springframework/web/servlet/handler/map2.properties diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.properties b/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.properties new file mode 100644 index 0000000000..18674d7966 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.properties @@ -0,0 +1,26 @@ +welcome.html=mainController +/path??matching.html=mainController +/pathmatchingTest.html=mainController +??path??matching.html=mainController +/administrator/pathmatching.html=mainController +/administrator/testlast*=mainController +/administrator/testing/longer/{*foobar}=mainController +/administrator/*/testlast*=mainController +/administrator/*/pathmatching.html=mainController +/pathmatching??.html=mainController +/*.jsp=mainController +/administrator/another/bla.xml=mainController +/*test*.jpeg=mainController +/*/test.jpeg=mainController +/outofpattern*yeah=mainController +/anotherTest*=mainController +/stillAnotherTestYeah=mainController +/shortpattern/testing=mainController +/show123.html=mainController +/sho*=mainController +/bookseats.html=mainController +/reservation.html=mainController +/payment.html=mainController +/confirmation.html=mainController +/test%26t%20est/path%26m%20atching.html=mainController +*=starController \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.xml index 112d10cfa9..2d709e4c36 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.xml +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.xml @@ -1,40 +1,22 @@ - + - - - - true - - - welcome.html=mainController - /path??matching.html=mainController - /pathmatchingTest.html=mainController - ??path??matching.html=mainController - /administrator/pathmatching.html=mainController - /administrator/testlast*=mainController - /administrator/testing/longer/{*foobar}=mainController - /administrator/*/testlast*=mainController - /administrator/*/pathmatching.html=mainController - /pathmatching??.html=mainController - /*.jsp=mainController - /administrator/another/bla.xml=mainController - /*test*.jpeg=mainController - /*/test.jpeg=mainController - /outofpattern*yeah=mainController - /anotherTest*=mainController - /stillAnotherTestYeah=mainController - /shortpattern/testing=mainController - /show123.html=mainController - /sho*=mainController - /bookseats.html=mainController - /reservation.html=mainController - /payment.html=mainController - /confirmation.html=mainController - /test%26t%20est/path%26m%20atching.html=mainController - *=starController - + + + + + + + + true + + + + + /org/springframework/web/servlet/handler/map3.properties