From 09717f7cccbce9a26c24040880c4bf79e7a4e534 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 | 33 +++++++++----- .../MvcUriComponentsBuilderTests.java | 45 ++++++++++++++++++- 2 files changed, 66 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 04044feca295..4939bb9d5def 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.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.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 @@ private static String getClassMapping(Class controllerType) { 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 @@ private static String getMethodMapping(Method method) { 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,18 @@ private static CompositeUriComponentsContributor getUriComponentsContributor() { return defaultUriComponentsContributor; } + private static String resolveEmbeddedValue(String value) { + if (!value.contains(SystemPropertyUtils.PLACEHOLDER_PREFIX)) { + return value; + } + WebApplicationContext webApplicationContext = getWebApplicationContext(); + if (webApplicationContext != null + && webApplicationContext.getAutowireCapableBeanFactory() instanceof ConfigurableBeanFactory cbf) { + return cbf.resolveEmbeddedValue(value); + } + 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 f4206144ef5b..9cde7b75dffa 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.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.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,16 @@ public void fromControllerWithCustomBaseUrlViaInstance() { 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 usesForwardedHostAsHostIfHeaderIsSet() throws Exception { this.request.setScheme("https"); @@ -293,6 +308,17 @@ public void fromMethodNameWithMetaAnnotation() { 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 +548,14 @@ public void fromMethodWithPrefix() { } 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 +607,11 @@ private class InvalidController { } + @RequestMapping("/${context.test.mapping}") + interface ConfigurablePersonController { + } + + private class UnmappedController { @RequestMapping @@ -636,6 +674,11 @@ HttpEntity methodWithOptionalParam(@RequestParam(defaultValue = "") String HttpEntity methodWithOptionalNamedParam(@RequestParam("search") Optional q) { return null; } + + @RequestMapping("/${method.test.mapping}/{id}/foo") + HttpEntity methodWithConfigurableMapping(@PathVariable String id) { + return null; + } }