Skip to content

Issue #361: Add Support for Resolving Property Values #391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertyResolver;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
* {@link MappingDiscoverer} implementation that inspects mappings from a particular annotation.
*
* @author Oliver Gierke
* @author Josh Ghiloni
*/
public class AnnotationMappingDiscoverer implements MappingDiscoverer {

Expand Down Expand Up @@ -62,6 +66,10 @@ public AnnotationMappingDiscoverer(Class<? extends Annotation> annotation, Strin
*/
@Override
public String getMapping(Class<?> type) {
return getMapping(null, type);
}

public String getMapping(PropertyResolver resolver, Class<?> type) {

Assert.notNull(type, "Type must not be null!");

Expand All @@ -72,7 +80,7 @@ public String getMapping(Class<?> type) {
type.getName()));
}

return mapping.length == 0 ? null : mapping[0];
return mapping.length == 0 ? null : maybeResolveValue(resolver, mapping[0]);
}

/*
Expand All @@ -81,9 +89,12 @@ public String getMapping(Class<?> type) {
*/
@Override
public String getMapping(Method method) {

return getMapping(method, null);
}

public String getMapping(Method method, PropertyResolver resolver) {
Assert.notNull(method, "Method must not be null!");
return getMapping(method.getDeclaringClass(), method);
return getMapping(method.getDeclaringClass(), method, resolver);
}

/*
Expand All @@ -92,6 +103,10 @@ public String getMapping(Method method) {
*/
@Override
public String getMapping(Class<?> type, Method method) {
return getMapping(type, method, null);
}

public String getMapping(Class<?> type, Method method, PropertyResolver resolver) {

Assert.notNull(type, "Type must not be null!");
Assert.notNull(method, "Method must not be null!");
Expand All @@ -109,7 +124,8 @@ public String getMapping(Class<?> type, Method method) {
return typeMapping;
}

return typeMapping == null || "/".equals(typeMapping) ? mapping[0] : typeMapping + mapping[0];
String returnValue = (typeMapping == null || "/".equals(typeMapping) ? mapping[0] : typeMapping + mapping[0]);
return maybeResolveValue(resolver, returnValue);
}

private String[] getMappingFrom(Annotation annotation) {
Expand All @@ -127,4 +143,12 @@ private String[] getMappingFrom(Annotation annotation) {
throw new IllegalStateException(String.format(
"Unsupported type for the mapping attribute! Support String and String[] but got %s!", value.getClass()));
}

private String maybeResolveValue(PropertyResolver resolver, String value) {
if (resolver != null && StringUtils.hasText(value)) {
value = resolver.resolvePlaceholders(value);
}

return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import javax.servlet.http.HttpServletRequest;

import org.springframework.core.env.PropertyResolver;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.core.AnnotationMappingDiscoverer;
import org.springframework.hateoas.core.DummyInvocationUtils;
Expand All @@ -43,6 +44,7 @@
* @author Oliver Gierke
* @author Kamill Sokol
* @author Greg Turnquist
* @author Josh Ghiloni
*/
public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuilder> {

Expand All @@ -66,7 +68,18 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
* @return
*/
public static ControllerLinkBuilder linkTo(Class<?> controller) {
return linkTo(controller, new Object[0]);
return linkTo(null, controller, new Object[0]);
}

/**
* Creates a new {@link ControllerLinkBuilder} with a base of the mapping annotated to the given controller class.
*
* @param env If not {@literal null}, it will attempt to resolve placeholders in mappings
* @param controller the class to discover the annotation on, must not be {@literal null}.
* @return
*/
public static ControllerLinkBuilder linkTo(PropertyResolver resolver, Class<?> controller) {
return linkTo(resolver, controller, new Object[0]);
}

/**
Expand All @@ -79,11 +92,26 @@ public static ControllerLinkBuilder linkTo(Class<?> controller) {
* @return
*/
public static ControllerLinkBuilder linkTo(Class<?> controller, Object... parameters) {
return linkTo(null, controller, parameters);
}

/**
* Creates a new {@link ControllerLinkBuilder} with a base of the mapping annotated to the given controller class. The
* additional parameters are used to fill up potentially available path variables in the class scop request mapping.
*
* If the env parameter is non-null, it will attempt to resolve any placeholders found in mappings
*
* @param env
* @param controller
* @param parameters
* @return
*/
public static ControllerLinkBuilder linkTo(PropertyResolver resolver, Class<?> controller, Object... parameters) {

Assert.notNull(controller);

ControllerLinkBuilder builder = new ControllerLinkBuilder(getBuilder());
String mapping = DISCOVERER.getMapping(controller);
String mapping = ((AnnotationMappingDiscoverer)DISCOVERER).getMapping(resolver, controller);

UriComponents uriComponents = UriComponentsBuilder.fromUriString(mapping == null ? "/" : mapping).build();
UriComponents expandedComponents = uriComponents.expand(parameters);
Expand All @@ -95,18 +123,49 @@ public static ControllerLinkBuilder linkTo(Class<?> controller, Object... parame
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(Method, Object...)
*/
public static ControllerLinkBuilder linkTo(Method method, Object... parameters) {
return linkTo(method.getDeclaringClass(), method, parameters);
return linkTo(null, method.getDeclaringClass(), method, parameters);
}

/**
* Similar to {@link org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(Method, Object...)}
* but takes an optional {@link PropertyResolver} parameter. If it is non-null, it will attempt to resolve
* any placeholders in requestMappings that were found.
*
* @param resolver
* @param method
* @param parameters
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(Method, Object...)
* @return
*/
public static ControllerLinkBuilder linkTo(PropertyResolver resolver, Method method, Object... parameters) {
return linkTo(resolver, method.getDeclaringClass(), method, parameters);
}

/*
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(Class<?>, Method, Object...)
*/
public static ControllerLinkBuilder linkTo(Class<?> controller, Method method, Object... parameters) {
return linkTo(null, controller, method, parameters);
}

/**
* Similar to {@link org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(Class<?>, Method, Object...)}
* but takes an optional {@link PropertyResolver} parameter. If it is non-null, it will attempt to resolve
* any placeholders in requestMappings that were found.
*
* @param resolver
* @param controller
* @param method
* @param parameters
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(Class<?>, Method, Object...)
* @return
*/
public static ControllerLinkBuilder linkTo(PropertyResolver resolver, Class<?> controller, Method method, Object... parameters) {

Assert.notNull(controller, "Controller type must not be null!");
Assert.notNull(method, "Method must not be null!");

UriTemplate template = new UriTemplate(DISCOVERER.getMapping(controller, method));
UriTemplate template = new UriTemplate(((AnnotationMappingDiscoverer)DISCOVERER).getMapping(controller, method, resolver));
URI uri = template.expand(parameters);

return new ControllerLinkBuilder(getBuilder()).slash(uri);
Expand Down Expand Up @@ -135,7 +194,37 @@ public static ControllerLinkBuilder linkTo(Class<?> controller, Method method, O
* @return
*/
public static ControllerLinkBuilder linkTo(Object invocationValue) {
return FACTORY.linkTo(invocationValue);
return FACTORY.linkTo(null, invocationValue);
}

/**
* Creates a {@link ControllerLinkBuilder} pointing to a controller method. Hand in a dummy method invocation result
* you can create via {@link #methodOn(Class, Object...)} or {@link DummyInvocationUtils#methodOn(Class, Object...)}.
*
* <pre>
* @RequestMapping("/customers")
* class CustomerController {
*
* @RequestMapping("/{id}/addresses")
* HttpEntity&lt;Addresses&gt; showAddresses(@PathVariable Long id) { … }
* }
*
* Link link = linkTo(methodOn(CustomerController.class).showAddresses(2L)).withRel("addresses");
* </pre>
*
* If the {@link PropertyResolver} parameter is non-null, it will attempt to resolve any placeholders
* found in the mapping
*
* The resulting {@link Link} instance will point to {@code /customers/2/addresses} and have a rel of
* {@code addresses}. For more details on the method invocation constraints, see
* {@link DummyInvocationUtils#methodOn(Class, Object...)}.
*
* @param resolver
* @param invocationValue
* @return
*/
public static ControllerLinkBuilder linkTo(PropertyResolver resolver, Object invocationValue) {
return FACTORY.linkTo(resolver, invocationValue);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.Map;

import org.springframework.core.MethodParameter;
import org.springframework.core.env.PropertyResolver;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MethodLinkBuilderFactory;
import org.springframework.hateoas.core.AnnotationAttribute;
Expand Down Expand Up @@ -84,6 +85,10 @@ public void setUriComponentsContributors(List<? extends UriComponentsContributor
public ControllerLinkBuilder linkTo(Class<?> controller) {
return ControllerLinkBuilder.linkTo(controller);
}

public ControllerLinkBuilder linkTo(PropertyResolver resolver, Class<?> controller) {
return ControllerLinkBuilder.linkTo(resolver, controller);
}

/*
* (non-Javadoc)
Expand All @@ -93,6 +98,10 @@ public ControllerLinkBuilder linkTo(Class<?> controller) {
public ControllerLinkBuilder linkTo(Class<?> controller, Object... parameters) {
return ControllerLinkBuilder.linkTo(controller, parameters);
}

public ControllerLinkBuilder linkTo(PropertyResolver resolver, Class<?> controller, Object... parameters) {
return ControllerLinkBuilder.linkTo(resolver, controller, parameters);
}

/*
* (non-Javadoc)
Expand All @@ -102,13 +111,21 @@ public ControllerLinkBuilder linkTo(Class<?> controller, Object... parameters) {
public ControllerLinkBuilder linkTo(Class<?> controller, Method method, Object... parameters) {
return ControllerLinkBuilder.linkTo(controller, method, parameters);
}

public ControllerLinkBuilder linkTo(PropertyResolver resolver, Class<?> controller, Method method, Object... parameters) {
return ControllerLinkBuilder.linkTo(resolver, controller, method, parameters);
}

/*
* (non-Javadoc)
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(java.lang.Object)
*/
@Override
public ControllerLinkBuilder linkTo(Object invocationValue) {
return linkTo(null, invocationValue);
}

public ControllerLinkBuilder linkTo(PropertyResolver resolver, Object invocationValue) {

Assert.isInstanceOf(LastInvocationAware.class, invocationValue);
LastInvocationAware invocations = (LastInvocationAware) invocationValue;
Expand All @@ -117,7 +134,7 @@ public ControllerLinkBuilder linkTo(Object invocationValue) {
Iterator<Object> classMappingParameters = invocations.getObjectParameters();
Method method = invocation.getMethod();

String mapping = DISCOVERER.getMapping(invocation.getTargetType(), method);
String mapping = ((AnnotationMappingDiscoverer)DISCOVERER).getMapping(invocation.getTargetType(), method, resolver);
UriComponentsBuilder builder = ControllerLinkBuilder.getBuilder().path(mapping);

UriTemplate template = new UriTemplate(mapping);
Expand Down Expand Up @@ -148,6 +165,10 @@ public ControllerLinkBuilder linkTo(Object invocationValue) {
public ControllerLinkBuilder linkTo(Method method, Object... parameters) {
return ControllerLinkBuilder.linkTo(method, parameters);
}

public ControllerLinkBuilder linkTo(PropertyResolver resolver, Method method, Object... parameters) {
return ControllerLinkBuilder.linkTo(resolver, method, parameters);
}

/**
* Applies the configured {@link UriComponentsContributor}s to the given {@link UriComponentsBuilder}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@
import static org.junit.Assert.*;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import org.junit.Test;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.web.bind.annotation.RequestMapping;

/**
Expand Down Expand Up @@ -96,6 +100,34 @@ public void includesTypeMappingFromChildClass() throws Exception {
Method method = ParentWithMethod.class.getMethod("mapping");
assertThat(discoverer.getMapping(ChildWithTypeMapping.class, method), is("/child/parent"));
}

/**
* @see #361
*/
@Test
public void resolvesVariablesInMappings() throws Exception {
Map<String, Object> source = new HashMap<String, Object>();
source.put("test.variable", "/dynamicparent");
source.put("test.child", "/dynamicchild");

StandardEnvironment env = new StandardEnvironment();
env.getPropertySources().addLast(new MapPropertySource("mapping-env", source));

Method method = DynamicEndpointControllerWithMethod.class.getMethod("method");

// test regression first
assertThat(discoverer.getMapping(DynamicEndpointController.class), is("${test.variable}"));
assertThat(discoverer.getMapping(DynamicEndpointControllerWithMethod.class, method),
is("${test.variable}${test.child}"));

// test new feature
AnnotationMappingDiscoverer concreteDiscoverer = (AnnotationMappingDiscoverer)discoverer;
assertThat(concreteDiscoverer.getMapping(env, DynamicEndpointController.class),
is("/dynamicparent"));
assertThat(concreteDiscoverer.getMapping(method, env), is("/dynamicparent/dynamicchild"));
assertThat(concreteDiscoverer.getMapping(DynamicEndpointControllerWithMethod.class, method, env),
is("/dynamicparent/dynamicchild"));
}

@RequestMapping("/type")
interface MyController {
Expand Down Expand Up @@ -139,4 +171,13 @@ interface ParentWithMethod {

@RequestMapping("/child")
interface ChildWithTypeMapping extends ParentWithMethod {}

@RequestMapping("${test.variable}")
interface DynamicEndpointController {}

@RequestMapping("${test.variable}")
interface DynamicEndpointControllerWithMethod {
@RequestMapping("${test.child}")
void method();
}
}
Loading