From 531dd8f5ed3f012e6c1c26bb8eebac48bf0cd8bc Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 19 Dec 2012 13:10:12 +0100 Subject: [PATCH] #18 - Added support for creating links from method mappings. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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. --- readme.md | 23 +++ .../hateoas/MethodLinkBuilderFactory.java | 51 ++++++ .../core/AnnotationMappingDiscoverer.java | 27 +++- .../hateoas/core/ControllerEntityLinks.java | 6 +- .../hateoas/core/DummyInvocationUtils.java | 151 ++++++++++++++++++ .../hateoas/core/LinkBuilderUtils.java | 94 ----------- .../hateoas/core/MappingDiscoverer.java | 16 +- .../hateoas/core/MethodParameters.java | 11 +- .../hateoas/jaxrs/JaxRsLinkBuilder.java | 8 +- .../hateoas/mvc/ControllerLinkBuilder.java | 80 +++++++++- .../mvc/ControllerLinkBuilderFactory.java | 27 +++- .../AnnotationMappingDiscovererUnitTest.java | 75 +++++++++ .../mvc/ControllerLinkBuilderUnitTest.java | 32 +++- ...java => DummyInvocationUtilsUnitTest.java} | 8 +- template.mf | 1 + 15 files changed, 487 insertions(+), 123 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/MethodLinkBuilderFactory.java create mode 100644 src/main/java/org/springframework/hateoas/core/DummyInvocationUtils.java delete mode 100644 src/main/java/org/springframework/hateoas/core/LinkBuilderUtils.java create mode 100644 src/test/java/org/springframework/hateoas/core/AnnotationMappingDiscovererUnitTest.java rename src/test/java/org/springframework/hateoas/mvc/{LinkBuilderUtilsUnitTest.java => DummyInvocationUtilsUnitTest.java} (81%) diff --git a/readme.md b/readme.md index 86eaa490..539f6838 100644 --- a/readme.md +++ b/readme.md @@ -123,6 +123,29 @@ headers.setLocation(linkTo(PersonController.class).slash(person).toUri()); return new ResponseEntity(headers, HttpStatus.CREATED); ``` +### Building links pointing to methods + +As of version 0.4 you can even easily build links pointing to methods or creating dummy controller method invocations. The first approach is to hand a `Method` instance to the `ControllerLinkBuilder`: + +```java +Method method = PersonController.class.getMethod("show", Long.class); +Link link = linkTo(method, 2L).withSelfRel(); + +assertThat(link.getHref(), is("/people/2"))); +``` + +This is still a bit dissatisfying as we have to get a `Method` instance first, which throws an exception and is generally quite cumbersome. At least we don't repeat the mapping. An even better approach is to have a dummy method invocation of the target method on a controller proxy we can create easily using the `methodOn(…)` helper. + +```java +Link link = linkTo(methodOn(PersonController.class).show(2L)).withSelfRel(); +assertThat(link.getHref(), is("/people/2"))); +``` + +`methodOn(…)` creates a proxy of the controller class that is recording the method invocation and exposed it in a proxy created for the return type of the method. This allows the fluent expression of the method we want to obtain the mapping for. However there are a few constraints on the methods that can be obtained using this technique: + +1. The return type has to be capable of proxying as we need to expose the method invocation on it. +2. The parameters handed into the methods are generally neglected, except the ones referred to through `@PathVariable` as they make up the URI. + ## Resource assembler As the mapping from an entity to a resource type will have to be used in multiple places it makes sense to create a dedicated class responsible for doing so. The conversion will of course contain very custom steps but also a few boilerplate ones: diff --git a/src/main/java/org/springframework/hateoas/MethodLinkBuilderFactory.java b/src/main/java/org/springframework/hateoas/MethodLinkBuilderFactory.java new file mode 100644 index 00000000..bb6a689d --- /dev/null +++ b/src/main/java/org/springframework/hateoas/MethodLinkBuilderFactory.java @@ -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 extends LinkBuilderFactory { + + /** + * 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); +} diff --git a/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java b/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java index ac844362..9b6d52ed 100644 --- a/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java @@ -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 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 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 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( diff --git a/src/main/java/org/springframework/hateoas/core/ControllerEntityLinks.java b/src/main/java/org/springframework/hateoas/core/ControllerEntityLinks.java index 0c959373..c2326a2e 100644 --- a/src/main/java/org/springframework/hateoas/core/ControllerEntityLinks.java +++ b/src/main/java/org/springframework/hateoas/core/ControllerEntityLinks.java @@ -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; *
  • Individual resources are exposed via a nested mapping consisting of the id of the managed entity, e.g. {@code * @RequestMapping("/{id}")}.
  • * - * + *
      * @Controller
      * @ExposesResourceFor(Order.class)
      * @RequestMapping("/orders")
    @@ -47,7 +47,7 @@ import org.springframework.util.Assert;
      *   @RequestMapping("/{id}")
      *   ResponseEntity order(@PathVariable("id") … ) { … }  
      * }
    - * 
    + * 
    * * @author Oliver Gierke */ diff --git a/src/main/java/org/springframework/hateoas/core/DummyInvocationUtils.java b/src/main/java/org/springframework/hateoas/core/DummyInvocationUtils.java new file mode 100644 index 00000000..e32538c2 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/DummyInvocationUtils.java @@ -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 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 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 methodOn(Class 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 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(); + } +} diff --git a/src/main/java/org/springframework/hateoas/core/LinkBuilderUtils.java b/src/main/java/org/springframework/hateoas/core/LinkBuilderUtils.java deleted file mode 100644 index 4378466b..00000000 --- a/src/main/java/org/springframework/hateoas/core/LinkBuilderUtils.java +++ /dev/null @@ -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 methodOn(Class 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(); - } -} diff --git a/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java b/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java index bf7cc0d2..627a003b 100644 --- a/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java @@ -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); - } diff --git a/src/main/java/org/springframework/hateoas/core/MethodParameters.java b/src/main/java/org/springframework/hateoas/core/MethodParameters.java index ff787f43..60e96782 100644 --- a/src/main/java/org/springframework/hateoas/core/MethodParameters.java +++ b/src/main/java/org/springframework/hateoas/core/MethodParameters.java @@ -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 parameters; /** @@ -41,7 +47,10 @@ public class MethodParameters { this.parameters = new ArrayList(); 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); } } diff --git a/src/main/java/org/springframework/hateoas/jaxrs/JaxRsLinkBuilder.java b/src/main/java/org/springframework/hateoas/jaxrs/JaxRsLinkBuilder.java index f5ab0611..a6d51992 100644 --- a/src/main/java/org/springframework/hateoas/jaxrs/JaxRsLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/jaxrs/JaxRsLinkBuilder.java @@ -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 { +public class JaxRsLinkBuilder extends LinkBuilderSupport { + + private static final MappingDiscoverer DISCOVERER = new AnnotationMappingDiscoverer(Path.class); /** * Creates a new {@link JaxRsLinkBuilder} from the given {@link UriComponentsBuilder}. diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java index d3b1f390..5723a9d1 100755 --- a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java @@ -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 { +public class ControllerLinkBuilder extends LinkBuilderSupport { + + 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 + * @RequestMapping("/customers") + * class CustomerController { + * + * @RequestMapping("/{id}/addresses") + * HttpEntity<Addresses> showAddresses(@PathVariable Long id) { … } + * } + * + * Link link = linkTo(methodOn(CustomerController.class).showAddresses(2L)).withRel("addresses"); + * + * + * 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 classMappingParameters = invocations.getObjectParameters(); Method method = invocation.getMethod(); UriTemplate template = new UriTemplate(DISCOVERER.getMapping(method)); - URI uri = template.expand(accessor.getBoundParameters(invocation)); + Map values = new HashMap(); - return new ControllerLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping()).slash(uri); + if (classMappingParameters.hasNext()) { + for (String variable : template.getVariableNames()) { + values.put(variable, classMappingParameters.next()); + } } - public static T methodOn(Class 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 methodOn(Class controller, Object... parameters) { + return DummyInvocationUtils.methodOn(controller, parameters); } /* diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java index 60cc353d..110e7960 100644 --- a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java @@ -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 { +public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory { /* * (non-Javadoc) @@ -44,4 +45,22 @@ public class ControllerLinkBuilderFactory implements LinkBuilderFactory 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); + } } diff --git a/src/test/java/org/springframework/hateoas/core/AnnotationMappingDiscovererUnitTest.java b/src/test/java/org/springframework/hateoas/core/AnnotationMappingDiscovererUnitTest.java new file mode 100644 index 00000000..c7120610 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/core/AnnotationMappingDiscovererUnitTest.java @@ -0,0 +1,75 @@ +/* + * 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 static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Method; + +import org.junit.Test; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * Unit tests for {@link AnnotationMappingDiscoverer}. + * + * @author Oliver Gierke + */ +public class AnnotationMappingDiscovererUnitTest { + + MappingDiscoverer discoverer = new AnnotationMappingDiscoverer(RequestMapping.class); + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullAnnotation() { + new AnnotationMappingDiscoverer(null); + } + + @Test + public void discoversTypeLevelMapping() { + assertThat(discoverer.getMapping(MyController.class), is("/type")); + } + + @Test + public void discoversMethodLevelMapping() throws Exception { + Method method = MyController.class.getMethod("method"); + assertThat(discoverer.getMapping(method), is("/type/method")); + } + + @Test + public void returnsNullForNonExistentTypeLevelMapping() { + assertThat(discoverer.getMapping(ControllerWithoutTypeLevelMapping.class), is(nullValue())); + } + + @Test + public void resolvesMethodLevelMappingWithoutTypeLevelMapping() throws Exception { + + Method method = ControllerWithoutTypeLevelMapping.class.getMethod("method"); + assertThat(discoverer.getMapping(method), is("/method")); + } + + @RequestMapping("/type") + interface MyController { + + @RequestMapping("/method") + void method(); + } + + interface ControllerWithoutTypeLevelMapping { + + @RequestMapping("/method") + void method(); + } +} diff --git a/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java b/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java index 88b749f9..b058a6e8 100644 --- a/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java +++ b/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java @@ -25,6 +25,9 @@ import org.junit.Test; import org.springframework.hateoas.Identifiable; import org.springframework.hateoas.Link; import org.springframework.hateoas.TestUtils; +import org.springframework.http.HttpEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; /** @@ -69,7 +72,7 @@ public class ControllerLinkBuilderUnitTest extends TestUtils { linkTo(InvalidController.class); } - @Test + @Test(expected = IllegalArgumentException.class) public void createsLinkToUnmappedController() { linkTo(UnmappedController.class); } @@ -95,6 +98,20 @@ public class ControllerLinkBuilderUnitTest extends TestUtils { assertThat(link.getHref(), Matchers.endsWith("/people")); } + @Test + public void linksToMethod() { + + Link link = linkTo(methodOn(ControllerWithMethods.class).myMethod(null)).withSelfRel(); + assertThat(link.getHref(), Matchers.endsWith("/something/else")); + } + + @Test + public void linksToMethodWithPathVariable() { + + Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithPathVariable("1")).withSelfRel(); + assertThat(link.getHref(), Matchers.endsWith("/something/1/foo")); + } + static class Person implements Identifiable { Long id; @@ -128,4 +145,17 @@ public class ControllerLinkBuilderUnitTest extends TestUtils { } + @RequestMapping("/something") + static class ControllerWithMethods { + + @RequestMapping("/else") + HttpEntity myMethod(@RequestBody Object payload) { + return null; + } + + @RequestMapping("/{id}/foo") + HttpEntity methodWithPathVariable(@PathVariable String id) { + return null; + } + } } diff --git a/src/test/java/org/springframework/hateoas/mvc/LinkBuilderUtilsUnitTest.java b/src/test/java/org/springframework/hateoas/mvc/DummyInvocationUtilsUnitTest.java similarity index 81% rename from src/test/java/org/springframework/hateoas/mvc/LinkBuilderUtilsUnitTest.java rename to src/test/java/org/springframework/hateoas/mvc/DummyInvocationUtilsUnitTest.java index 9bf6e883..a4f4234c 100644 --- a/src/test/java/org/springframework/hateoas/mvc/LinkBuilderUtilsUnitTest.java +++ b/src/test/java/org/springframework/hateoas/mvc/DummyInvocationUtilsUnitTest.java @@ -17,7 +17,7 @@ package org.springframework.hateoas.mvc; import org.junit.Test; import org.springframework.hateoas.TestUtils; -import org.springframework.hateoas.core.LinkBuilderUtils; +import org.springframework.hateoas.core.DummyInvocationUtils; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -27,14 +27,12 @@ import org.springframework.web.bind.annotation.RequestMapping; /** * @author Oliver Gierke */ -public class LinkBuilderUtilsUnitTest extends TestUtils { +public class DummyInvocationUtilsUnitTest extends TestUtils { @Test public void test() { - ControllerLinkBuilder builder = ControllerLinkBuilder.linkTo(LinkBuilderUtils.methodOn(SampleController.class) - .someMethod(1L)); - System.out.println(builder.toUri()); + ControllerLinkBuilder.linkTo(DummyInvocationUtils.methodOn(SampleController.class).someMethod(1L)); } diff --git a/template.mf b/template.mf index 252ef0cb..c3b34056 100644 --- a/template.mf +++ b/template.mf @@ -8,5 +8,6 @@ Import-Template: javax.ws.rs.*;version="${jaxrs.version:[=.=.=,+1.0.0)}";resolution:=optional, javax.xml.bind.*;version="0", net.minidev.json.*;version="${minidevjson.version:[=.=.=,+1.0.0)}";resolution:=optional, + org.aopalliance.*;version="[1.0.0,2.0.0)";resolution:=optional, org.springframework.*;version="${spring.version:[=.=.=,+1.0.0)}";resolution:=optional, org.codehaus.jackson.*;version="${jackson1.version:[=.=.=,+1.0.0)}";resolution:=optional