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 {} }