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 index 8d2cf41675..41398f2ece 100644 --- a/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java @@ -15,24 +15,29 @@ */ package org.springframework.web.util; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import javax.servlet.RequestDispatcher; import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletMapping; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.MappingMatch; import org.springframework.http.server.PathContainer; import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** - * 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. + * Utility class to assist with preparation and access to the lookup path for + * request mapping purposes. This can be the parsed {@link RequestPath} + * representation of the path when use of + * {@link org.springframework.web.util.pattern.PathPattern parsed patterns} + * is enabled or a String path for use with a + * {@link org.springframework.util.PathMatcher} otherwise. * * @author Rossen Stoyanchev * @since 5.3 @@ -44,20 +49,21 @@ public abstract class ServletRequestPathUtils { /** - * 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}. + * Parse the {@link HttpServletRequest#getRequestURI() requestURI} to a + * {@link RequestPath} and save it in the request attribute + * {@link #PATH_ATTRIBUTE} for subsequent use with + * {@link org.springframework.web.util.pattern.PathPattern parsed patterns}. + * The returned {@code RequestPath} will have both the contextPath and any + * servletPath prefix omitted from the {@link RequestPath#pathWithinApplication() + * pathWithinApplication} it exposes. * - *

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

This method is typically called by the {@code DispatcherServlet} to + * if any {@code HandlerMapping} indicates that it uses parsed patterns. + * After that the pre-parsed and cached {@code RequestPath} can be accessed + * through {@link #getParsedRequestPath(ServletRequest)}. */ 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()); + RequestPath requestPath = ServletRequestPath.parse(request); request.setAttribute(PATH_ATTRIBUTE, requestPath); return requestPath; } @@ -172,4 +178,108 @@ public abstract class ServletRequestPathUtils { request.getAttribute(UrlPathHelper.PATH_ATTRIBUTE) != null); } + + /** + * Simple wrapper around the default {@link RequestPath} implementation that + * supports a servletPath as an additional prefix to be omitted from + * {@link #pathWithinApplication()}. + */ + private static class ServletRequestPath implements RequestPath { + + private final RequestPath requestPath; + + private final PathContainer contextPath; + + private ServletRequestPath(String rawPath, @Nullable String contextPath, String servletPathPrefix) { + Assert.notNull(servletPathPrefix, "`servletPathPrefix` is required"); + this.requestPath = RequestPath.parse(rawPath, contextPath + servletPathPrefix); + this.contextPath = PathContainer.parsePath(StringUtils.hasText(contextPath) ? contextPath : ""); + } + + @Override + public String value() { + return this.requestPath.value(); + } + + @Override + public List elements() { + return this.requestPath.elements(); + } + + @Override + public PathContainer contextPath() { + return this.contextPath; + } + + @Override + public PathContainer pathWithinApplication() { + return this.requestPath.pathWithinApplication(); + } + + @Override + public RequestPath modifyContextPath(String contextPath) { + throw new UnsupportedOperationException(); + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + return (this.requestPath.equals(((ServletRequestPath) other).requestPath)); + } + + @Override + public int hashCode() { + return this.requestPath.hashCode(); + } + + @Override + public String toString() { + return this.requestPath.toString(); + } + + + public static RequestPath parse(HttpServletRequest request) { + String requestUri = (String) request.getAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE); + if (requestUri == null) { + requestUri = request.getRequestURI(); + } + if (UrlPathHelper.servlet4Present) { + String servletPathPrefix = Servlet4Delegate.getServletPathPrefix(request); + if (StringUtils.hasText(servletPathPrefix)) { + return new ServletRequestPath(requestUri, request.getContextPath(), servletPathPrefix); + } + } + return RequestPath.parse(requestUri, request.getContextPath()); + } + } + + + /** + * Inner class to avoid a hard dependency on Servlet 4 {@link HttpServletMapping} + * and {@link MappingMatch} at runtime. + */ + private static class Servlet4Delegate { + + @Nullable + public static String getServletPathPrefix(HttpServletRequest request) { + HttpServletMapping mapping = (HttpServletMapping) request.getAttribute(RequestDispatcher.INCLUDE_MAPPING); + if (mapping == null) { + mapping = request.getHttpServletMapping(); + } + MappingMatch match = mapping.getMappingMatch(); + if (!ObjectUtils.nullSafeEquals(match, MappingMatch.PATH)) { + return null; + } + String servletPath = (String) request.getAttribute(WebUtils.INCLUDE_SERVLET_PATH_ATTRIBUTE); + servletPath = (servletPath != null ? servletPath : request.getServletPath()); + return UriUtils.encodePath(servletPath, StandardCharsets.UTF_8); + } + } + } 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 f283af87fe..c216c25ae6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -62,7 +62,7 @@ public class UrlPathHelper { */ public static final String PATH_ATTRIBUTE = UrlPathHelper.class.getName() + ".PATH"; - private static final boolean servlet4Present = + static final boolean servlet4Present = ClassUtils.hasMethod(HttpServletRequest.class, "getHttpServletMapping"); /** 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 b5094d2cd2..3fa6880171 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-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -26,40 +26,37 @@ import static org.assertj.core.api.Assertions.assertThat; class DefaultRequestPathTests { @Test - void requestPath() { + void parse() { // basic - testRequestPath("/app/a/b/c", "/app", "/a/b/c"); + testParse("/app/a/b/c", "/app", "/a/b/c"); // no context path - testRequestPath("/a/b/c", "", "/a/b/c"); + testParse("/a/b/c", "", "/a/b/c"); // context path only - testRequestPath("/a/b", "/a/b", ""); + testParse("/a/b", "/a/b", ""); // root path - testRequestPath("/", "", "/"); + testParse("/", "", "/"); // empty path - testRequestPath("", "", ""); - testRequestPath("", "/", ""); + testParse("", "", ""); + testParse("", "/", ""); // trailing slash - testRequestPath("/app/a/", "/app", "/a/"); - testRequestPath("/app/a//", "/app", "/a//"); + testParse("/app/a/", "/app", "/a/"); + testParse("/app/a//", "/app", "/a//"); } - private void testRequestPath(String fullPath, String contextPath, String pathWithinApplication) { - + private void testParse(String fullPath, String contextPath, String pathWithinApplication) { RequestPath requestPath = RequestPath.parse(fullPath, contextPath); - Object expected = contextPath.equals("/") ? "" : contextPath; assertThat(requestPath.contextPath().value()).isEqualTo(expected); assertThat(requestPath.pathWithinApplication().value()).isEqualTo(pathWithinApplication); } @Test - void updateRequestPath() { - + void modifyContextPath() { RequestPath requestPath = RequestPath.parse("/aA/bB/cC", null); assertThat(requestPath.contextPath().value()).isEqualTo(""); diff --git a/spring-web/src/test/java/org/springframework/web/util/ServletRequestPathUtilsTests.java b/spring-web/src/test/java/org/springframework/web/util/ServletRequestPathUtilsTests.java new file mode 100644 index 0000000000..6814bd55df --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/util/ServletRequestPathUtilsTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2021 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.http.MappingMatch; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.server.RequestPath; +import org.springframework.web.testfixture.servlet.MockHttpServletMapping; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ServletRequestPathUtils}. + * @author Rossen Stoyanchev + */ +public class ServletRequestPathUtilsTests { + + @Test + void parseAndCache() { + // basic + testParseAndCache("/app/servlet/a/b/c", "/app", "/servlet", "/a/b/c"); + + // contextPath only, servletPathOnly, contextPath and servletPathOnly + testParseAndCache("/app/a/b/c", "/app", "", "/a/b/c"); + testParseAndCache("/servlet/a/b/c", "", "/servlet", "/a/b/c"); + testParseAndCache("/app1/app2/servlet1/servlet2", "/app1/app2", "/servlet1/servlet2", ""); + + // trailing slash + testParseAndCache("/app/servlet/a/", "/app", "/servlet", "/a/"); + testParseAndCache("/app/servlet/a//", "/app", "/servlet", "/a//"); + } + + private void testParseAndCache( + String requestUri, String contextPath, String servletPath, String pathWithinApplication) { + + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setContextPath(contextPath); + request.setServletPath(servletPath); + request.setHttpServletMapping(new MockHttpServletMapping( + pathWithinApplication, contextPath, "myServlet", MappingMatch.PATH)); + + RequestPath requestPath = ServletRequestPathUtils.parseAndCache(request); + + assertThat(requestPath.contextPath().value()).isEqualTo(contextPath); + assertThat(requestPath.pathWithinApplication().value()).isEqualTo(pathWithinApplication); + } + +}