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 279ceaae7497..74be8b250463 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 @@ -16,11 +16,16 @@ package org.springframework.web.util; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletMapping; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.MappingMatch; @@ -110,6 +115,14 @@ public static void clearParsedRequestPath(ServletRequest request) { request.removeAttribute(PATH_ATTRIBUTE); } + /** + * Provide a {@link Filter} that manages the parsing and caching of a {@link RequestPath}. + * @return the described {@link Filter} + * @since 6.2.3 + */ + public static Filter getParsedRequestPathCacheFilter() { + return new RequestPathCacheFilter(); + } // Methods to select either parsed RequestPath or resolved String lookupPath @@ -312,4 +325,20 @@ PathElements withContextPath(String contextPath) { } } + private static final class RequestPathCacheFilter implements Filter { + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + if (!(req instanceof HttpServletRequest request)) { + chain.doFilter(req, res); + return; + } + RequestPath previousRequestPath = (RequestPath) request.getAttribute(PATH_ATTRIBUTE); + try { + parseAndCache(request); + chain.doFilter(req, res); + } + finally { + setParsedRequestPath(previousRequestPath, request); + } + } + } } 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 index 90a06875f750..7a2fc4d63e3c 100644 --- a/spring-web/src/test/java/org/springframework/web/util/ServletRequestPathUtilsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/ServletRequestPathUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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,12 @@ package org.springframework.web.util; +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletMapping; import jakarta.servlet.http.MappingMatch; import org.junit.jupiter.api.Test; @@ -88,6 +94,35 @@ void modifyPathContextWithContextPathEndingWithSlash() { .withMessage("Invalid contextPath '/persons/': must start with '/' and not end with '/'"); } + @Test + void filterParsesAndCachesThenCleansUp() throws IOException, ServletException { + MockHttpServletRequest request = createRequest("/servlet/request", "", "/servlet", "/request"); + Filter filter = ServletRequestPathUtils.getParsedRequestPathCacheFilter(); + filter.doFilter(request, null, (req, res) -> { + RequestPath currentPath = ServletRequestPathUtils.getParsedRequestPath(request); + assertThat(currentPath.pathWithinApplication().value()).isEqualTo("/request"); + }); + assertThat(ServletRequestPathUtils.hasParsedRequestPath(request)).isFalse(); + } + + @Test + void filterParsesCachesAndThenRestores() throws IOException, ServletException { + MockHttpServletRequest request = createRequest("/servlet/request", "", "/servlet", "/request"); + RequestPath requestPath = ServletRequestPathUtils.parseAndCache(request); + + HttpServletMapping mapping = new MockHttpServletMapping("/include", "", "myServlet", MappingMatch.PATH); + request.setAttribute(RequestDispatcher.INCLUDE_MAPPING, mapping); + request.setAttribute(WebUtils.INCLUDE_SERVLET_PATH_ATTRIBUTE, "/servlet"); + request.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE, "/servlet/include"); + + Filter filter = ServletRequestPathUtils.getParsedRequestPathCacheFilter(); + filter.doFilter(request, null, (req, res) -> { + RequestPath currentPath = ServletRequestPathUtils.getParsedRequestPath(request); + assertThat(currentPath.pathWithinApplication().value()).isEqualTo("/include"); + }); + assertThat(requestPath).isEqualTo(ServletRequestPathUtils.getParsedRequestPath(request)); + } + private void testParseAndCache( String requestUri, String contextPath, String servletPath, String pathWithinApplication) { @@ -100,13 +135,19 @@ private void testParseAndCache( private static RequestPath createRequestPath( String requestUri, String contextPath, String servletPath, String pathWithinApplication) { + MockHttpServletRequest request = createRequest(requestUri, contextPath, servletPath, pathWithinApplication); + return ServletRequestPathUtils.parseAndCache(request); + } + + private static MockHttpServletRequest createRequest( + 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)); - - return ServletRequestPathUtils.parseAndCache(request); + return request; } }