#18 - Added support for creating links from method mappings.

ControllerLinkBuilder(Factory) now has a linkTo(Method method, Object… parameters) and linkTo(Object dummyMethodInvocationResult) method to either inspect the given method or the result of the dummy method invocation that can be created through DummyInvocationUtils.methodOn(…). So for the following controller:

@Controller
@RequestMapping("/people")
class PersonController {
  
  @RequestMapping(value = "/{person}", method = RequestMethod.GET)
  public HttpEntity<PersonResource> show(@PathVariable Long person) { … }
}

you could now do:

Link link = linkTo(methodOn(PersonController.class).show(2L)).withSelfRel();
assertThat(link.getHref(), is("/people/2")));

The code is highly inspired by the code Dietrich Schulten (@dschulten) provided in his pull requests but radically reduced to the core functionality. I also didn't add support for JAX-RS in the first place, but this is definitely a topic going forward.
This commit is contained in:
Oliver Gierke
2012-12-19 13:10:12 +01:00
parent 220366b240
commit 531dd8f5ed
15 changed files with 487 additions and 123 deletions

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas;
import java.lang.reflect.Method;
import org.springframework.hateoas.core.DummyInvocationUtils;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
/**
* Extension of {@link LinkBuilderFactory} for implementations that also support creating {@link LinkBuilder}s by
* pointing to a method.
*
* @author Oliver Gierke
*/
public interface MethodLinkBuilderFactory<T extends LinkBuilder> extends LinkBuilderFactory<T> {
/**
* Returns a {@link LinkBuilder} pointing to the URI mapped to the given {@link Method} and expanding this mapping
* using the given parameters.
*
* @param method must not be {@literal null}.
* @param parameters
* @return
*/
T linkTo(Method method, Object... parameters);
/**
* Returns a {@link LinkBuilder} pointing to the URI mapped to the method the result is handed into this method. Use
* {@link DummyInvocationUtils#methodOn(Class, Object...)} to obtain a dummy instance of a controller to record a dummy
* method invocation on. See {@link ControllerLinkBuilder#linkTo(Object)} for an example.
*
* @see ControllerLinkBuilder#linkTo(Object)
* @param methodInvocationResult must not be {@literal null}.
* @return
*/
T linkTo(Object methodInvocationResult);
}

View File

@@ -23,6 +23,8 @@ import java.lang.reflect.Method;
import org.springframework.util.Assert;
/**
* {@link MappingDiscoverer} implementation that inspects mappings from a particular annotation.
*
* @author Oliver Gierke
*/
public class AnnotationMappingDiscoverer implements MappingDiscoverer {
@@ -30,10 +32,22 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer {
private final Class<? extends Annotation> annotationType;
private final String mappingAttributeName;
/**
* Creates an {@link AnnotationMappingDiscoverer} for the given annotation type. Will lookup the {@code value}
* attribute by default.
*
* @param annotation must not be {@literal null}.
*/
public AnnotationMappingDiscoverer(Class<? extends Annotation> annotation) {
this(annotation, null);
}
/**
* Creates an {@link AnnotationMappingDiscoverer} for the given annotation type and attribute name.
*
* @param annotation must not be {@literal null}.
* @param mappingAttributeName if {@literal null}, it defaults to {@code value}.
*/
public AnnotationMappingDiscoverer(Class<? extends Annotation> annotation, String mappingAttributeName) {
Assert.notNull(annotation);
@@ -42,7 +56,8 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer {
this.mappingAttributeName = mappingAttributeName;
}
/* (non-Javadoc)
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.Class)
*/
@Override
@@ -55,10 +70,11 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer {
type.getName()));
}
return mapping[0];
return mapping.length == 0 ? null : mapping[0];
}
/* (non-Javadoc)
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.reflect.Method)
*/
@Override
@@ -71,7 +87,8 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer {
method.toString()));
}
return getMapping(method.getDeclaringClass()) + mapping[0];
String typeMapping = getMapping(method.getDeclaringClass());
return typeMapping == null ? mapping[0] : typeMapping + mapping[0];
}
private String[] getMappingFrom(Annotation annotation) {
@@ -82,6 +99,8 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer {
return new String[] { (String) value };
} else if (value instanceof String[]) {
return (String[]) value;
} else if (value == null) {
return new String[0];
}
throw new IllegalStateException(String.format(

View File

@@ -20,10 +20,10 @@ import java.util.Map;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.ExposesResourceFor;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkBuilder;
import org.springframework.hateoas.LinkBuilderFactory;
import org.springframework.hateoas.ExposesResourceFor;
import org.springframework.util.Assert;
/**
@@ -35,7 +35,7 @@ import org.springframework.util.Assert;
* <li>Individual resources are exposed via a nested mapping consisting of the id of the managed entity, e.g. {@code
* @RequestMapping("/{id}")}.<li>
* </ol>
* <code>
* <pre>
* @Controller
* @ExposesResourceFor(Order.class)
* @RequestMapping("/orders")
@@ -47,7 +47,7 @@ import org.springframework.util.Assert;
* @RequestMapping("/{id}")
* ResponseEntity order(@PathVariable("id") … ) { … }
* }
* </code>
* </pre>
*
* @author Oliver Gierke
*/

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2012 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.core;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Iterator;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.target.EmptyTargetSource;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Utility methods to capture dummy method invocations.
*
* @author Oliver Gierke
*/
public class DummyInvocationUtils {
public interface LastInvocationAware {
Iterator<Object> getObjectParameters();
MethodInvocation getLastInvocation();
}
/**
* Method interceptor that records the last method invocation and creates a proxy for the return value that exposes
* the method invocation.
*
* @author Oliver Gierke
*/
private static class InvocationRecordingMethodInterceptor implements MethodInterceptor, LastInvocationAware {
private static final Method GET_INVOCATIONS;
private static final Method GET_OBJECT_PARAMETERS;
private final Object[] objectParameters;
private MethodInvocation invocation;
static {
GET_INVOCATIONS = ReflectionUtils.findMethod(LastInvocationAware.class, "getLastInvocation");
GET_OBJECT_PARAMETERS = ReflectionUtils.findMethod(LastInvocationAware.class, "getObjectParameters");
}
/**
* Creates a new {@link InvocationRecordingMethodInterceptor} carrying the given parameters forward that might be
* needed to populate the class level mapping.
*
* @param parameters
*/
public InvocationRecordingMethodInterceptor(Object... parameters) {
this.objectParameters = parameters.clone();
}
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
if (GET_INVOCATIONS.equals(method)) {
return getLastInvocation();
} else if (GET_OBJECT_PARAMETERS.equals(method)) {
return getObjectParameters();
} else if (Object.class.equals(method.getDeclaringClass())) {
return invocation.proceed();
}
this.invocation = invocation;
Class<?> returnType = method.getReturnType();
return returnType.cast(getProxyWithInterceptor(returnType, this));
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware#getLastInvocation()
*/
@Override
public MethodInvocation getLastInvocation() {
return invocation;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware#getObjectParameters()
*/
@Override
public Iterator<Object> getObjectParameters() {
return Arrays.asList(objectParameters).iterator();
}
}
/**
* Returns a proxy of the given type, backed by an {@link EmptyTargetSource} to simply drop method invocations but
* equips it with an {@link InvocationRecordingMethodInterceptor}. The interceptor records the last invocation and
* returns a proxy of the return type that also implements {@link LastInvocationAware} so that the last method
* invocation can be inspected. Parameters passed to the subsequent method invocation are generally neglected except
* the ones that might be mapped into the URI translation eventually, e.g. {@linke PathVariable} in the case of Spring
* MVC. Note, that the return types of the methods have to be capable to be proxied.
*
* @param type must not be {@literal null}.
* @param parameters parameters to extend template variables in the type level mapping.
* @return
*/
public static <T> T methodOn(Class<T> type, Object... parameters) {
Assert.notNull(type, "Given type must not be null!");
MethodInterceptor interceptor = new InvocationRecordingMethodInterceptor(parameters);
return getProxyWithInterceptor(type, interceptor);
}
@SuppressWarnings("unchecked")
private static <T> T getProxyWithInterceptor(Class<?> type, MethodInterceptor interceptor) {
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
if (type.isInterface()) {
factory.addInterface(type);
} else {
factory.setProxyTargetClass(true);
}
factory.setTargetClass(type);
factory.addInterface(LastInvocationAware.class);
factory.addAdvice(interceptor);
return (T) factory.getProxy();
}
}

View File

@@ -1,94 +0,0 @@
/*
* Copyright 2012 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.core;
import java.lang.reflect.Method;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.target.EmptyTargetSource;
import org.springframework.util.ReflectionUtils;
/**
* @author Oliver Gierke
*/
public class LinkBuilderUtils {
public interface LastInvocationAware {
MethodInvocation getLastInvocation();
}
private static class InvocationRecordingMethodInterceptor implements MethodInterceptor, LastInvocationAware {
private static final Method GET_INVOCATIONS;
private MethodInvocation invocation;
static {
GET_INVOCATIONS = ReflectionUtils.findMethod(LastInvocationAware.class, "getLastInvocation");
}
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (GET_INVOCATIONS.equals(invocation.getMethod())) {
return getLastInvocation();
} else if (Object.class.equals(invocation.getMethod().getDeclaringClass())) {
return invocation.proceed();
}
this.invocation = invocation;
Class<?> returnType = invocation.getMethod().getReturnType();
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
factory.setTargetClass(returnType);
factory.addInterface(LastInvocationAware.class);
factory.setProxyTargetClass(true);
factory.addAdvice(this);
return returnType.cast(factory.getProxy());
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.LinkBuilderUtils.LastInvocationAware#getLastInvocation()
*/
@Override
public MethodInvocation getLastInvocation() {
return invocation;
}
}
@SuppressWarnings("unchecked")
public static <T> T methodOn(Class<T> type) {
MethodInterceptor interceptor = new InvocationRecordingMethodInterceptor();
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
factory.setProxyTargetClass(true);
factory.setTargetClass(type);
factory.addInterface(LastInvocationAware.class);
factory.addAdvice(interceptor);
return (T) factory.getProxy();
}
}

View File

@@ -18,13 +18,25 @@ package org.springframework.hateoas.core;
import java.lang.reflect.Method;
/**
*
* Strategy interface to discover a URI mapping for either a given type or method.
*
* @author Oliver Gierke
*/
public interface MappingDiscoverer {
/**
* Returns the mapping associated with the given type.
*
* @param type must not be {@literal null}.
* @return the type-level mapping or {@literal null} in case none is present.
*/
String getMapping(Class<?> type);
/**
* Returns the mapping associated with the given {@link Method}. This will include the type-level mapping.
*
* @param method must not be {@literal null}.
* @return the method mapping including the type-level one or {@literal null} if neither of them present.
*/
String getMapping(Method method);
}

View File

@@ -20,14 +20,20 @@ import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.util.Assert;
/**
* Value object to represent {@link MethodParameters} to allow to easily find the ones with a given annotation.
*
* @author Oliver Gierke
*/
public class MethodParameters {
private static final ParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();
private final List<MethodParameter> parameters;
/**
@@ -41,7 +47,10 @@ public class MethodParameters {
this.parameters = new ArrayList<MethodParameter>();
for (int i = 0; i < method.getParameterTypes().length; i++) {
parameters.add(new MethodParameter(method, i));
MethodParameter parameter = new MethodParameter(method, i);
parameter.initParameterNameDiscovery(DISCOVERER);
parameters.add(parameter);
}
}

View File

@@ -18,7 +18,9 @@ package org.springframework.hateoas.jaxrs;
import javax.ws.rs.Path;
import org.springframework.hateoas.LinkBuilder;
import org.springframework.hateoas.mvc.UriComponentsLinkBuilder;
import org.springframework.hateoas.core.AnnotationMappingDiscoverer;
import org.springframework.hateoas.core.LinkBuilderSupport;
import org.springframework.hateoas.core.MappingDiscoverer;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
@@ -28,7 +30,9 @@ import org.springframework.web.util.UriTemplate;
*
* @author Oliver Gierke
*/
public class JaxRsLinkBuilder extends UriComponentsLinkBuilder<JaxRsLinkBuilder> {
public class JaxRsLinkBuilder extends LinkBuilderSupport<JaxRsLinkBuilder> {
private static final MappingDiscoverer DISCOVERER = new AnnotationMappingDiscoverer(Path.class);
/**
* Creates a new {@link JaxRsLinkBuilder} from the given {@link UriComponentsBuilder}.

View File

@@ -17,9 +17,18 @@ package org.springframework.hateoas.mvc;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.core.AnnotationAttribute;
import org.springframework.hateoas.core.AnnotationMappingDiscoverer;
import org.springframework.hateoas.core.DummyInvocationUtils;
import org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware;
import org.springframework.hateoas.core.LinkBuilderSupport;
import org.springframework.hateoas.core.MappingDiscoverer;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -32,7 +41,11 @@ import org.springframework.web.util.UriTemplate;
*
* @author Oliver Gierke
*/
public class ControllerLinkBuilder extends UriComponentsLinkBuilder<ControllerLinkBuilder> {
public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuilder> {
private static final MappingDiscoverer DISCOVERER = new AnnotationMappingDiscoverer(RequestMapping.class);
private static final AnnotatedParametersParameterAccessor accessor = new AnnotatedParametersParameterAccessor(
new AnnotationAttribute(PathVariable.class));
/**
* Creates a new {@link ControllerLinkBuilder} using the given {@link UriComponentsBuilder}.
@@ -67,27 +80,80 @@ public class ControllerLinkBuilder extends UriComponentsLinkBuilder<ControllerLi
Assert.notNull(controller);
ControllerLinkBuilder builder = new ControllerLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping());
String mapping = DISCOVERER.getMapping(controller);
UriTemplate template = new UriTemplate(DISCOVERER.getMapping(controller));
return builder.slash(template.expand(parameters));
if (mapping == null) {
throw new IllegalArgumentException(
String.format("No mapping found on controller class %s!", controller.getName()));
}
UriTemplate template = new UriTemplate(mapping);
return builder.slash(template.expand(parameters));
}
public static ControllerLinkBuilder linkTo(Method method, Object... parameters) {
UriTemplate template = new UriTemplate(DISCOVERER.getMapping(method));
URI uri = template.expand(parameters);
return new ControllerLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping()).slash(uri);
}
/**
* 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>
*
* 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 invocationValue
* @return
*/
public static ControllerLinkBuilder linkTo(Object invocationValue) {
Assert.isInstanceOf(LastInvocationAware.class, invocationValue);
LastInvocationAware invocations = (LastInvocationAware) invocationValue;
MethodInvocation invocation = invocations.getLastInvocation();
Iterator<Object> classMappingParameters = invocations.getObjectParameters();
Method method = invocation.getMethod();
UriTemplate template = new UriTemplate(DISCOVERER.getMapping(method));
URI uri = template.expand(accessor.getBoundParameters(invocation));
Map<String, Object> values = new HashMap<String, Object>();
return new ControllerLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping()).slash(uri);
if (classMappingParameters.hasNext()) {
for (String variable : template.getVariableNames()) {
values.put(variable, classMappingParameters.next());
}
}
public static <T> T methodOn(Class<T> controller) {
return LinkBuilderUtils.methodOn(controller);
values.putAll(accessor.getBoundParameters(invocation));
URI uri = template.expand(values);
return new ControllerLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping()).slash(uri);
}
/**
* Wrapper for {@link DummyInvocationUtils#methodOn(Class, Object...)} to be available in case you work with static
* imports of {@link ControllerLinkBuilder}.
*
* @param controller must not be {@literal null}.
* @param parameters parameters to extend template variables in the type level mapping.
* @return
*/
public static <T> T methodOn(Class<T> controller, Object... parameters) {
return DummyInvocationUtils.methodOn(controller, parameters);
}
/*

View File

@@ -15,17 +15,18 @@
*/
package org.springframework.hateoas.mvc;
import org.springframework.hateoas.LinkBuilderFactory;
import java.lang.reflect.Method;
import org.springframework.hateoas.MethodLinkBuilderFactory;
import org.springframework.hateoas.core.LinkBuilderSupport;
/**
* Factory for {@link LinkBuilderSupport} instances based on the request mapping annotated on the given
* controller.
* Factory for {@link LinkBuilderSupport} instances based on the request mapping annotated on the given controller.
*
* @author Ricardo Gladwell
* @author Oliver Gierke
*/
public class ControllerLinkBuilderFactory implements LinkBuilderFactory<ControllerLinkBuilder> {
public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory<ControllerLinkBuilder> {
/*
* (non-Javadoc)
@@ -44,4 +45,22 @@ public class ControllerLinkBuilderFactory implements LinkBuilderFactory<Controll
public ControllerLinkBuilder linkTo(Class<?> controller, Object... parameters) {
return ControllerLinkBuilder.linkTo(controller, parameters);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(java.lang.Object)
*/
@Override
public ControllerLinkBuilder linkTo(Object methodInvocationResult) {
return ControllerLinkBuilder.linkTo(methodInvocationResult);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(java.lang.reflect.Method, java.lang.Object[])
*/
@Override
public ControllerLinkBuilder linkTo(Method method, Object... parameters) {
return ControllerLinkBuilder.linkTo(method, parameters);
}
}