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