From 8039c306e9864d8077f727730b311048e3f339ea Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Mon, 18 Feb 2013 19:11:58 +0100 Subject: [PATCH] #52 - Introduced UriComponentsContributor SPI. Introduced UriComponentsContributor interface to allow components to enhance the UriComponentsBuilder used while building links point in to controller methods. Moved logic from ControllerLinkBuilder into ControllerLinkBuilderFactory as we now potentially need access to Spring injected components. Might be necessary to get rid of the static LinkBuilder implementations entirely. --- .../LinkBuilderBeanDefinitionRegistrar.java | 7 +- .../hateoas/mvc/ControllerLinkBuilder.java | 36 +------- .../mvc/ControllerLinkBuilderFactory.java | 89 ++++++++++++++++++- .../hateoas/mvc/UriComponentsContributor.java | 49 ++++++++++ .../ControllerLinkBuilderFactoryUnitTest.java | 44 +++++++++ 5 files changed, 188 insertions(+), 37 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/mvc/UriComponentsContributor.java diff --git a/src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java index dc51a4ab..edd89a7f 100644 --- a/src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java +++ b/src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-2013 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. @@ -83,9 +83,12 @@ public class LinkBuilderBeanDefinitionRegistrar implements ImportBeanDefinitionR private static BeanDefinitionBuilder getEntityControllerLinksFor(Class type, Class> linkBuilderFactoryType) { + RootBeanDefinition definition = new RootBeanDefinition(linkBuilderFactoryType); + definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ControllerEntityLinksFactoryBean.class); builder.addPropertyValue("annotation", type); - builder.addPropertyValue("linkBuilderFactory", new RootBeanDefinition(linkBuilderFactoryType)); + builder.addPropertyValue("linkBuilderFactory", definition); return builder; } diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java index d51397a1..88e66d7c 100755 --- a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java @@ -17,23 +17,16 @@ 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 javax.servlet.http.HttpServletRequest; -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.util.StringUtils; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; @@ -50,15 +43,14 @@ import org.springframework.web.util.UriTemplate; 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)); + private static final ControllerLinkBuilderFactory FACTORY = new ControllerLinkBuilderFactory(); /** * Creates a new {@link ControllerLinkBuilder} using the given {@link UriComponentsBuilder}. * * @param builder must not be {@literal null}. */ - private ControllerLinkBuilder(UriComponentsBuilder builder) { + ControllerLinkBuilder(UriComponentsBuilder builder) { super(builder); } @@ -127,27 +119,7 @@ public class ControllerLinkBuilder extends LinkBuilderSupport classMappingParameters = invocations.getObjectParameters(); - Method method = invocation.getMethod(); - - UriTemplate template = new UriTemplate(DISCOVERER.getMapping(method)); - Map values = new HashMap(); - - if (classMappingParameters.hasNext()) { - for (String variable : template.getVariableNames()) { - values.put(variable, classMappingParameters.next()); - } - } - - values.putAll(accessor.getBoundParameters(invocation)); - URI uri = template.expand(values); - - return new ControllerLinkBuilder(getBuilder()).slash(uri); + return FACTORY.linkTo(invocationValue); } /** @@ -186,7 +158,7 @@ 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)); + + private List uriComponentsContributors = new ArrayList(); + + /** + * Configures the {@link UriComponentsContributor} to be used when building {@link Link} instances from method + * invocations. + * + * @see #linkTo(Object) + * @param uriComponentsContributors the uriComponentsContributors to set + */ + public void setUriComponentsContributors(List uriComponentsContributors) { + this.uriComponentsContributors = Collections.unmodifiableList(uriComponentsContributors); + } + /* * (non-Javadoc) * @see org.springframework.hateoas.LinkBuilderFactory#linkTo(java.lang.Class) @@ -51,8 +89,29 @@ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory classMappingParameters = invocations.getObjectParameters(); + Method method = invocation.getMethod(); + + UriTemplate template = new UriTemplate(DISCOVERER.getMapping(method)); + Map values = new HashMap(); + + if (classMappingParameters.hasNext()) { + for (String variable : template.getVariableNames()) { + values.put(variable, classMappingParameters.next()); + } + } + + values.putAll(ACCESSOR.getBoundParameters(invocation)); + URI uri = template.expand(values); + + UriComponentsBuilder builder = ControllerLinkBuilder.getBuilder(); + return new ControllerLinkBuilder(applyUriComponentsContributer(builder, invocation)).slash(uri); } /* @@ -63,4 +122,28 @@ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory parameterValues = Arrays.asList(invocation.getArguments()).iterator(); + + for (MethodParameter parameter : parameters.getParameters()) { + Object parameterValue = parameterValues.next(); + for (UriComponentsContributor contributor : uriComponentsContributors) { + if (contributor.supportsParameter(parameter)) { + contributor.enhance(builder, parameter, parameterValue); + } + } + } + + return builder; + } } diff --git a/src/main/java/org/springframework/hateoas/mvc/UriComponentsContributor.java b/src/main/java/org/springframework/hateoas/mvc/UriComponentsContributor.java new file mode 100644 index 00000000..74743d1a --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/UriComponentsContributor.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013 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.mvc; + +import org.springframework.core.MethodParameter; +import org.springframework.hateoas.MethodLinkBuilderFactory; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * SPI callback to enhance a {@link UriComponentsBuilder} when referring to a method through a dummy method invocation. + * Will usually be implemented in implementations of {@link HandlerMethodArgumentResolver} as they represent exactly the + * same functionality inverted. + * + * @see MethodLinkBuilderFactory#linkTo(Object) + * @author Oliver Gierke + */ +public interface UriComponentsContributor { + + /** + * Returns whether the {@link UriComponentsBuilder} supports the given {@link MethodParameter}. + * + * @param parameter will never be {@literal null}. + * @return + */ + boolean supportsParameter(MethodParameter parameter); + + /** + * Enhance the given {@link UriComponentsBuilder} with the given value. + * + * @param builder will never be {@literal null}. + * @param parameter will never be {@literal null}. + * @param value can be {@literal null}. + */ + void enhance(UriComponentsBuilder builder, MethodParameter parameter, Object value); +} diff --git a/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactoryUnitTest.java b/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactoryUnitTest.java index ab935f70..18cce51a 100644 --- a/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactoryUnitTest.java +++ b/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactoryUnitTest.java @@ -17,12 +17,20 @@ package org.springframework.hateoas.mvc; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.springframework.hateoas.core.DummyInvocationUtils.*; + +import java.util.Arrays; import org.junit.Test; +import org.springframework.core.MethodParameter; import org.springframework.hateoas.Link; import org.springframework.hateoas.TestUtils; import org.springframework.hateoas.mvc.ControllerLinkBuilderUnitTest.PersonControllerImpl; import org.springframework.hateoas.mvc.ControllerLinkBuilderUnitTest.PersonsAddressesController; +import org.springframework.http.HttpEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.util.UriComponentsBuilder; /** * Unit tests for {@link ControllerLinkBuilderFactory}. @@ -51,4 +59,40 @@ public class ControllerLinkBuilderFactoryUnitTest extends TestUtils { assertThat(link.getRel(), is(Link.REL_SELF)); assertThat(link.getHref(), endsWith("/people/15/addresses")); } + + @Test + public void appliesParameterValueIfContributorConfigured() { + + ControllerLinkBuilderFactory factory = new ControllerLinkBuilderFactory(); + factory.setUriComponentsContributors(Arrays.asList(new SampleUriComponentsContributor())); + + SpecialType specialType = new SpecialType(); + specialType.parameterValue = "value"; + + Link link = factory.linkTo(methodOn(SampleController.class).sampleMethod(1L, specialType)).withSelfRel(); + assertThat(link.getHref(), endsWith("/sample/1?foo=value")); + } + + static interface SampleController { + + @RequestMapping("/sample/{id}") + HttpEntity sampleMethod(@PathVariable("id") Long id, SpecialType parameter); + } + + static class SampleUriComponentsContributor implements UriComponentsContributor { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return SpecialType.class.equals(parameter.getParameterType()); + } + + @Override + public void enhance(UriComponentsBuilder builder, MethodParameter parameter, Object value) { + builder.queryParam("foo", ((SpecialType) value).parameterValue); + } + } + + static class SpecialType { + String parameterValue; + } }