#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.
This commit is contained in:
Oliver Gierke
2013-02-18 19:11:58 +01:00
parent 72837210ee
commit 8039c306e9
5 changed files with 188 additions and 37 deletions

View File

@@ -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<? extends Annotation> type,
Class<? extends LinkBuilderFactory<?>> 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;
}

View File

@@ -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<ControllerLinkBuilder> {
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<ControllerLinkBuil
* @return
*/
public static ControllerLinkBuilder linkTo(Object invocationValue) {
Assert.isInstanceOf(LastInvocationAware.class, invocationValue);
LastInvocationAware invocations = (LastInvocationAware) invocationValue;
MethodInvocation invocation = invocations.getLastInvocation();
Iterator<Object> classMappingParameters = invocations.getObjectParameters();
Method method = invocation.getMethod();
UriTemplate template = new UriTemplate(DISCOVERER.getMapping(method));
Map<String, Object> values = new HashMap<String, Object>();
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<ControllerLinkBuil
*
* @return
*/
private static UriComponentsBuilder getBuilder() {
static UriComponentsBuilder getBuilder() {
HttpServletRequest request = getCurrentRequest();
ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request);

View File

@@ -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.
@@ -16,9 +16,30 @@
package org.springframework.hateoas.mvc;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.core.MethodParameter;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MethodLinkBuilderFactory;
import org.springframework.hateoas.core.AnnotationAttribute;
import org.springframework.hateoas.core.AnnotationMappingDiscoverer;
import org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware;
import org.springframework.hateoas.core.LinkBuilderSupport;
import org.springframework.hateoas.core.MappingDiscoverer;
import org.springframework.hateoas.core.MethodParameters;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
/**
* Factory for {@link LinkBuilderSupport} instances based on the request mapping annotated on the given controller.
@@ -28,6 +49,23 @@ import org.springframework.hateoas.core.LinkBuilderSupport;
*/
public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory<ControllerLinkBuilder> {
private static final MappingDiscoverer DISCOVERER = new AnnotationMappingDiscoverer(RequestMapping.class);
private static final AnnotatedParametersParameterAccessor ACCESSOR = new AnnotatedParametersParameterAccessor(
new AnnotationAttribute(PathVariable.class));
private List<UriComponentsContributor> uriComponentsContributors = new ArrayList<UriComponentsContributor>();
/**
* 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<? extends UriComponentsContributor> 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<Co
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(java.lang.Object)
*/
@Override
public ControllerLinkBuilder linkTo(Object methodInvocationResult) {
return ControllerLinkBuilder.linkTo(methodInvocationResult);
public ControllerLinkBuilder linkTo(Object invocationValue) {
Assert.isInstanceOf(LastInvocationAware.class, invocationValue);
LastInvocationAware invocations = (LastInvocationAware) invocationValue;
MethodInvocation invocation = invocations.getLastInvocation();
Iterator<Object> classMappingParameters = invocations.getObjectParameters();
Method method = invocation.getMethod();
UriTemplate template = new UriTemplate(DISCOVERER.getMapping(method));
Map<String, Object> values = new HashMap<String, Object>();
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<Co
public ControllerLinkBuilder linkTo(Method method, Object... parameters) {
return ControllerLinkBuilder.linkTo(method, parameters);
}
/**
* Applies the configured {@link UriComponentsContributor}s to the given {@link UriComponentsBuilder}.
*
* @param builder will never be {@literal null}.
* @param invocation will never be {@literal null}.
* @return
*/
protected UriComponentsBuilder applyUriComponentsContributer(UriComponentsBuilder builder, MethodInvocation invocation) {
MethodParameters parameters = new MethodParameters(invocation.getMethod());
Iterator<Object> 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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}