From a8273a30091cb10d6bbe0e8c41834f0b0ad129d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 5 Jan 2024 16:04:14 +0100 Subject: [PATCH] Resolve property placeholder in RequestMapping if necessary This commit makes sure to resolve placeholders in request mappings using the EmbeddedValueResolver of the current WebApplicationContext. To avoid retrieving the context too often, we check for the presence of the standard placeholder prefix. Closes gh-26795 --- .../annotation/MvcUriComponentsBuilder.java | 35 +++++++---- .../MvcUriComponentsBuilderTests.java | 60 ++++++++++++++++++- 2 files changed, 83 insertions(+), 12 deletions(-) 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 04044feca2..ea2e301068 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,6 +30,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.cglib.core.SpringNamingPolicy; import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.Enhancer; @@ -52,6 +53,7 @@ import org.springframework.util.PathMatcher; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.util.StringUtils; +import org.springframework.util.SystemPropertyUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestAttributes; @@ -582,14 +584,7 @@ public class MvcUriComponentsBuilder { if (mapping == null) { return ""; } - String[] paths = mapping.path(); - if (ObjectUtils.isEmpty(paths) || !StringUtils.hasLength(paths[0])) { - return ""; - } - if (paths.length > 1 && logger.isTraceEnabled()) { - logger.trace("Using first of multiple paths on " + controllerType.getName()); - } - return paths[0]; + return getPathMapping(mapping, controllerType.getName()); } private static String getMethodMapping(Method method) { @@ -598,14 +593,18 @@ public class MvcUriComponentsBuilder { if (requestMapping == null) { throw new IllegalArgumentException("No @RequestMapping on: " + method.toGenericString()); } + return getPathMapping(requestMapping, method.toGenericString()); + } + + private static String getPathMapping(RequestMapping requestMapping, String source) { String[] paths = requestMapping.path(); if (ObjectUtils.isEmpty(paths) || !StringUtils.hasLength(paths[0])) { return ""; } if (paths.length > 1 && logger.isTraceEnabled()) { - logger.trace("Using first of multiple paths on " + method.toGenericString()); + logger.trace("Using first of multiple paths on " + source); } - return paths[0]; + return resolveEmbeddedValue(paths[0]); } private static Method getMethod(Class controllerType, final String methodName, final Object... args) { @@ -663,6 +662,20 @@ public class MvcUriComponentsBuilder { return defaultUriComponentsContributor; } + private static String resolveEmbeddedValue(String value) { + if (value.contains(SystemPropertyUtils.PLACEHOLDER_PREFIX)) { + WebApplicationContext webApplicationContext = getWebApplicationContext(); + if (webApplicationContext != null + && webApplicationContext.getAutowireCapableBeanFactory() instanceof ConfigurableBeanFactory cbf) { + String resolvedEmbeddedValue = cbf.resolveEmbeddedValue(value); + if (resolvedEmbeddedValue != null) { + return resolvedEmbeddedValue; + } + } + } + return value; + } + @Nullable private static WebApplicationContext getWebApplicationContext() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java index f4206144ef..b997908904 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2002-2024 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,6 +25,7 @@ import java.sql.Savepoint; import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Optional; import jakarta.servlet.http.HttpServletRequest; @@ -34,10 +35,14 @@ import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Bean; import org.springframework.core.annotation.AliasFor; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.StandardEnvironment; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.http.HttpEntity; import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Controller; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; @@ -146,6 +151,31 @@ public class MvcUriComponentsBuilderTests { assertThat(builder.toUriString()).isEqualTo("https://example.org:9090/base"); } + @Test + public void fromControllerWithPlaceholder() { + StandardEnvironment environment = new StandardEnvironment(); + environment.getPropertySources().addFirst(new MapPropertySource("test", + Map.of("context.test.mapping", "people"))); + initWebApplicationContext(WebConfig.class, environment); + UriComponents uriComponents = fromController(ConfigurablePersonController.class).build(); + assertThat(uriComponents.toUriString()).endsWith("/people"); + } + + @Test + public void fromControllerWithPlaceholderAndMissingValue() { + StandardEnvironment environment = new StandardEnvironment(); + assertThat(environment.containsProperty("context.test.mapping")).isFalse(); + initWebApplicationContext(WebConfig.class, environment); + UriComponents uriComponents = fromController(ConfigurablePersonController.class).build(); + assertThat(uriComponents.toUriString()).endsWith("/${context.test.mapping}"); + } + + @Test + public void fromControllerWithPlaceholderAndNoValueResolver() { + UriComponents uriComponents = fromController(ConfigurablePersonController.class).build(); + assertThat(uriComponents.toUriString()).endsWith("/${context.test.mapping}"); + } + @Test public void usesForwardedHostAsHostIfHeaderIsSet() throws Exception { this.request.setScheme("https"); @@ -293,6 +323,17 @@ public class MvcUriComponentsBuilderTests { assertThat(uriComponents.toUriString()).isEqualTo("http://localhost/input"); } + @Test + public void fromMethodNameConfigurablePath() { + StandardEnvironment environment = new StandardEnvironment(); + environment.getPropertySources().addFirst(new MapPropertySource("test", + Map.of("method.test.mapping", "custom"))); + initWebApplicationContext(WebConfig.class, environment); + UriComponents uriComponents = fromMethodName(ControllerWithMethods.class, + "methodWithConfigurableMapping", "1").build(); + assertThat(uriComponents.toUriString()).isEqualTo("http://localhost/something/custom/1/foo"); + } + @Test public void fromMethodCallOnSubclass() { UriComponents uriComponents = fromMethodCall(on(ExtendedController.class).myMethod(null)).build(); @@ -522,7 +563,14 @@ public class MvcUriComponentsBuilderTests { } private void initWebApplicationContext(Class configClass) { + initWebApplicationContext(configClass, null); + } + + private void initWebApplicationContext(Class configClass, @Nullable ConfigurableEnvironment environment) { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + if (environment != null) { + context.setEnvironment(environment); + } context.setServletContext(new MockServletContext()); context.register(configClass); context.refresh(); @@ -574,6 +622,11 @@ public class MvcUriComponentsBuilderTests { } + @RequestMapping("/${context.test.mapping}") + interface ConfigurablePersonController { + } + + private class UnmappedController { @RequestMapping @@ -636,6 +689,11 @@ public class MvcUriComponentsBuilderTests { HttpEntity methodWithOptionalNamedParam(@RequestParam("search") Optional q) { return null; } + + @RequestMapping("/${method.test.mapping}/{id}/foo") + HttpEntity methodWithConfigurableMapping(@PathVariable String id) { + return null; + } }