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);
+ }
+
+}