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 fb9cb372694d..7023ef56459d 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 @@ -100,6 +100,14 @@ public interface HandlerMapping { */ String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern"; + /** + * Name of the {@link HttpServletRequest} attribute that contains the best + * matching {@link org.springframework.web.util.pattern.PathPattern}, when + * parsed patterns are in use. + * @since 7.1 + */ + String BEST_MATCHING_PATH_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPathPattern"; + /** * Name of the boolean {@link HttpServletRequest} attribute that indicates * whether type-level mappings should be inspected. 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 5b85bb09f0e8..5b477dc5c04d 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 @@ -221,6 +221,7 @@ private void setAttributes(HttpServletRequest servletRequest, ServerRequest requ if (matchingPattern != null) { servletRequest.removeAttribute(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE); servletRequest.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, matchingPattern.getPatternString()); + servletRequest.setAttribute(BEST_MATCHING_PATH_PATTERN_ATTRIBUTE, matchingPattern); ServerHttpObservationFilter.findObservationContext(request.servletRequest()) .ifPresent(context -> context.setPathPattern(matchingPattern.getPatternString())); } 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 dfb8060c7925..6789592cc34e 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 @@ -319,7 +319,7 @@ private String getHandlerDescription(Object handler) { pathWithinMapping = UrlPathHelper.defaultInstance.removeSemicolonContent(pathWithinMapping); PathPattern.PathMatchInfo pathMatchInfo = pattern.matchAndExtract(path); Map uriVariables = (pathMatchInfo != null ? pathMatchInfo.getUriVariables(): null); - return buildPathExposingHandler(handler, pattern.getPatternString(), pathWithinMapping, uriVariables); + return buildPathExposingHandler(handler, pattern, pathWithinMapping, uriVariables); } /** @@ -419,12 +419,14 @@ protected void validateHandler(Object handler, HttpServletRequest request) throw /** * Build a handler object for the given raw handler, exposing the actual - * handler, the {@link #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE}, as well as + * handler, the {@link #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE}, + * the {@link #BEST_MATCHING_PATTERN_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 * @param rawHandler the raw handler to expose + * @param bestMatchingPattern the {@linkplain HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE best matching pattern} * @param pathWithinMapping the path to expose before executing the handler * @param uriTemplateVariables the URI template variables, can be {@code null} if no variables found * @return the final handler object @@ -440,8 +442,33 @@ protected Object buildPathExposingHandler(Object rawHandler, String bestMatching return chain; } + /** + * Build a handler object for the given raw handler, exposing the actual + * handler, the {@link #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE}, + * the {@link #BEST_MATCHING_PATH_PATTERN_ATTRIBUTE} + * the {@link #BEST_MATCHING_PATTERN_ATTRIBUTE}, as well as + * the {@link #URI_TEMPLATE_VARIABLES_ATTRIBUTE} before executing the handler. + * @param rawHandler the raw handler to expose + * @param bestMatchingPattern the {@linkplain HandlerMapping#BEST_MATCHING_PATH_PATTERN_ATTRIBUTE best matching pattern} + * @param pathWithinMapping the path to expose before executing the handler + * @param uriTemplateVariables the URI template variables, can be {@code null} if no variables found + * @return the final handler object + * @since 7.1 + */ + protected Object buildPathExposingHandler(Object rawHandler, PathPattern bestMatchingPattern, + String pathWithinMapping, @Nullable Map uriTemplateVariables) { + + HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler); + chain.addInterceptor(new PathExposingHandlerInterceptor(bestMatchingPattern, pathWithinMapping)); + if (!CollectionUtils.isEmpty(uriTemplateVariables)) { + chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables)); + } + return chain; + } + /** * Expose the path within the current mapping as request attribute. + * @param bestMatchingPattern the best matching pattern * @param pathWithinMapping the path within the current mapping * @param request the request to expose the path to * @see #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE @@ -455,6 +482,22 @@ protected void exposePathWithinMapping(String bestMatchingPattern, String pathWi request.setAttribute(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping); } + /** + * Expose the path within the current mapping as request attribute. + * @param bestMatchingPattern the best matching pattern + * @param pathWithinMapping the path within the current mapping + * @param request the request to expose the path to + * @since 7.1 + * @see #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + * @see #BEST_MATCHING_PATH_PATTERN_ATTRIBUTE + */ + protected void exposePathWithinMapping(PathPattern bestMatchingPattern, String pathWithinMapping, + HttpServletRequest request) { + + request.setAttribute(BEST_MATCHING_PATH_PATTERN_ATTRIBUTE, bestMatchingPattern); + exposePathWithinMapping(bestMatchingPattern.getPatternString(), pathWithinMapping, request); + } + /** * Expose the URI templates variables as request attribute. * @param uriTemplateVariables the URI template variables @@ -495,23 +538,39 @@ protected boolean supportsTypeLevelMappings() { /** * Special interceptor for exposing the - * {@link AbstractUrlHandlerMapping#PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE} attribute. + * {@link AbstractUrlHandlerMapping#PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE} and, + * when parsed patterns are in use, the + * {@link AbstractUrlHandlerMapping#BEST_MATCHING_PATH_PATTERN_ATTRIBUTE} attribute. * @see AbstractUrlHandlerMapping#exposePathWithinMapping */ private class PathExposingHandlerInterceptor implements HandlerInterceptor { private final String bestMatchingPattern; + private final @Nullable PathPattern bestMatchingPathPattern; + private final String pathWithinMapping; public PathExposingHandlerInterceptor(String bestMatchingPattern, String pathWithinMapping) { this.bestMatchingPattern = bestMatchingPattern; + this.bestMatchingPathPattern = null; + this.pathWithinMapping = pathWithinMapping; + } + + public PathExposingHandlerInterceptor(PathPattern bestMatchingPattern, String pathWithinMapping) { + this.bestMatchingPattern = bestMatchingPattern.getPatternString(); + this.bestMatchingPathPattern = bestMatchingPattern; this.pathWithinMapping = pathWithinMapping; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - exposePathWithinMapping(this.bestMatchingPattern, this.pathWithinMapping, request); + if (this.bestMatchingPathPattern != null) { + exposePathWithinMapping(this.bestMatchingPathPattern, this.pathWithinMapping, request); + } + else { + exposePathWithinMapping(this.bestMatchingPattern, this.pathWithinMapping, request); + } request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, handler); request.setAttribute(INTROSPECT_TYPE_LEVEL_MAPPING, supportsTypeLevelMappings()); return true; 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 0b5e366c0cb1..7005909f9770 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 @@ -171,6 +171,7 @@ private void extractMatchDetails( uriVariables = result.getUriVariables(); request.setAttribute(MATRIX_VARIABLES_ATTRIBUTE, result.getMatrixVariables()); } + request.setAttribute(BEST_MATCHING_PATH_PATTERN_ATTRIBUTE, bestPattern); request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern.getPatternString()); ServerHttpObservationFilter.findObservationContext(request) .ifPresent(context -> context.setPathPattern(bestPattern.getPatternString())); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/RouterFunctionMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/RouterFunctionMappingTests.java index 4428e38c5367..383ae9c2ba03 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/RouterFunctionMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/RouterFunctionMappingTests.java @@ -37,6 +37,7 @@ import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; 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; @@ -174,6 +175,9 @@ void mappedRequestShouldHoldAttributes() throws Exception { assertThat(result).isNotNull(); assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)).isEqualTo("/match"); + assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_PATH_PATTERN_ATTRIBUTE)) + .isInstanceOfSatisfying(PathPattern.class, + pattern -> assertThat(pattern.getPatternString()).isEqualTo("/match")); assertThat(ServerHttpObservationFilter.findObservationContext(request)) .hasValueSatisfying(context -> assertThat(context.getPathPattern()).isEqualTo("/match")); assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE)).isEqualTo(handlerFunction); 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 79cfd3748795..8f535095aa78 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 @@ -42,12 +42,15 @@ import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.UrlPathHelper; import org.springframework.web.util.WebUtils; +import org.springframework.web.util.pattern.PathPattern; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Named.named; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE; +import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_PATH_PATTERN_ATTRIBUTE; +import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE; import static org.springframework.web.servlet.HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE; import static org.springframework.web.servlet.HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; @@ -99,6 +102,30 @@ void resolveFromMap(SimpleUrlHandlerMapping handlerMapping) throws Exception { assertThat(request.getAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE)).isEqualTo(mainController); } + @HandlerMappingsTest + void resolveBestMatchingPathPatternAttribute(SimpleUrlHandlerMapping handlerMapping) throws Exception { + StaticApplicationContext applicationContext = new StaticApplicationContext(); + applicationContext.registerSingleton("mainController", Object.class); + Object mainController = applicationContext.getBean("mainController"); + handlerMapping.setUrlMap(Map.of("/welcome*", "mainController")); + handlerMapping.setApplicationContext(applicationContext); + + boolean usePathPatterns = handlerMapping.getPatternParser() != null; + MockHttpServletRequest request = PathPatternsTestUtils.initRequest("GET", "/welcome.x", usePathPatterns); + HandlerExecutionChain chain = getHandler(handlerMapping, request); + + assertThat(chain.getHandler()).isSameAs(mainController); + assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).isEqualTo("/welcome*"); + if (usePathPatterns) { + assertThat(request.getAttribute(BEST_MATCHING_PATH_PATTERN_ATTRIBUTE)) + .isInstanceOfSatisfying(PathPattern.class, + pattern -> assertThat(pattern.getPatternString()).isEqualTo("/welcome*")); + } + else { + assertThat(request.getAttribute(BEST_MATCHING_PATH_PATTERN_ATTRIBUTE)).isNull(); + } + } + @HandlerMappingsTest void resolvePatternFromMap(SimpleUrlHandlerMapping handlerMapping) throws Exception { StaticApplicationContext applicationContext = new StaticApplicationContext(); 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 d2937784e700..ded635c9b0fa 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 @@ -62,6 +62,7 @@ import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.pattern.PathPattern; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -315,6 +316,22 @@ void handleMatchBestMatchingPatternAttribute(TestRequestMappingInfoHandlerMappin mapping.handleMatch(info, "/1/2", request); assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)).isEqualTo("/{path1}/2"); + assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_PATH_PATTERN_ATTRIBUTE)).isNull(); + } + + @Test + void handleMatchBestMatchingPathPatternAttribute() { + TestRequestMappingInfoHandlerMapping mapping = new TestRequestMappingInfoHandlerMapping(); + RequestMappingInfo info = mapping.createInfo("/{path1}/2", "/**"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2"); + ServletRequestPathUtils.parseAndCache(request); + + mapping.handleMatch(info, "/1/2", request); + + assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)).isEqualTo("/{path1}/2"); + assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_PATH_PATTERN_ATTRIBUTE)) + .isInstanceOfSatisfying(PathPattern.class, + pattern -> assertThat(pattern.getPatternString()).isEqualTo("/{path1}/2")); } @SuppressWarnings("removal")