Skip to content

Commit

Permalink
Resolve property placeholder in RequestMapping if necessary
Browse files Browse the repository at this point in the history
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 spring-projectsgh-26795
  • Loading branch information
snicoll committed Jan 5, 2024
1 parent a108e70 commit 09717f7
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -574,6 +607,11 @@ private class InvalidController {
}


@RequestMapping("/${context.test.mapping}")
interface ConfigurablePersonController {
}


private class UnmappedController {

@RequestMapping
Expand Down Expand Up @@ -636,6 +674,11 @@ HttpEntity<Void> methodWithOptionalParam(@RequestParam(defaultValue = "") String
HttpEntity<Void> methodWithOptionalNamedParam(@RequestParam("search") Optional<String> q) {
return null;
}

@RequestMapping("/${method.test.mapping}/{id}/foo")
HttpEntity<Void> methodWithConfigurableMapping(@PathVariable String id) {
return null;
}
}


Expand Down

0 comments on commit 09717f7

Please sign in to comment.