From c9675640f22c5c0dbdf04706e6178e08552a6ebf Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 1 May 2014 18:18:06 +0200 Subject: [PATCH] #114 - Support for type mappings on sub-types in ControllerLinkBuilder. When building links for method invocations we previously only inspected the type that declared the method for type level mappings. We now use the type the method was invoked on explicitly. --- .../core/AnnotationMappingDiscoverer.java | 20 +++++++- .../hateoas/core/DummyInvocationUtils.java | 27 ++++++++-- .../hateoas/core/MappingDiscoverer.java | 12 ++++- .../mvc/ControllerLinkBuilderFactory.java | 4 +- .../AnnotationMappingDiscovererUnitTest.java | 42 +++++++++++++++- .../mvc/ControllerLinkBuilderUnitTest.java | 49 ++++++++++++++++--- 6 files changed, 135 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java b/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java index 0caba80f..b723877d 100644 --- a/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-2014 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. @@ -63,6 +63,8 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer { @Override public String getMapping(Class type) { + Assert.notNull(type, "Type must not be null!"); + String[] mapping = getMappingFrom(findAnnotation(type, annotationType)); if (mapping.length > 1) { @@ -80,6 +82,20 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer { @Override public String getMapping(Method method) { + Assert.notNull(method, "Method must not be null!"); + return getMapping(method.getDeclaringClass(), method); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.Class, java.lang.reflect.Method) + */ + @Override + public String getMapping(Class type, Method method) { + + Assert.notNull(type, "Type must not be null!"); + Assert.notNull(method, "Method must not be null!"); + String[] mapping = getMappingFrom(findAnnotation(method, annotationType)); if (mapping.length > 1) { @@ -87,7 +103,7 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer { method.toString())); } - String typeMapping = getMapping(method.getDeclaringClass()); + String typeMapping = getMapping(type); if (mapping == null || mapping.length == 0) { return typeMapping; diff --git a/src/main/java/org/springframework/hateoas/core/DummyInvocationUtils.java b/src/main/java/org/springframework/hateoas/core/DummyInvocationUtils.java index 91fb8649..3cd3ba69 100644 --- a/src/main/java/org/springframework/hateoas/core/DummyInvocationUtils.java +++ b/src/main/java/org/springframework/hateoas/core/DummyInvocationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-2014 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. @@ -58,6 +58,7 @@ public class DummyInvocationUtils { private static final Method GET_INVOCATIONS; private static final Method GET_OBJECT_PARAMETERS; + private final Class targetType; private final Object[] objectParameters; private MethodInvocation invocation; @@ -72,7 +73,9 @@ public class DummyInvocationUtils { * * @param parameters */ - public InvocationRecordingMethodInterceptor(Object... parameters) { + public InvocationRecordingMethodInterceptor(Class targetType, Object... parameters) { + + this.targetType = targetType; this.objectParameters = parameters.clone(); } @@ -90,7 +93,7 @@ public class DummyInvocationUtils { return ReflectionUtils.invokeMethod(method, obj, args); } - this.invocation = new SimpleMethodInvocation(method, args); + this.invocation = new SimpleMethodInvocation(targetType, method, args); Class returnType = method.getReturnType(); return returnType.cast(getProxyWithInterceptor(returnType, this)); @@ -140,7 +143,7 @@ public class DummyInvocationUtils { Assert.notNull(type, "Given type must not be null!"); - InvocationRecordingMethodInterceptor interceptor = new InvocationRecordingMethodInterceptor(parameters); + InvocationRecordingMethodInterceptor interceptor = new InvocationRecordingMethodInterceptor(type, parameters); return getProxyWithInterceptor(type, interceptor); } @@ -172,10 +175,13 @@ public class DummyInvocationUtils { Object[] getArguments(); Method getMethod(); + + Class getTargetType(); } static class SimpleMethodInvocation implements MethodInvocation { + private final Class targetType; private final Method method; private final Object[] arguments; @@ -185,11 +191,22 @@ public class DummyInvocationUtils { * @param method * @param arguments */ - private SimpleMethodInvocation(Method method, Object[] arguments) { + private SimpleMethodInvocation(Class targetType, Method method, Object[] arguments) { + + this.targetType = targetType; this.arguments = arguments; this.method = method; } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation#getTargetType() + */ + @Override + public Class getTargetType() { + return targetType; + } + /* * (non-Javadoc) * @see org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation#getArguments() diff --git a/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java b/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java index 627a003b..4b04464d 100644 --- a/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-2014 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. @@ -39,4 +39,14 @@ public interface MappingDiscoverer { * @return the method mapping including the type-level one or {@literal null} if neither of them present. */ String getMapping(Method method); + + /** + * Returns the mapping for the given {@link Method} invoked on the given type. This can be used to calculate the + * mapping for a super type method being invoked on a sub-type with a type mapping. + * + * @param type must not be {@literal null}. + * @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(Class type, Method method); } diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java index 8cc0d7e9..5c4b19ec 100644 --- a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-2014 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. @@ -103,7 +103,7 @@ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory classMappingParameters = invocations.getObjectParameters(); Method method = invocation.getMethod(); - String mapping = DISCOVERER.getMapping(method); + String mapping = DISCOVERER.getMapping(invocation.getTargetType(), method); UriComponentsBuilder builder = ControllerLinkBuilder.getBuilder().path(mapping); UriTemplate template = new UriTemplate(mapping); diff --git a/src/test/java/org/springframework/hateoas/core/AnnotationMappingDiscovererUnitTest.java b/src/test/java/org/springframework/hateoas/core/AnnotationMappingDiscovererUnitTest.java index 738437eb..c45b4968 100644 --- a/src/test/java/org/springframework/hateoas/core/AnnotationMappingDiscovererUnitTest.java +++ b/src/test/java/org/springframework/hateoas/core/AnnotationMappingDiscovererUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-2014 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. @@ -77,6 +77,26 @@ public class AnnotationMappingDiscovererUnitTest { assertThat(discoverer.getMapping(method), is("/type")); } + /** + * @see #114 + */ + @Test + public void detectsClassMappingOnSuperType() throws Exception { + + Method method = ChildController.class.getMethod("mapping"); + assertThat(discoverer.getMapping(method), is("/parent/child")); + } + + /** + * @see #114 + */ + @Test + public void includesTypeMappingFromChildClass() throws Exception { + + Method method = ParentWithMethod.class.getMethod("mapping"); + assertThat(discoverer.getMapping(ChildWithTypeMapping.class, method), is("/child/parent")); + } + @RequestMapping("/type") interface MyController { @@ -99,4 +119,24 @@ public class AnnotationMappingDiscovererUnitTest { @RequestMapping("/method") void method(); } + + @RequestMapping("/parent") + interface ParentController { + + } + + interface ChildController extends ParentController { + + @RequestMapping("/child") + void mapping(); + } + + interface ParentWithMethod { + + @RequestMapping("/parent") + void mapping(); + } + + @RequestMapping("/child") + interface ChildWithTypeMapping extends ParentWithMethod {} } diff --git a/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java b/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java index 06754413..3cb36a9a 100644 --- a/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java +++ b/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-2014 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. @@ -29,7 +29,6 @@ import org.springframework.hateoas.Identifiable; import org.springframework.hateoas.Link; import org.springframework.hateoas.TestUtils; import org.springframework.http.HttpEntity; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -324,6 +323,26 @@ public class ControllerLinkBuilderUnitTest extends TestUtils { assertThat(link.getHref(), startsWith("http://foobarhost/")); } + /** + * @see #114 + */ + @Test + public void discoversParentClassTypeMappingForInvocation() { + + Link link = linkTo(methodOn(ChildController.class).myMethod()).withSelfRel(); + assertThat(link.getHref(), endsWith("/parent/child")); + } + + /** + * @see #114 + */ + @Test + public void includesTypeMappingFromChildClass() { + + Link link = linkTo(methodOn(ChildWithTypeMapping.class).myMethod()).withSelfRel(); + assertThat(link.getHref(), endsWith("/child/parent")); + } + private static UriComponents toComponents(Link link) { return UriComponentsBuilder.fromUriString(link.getHref()).build(); } @@ -339,13 +358,9 @@ public class ControllerLinkBuilderUnitTest extends TestUtils { } @RequestMapping("/people") - interface PersonController { + interface PersonController {} - } - - class PersonControllerImpl implements PersonController { - - } + class PersonControllerImpl implements PersonController {} @RequestMapping("/people/{id}/addresses") static class PersonsAddressesController { @@ -400,4 +415,22 @@ public class ControllerLinkBuilderUnitTest extends TestUtils { return null; } } + + @RequestMapping("/parent") + interface ParentController {} + + interface ChildController extends ParentController { + + @RequestMapping("/child") + Object myMethod(); + } + + interface ParentWithMethod { + + @RequestMapping("/parent") + Object myMethod(); + } + + @RequestMapping("/child") + interface ChildWithTypeMapping extends ParentWithMethod {} }