Introduced MvcUriComponentsBuilder to create URIs pointing to controller methods.

MvcUriComponentsBuilder allows creating URIs that point to Spring MVC
controller methods annotated with @RequestMapping. It builds them by
exposing a mock method invocation API similar to Mockito, records the
method invocations and thus builds up the URI by inspecting the mapping
annotations and the parameters handed into the method invocations.

Introduced a new SPI UriComponentsContributor that should be implemented 
by HandlerMethodArgumentResolvers that actually contribute path segments 
or query parameters to a URI. While the newly introduced 
MvcUriComponentsBuilder looks up those UriComponentsContributor instances 
from the MVC configuration.

The MvcUriComponentsBuilderFactory (name to be discussed - MvcUris maybe?) 
prevents the multiple lookups by keeping the UriComponentsBuilder 
instances in an instance variable. So an instance of the factory could 
be exposed as Spring bean or through a HandlerMethodArgumentResolver to 
be injected into Controller methods.

Issue: SPR-10665, SPR-8826
This commit is contained in:
Oliver Gierke
2013-08-02 19:19:48 +02:00
committed by Rossen Stoyanchev
parent 92a48b72d7
commit 4fd27b12fc
18 changed files with 2036 additions and 1 deletions

View File

@@ -640,6 +640,7 @@ project("spring-webmvc") {
dependencies {
compile(project(":spring-core"))
compile(files(project(":spring-core").objenesisRepackJar))
compile(project(":spring-expression"))
compile(project(":spring-beans"))
compile(project(":spring-web"))

View File

@@ -0,0 +1,125 @@
/*
* 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.
* 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.core;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
/**
* Simply helper to reference a dedicated attribute of an {@link Annotation}.
*
* @author Oliver Gierke
*/
public class AnnotationAttribute {
private final Class<? extends Annotation> annotationType;
private final String attributeName;
/**
* Creates a new {@link AnnotationAttribute} to the {@code value} attribute of the
* given {@link Annotation} type.
*
* @param annotationType must not be {@literal null}.
*/
public AnnotationAttribute(Class<? extends Annotation> annotationType) {
this(annotationType, null);
}
/**
* Creates a new {@link AnnotationAttribute} for the given {@link Annotation} type and
* annotation attribute name.
*
* @param annotationType must not be {@literal null}.
* @param attributeName can be {@literal null}, defaults to {@code value}.
*/
public AnnotationAttribute(Class<? extends Annotation> annotationType,
String attributeName) {
Assert.notNull(annotationType);
this.annotationType = annotationType;
this.attributeName = attributeName;
}
/**
* Returns the annotation type.
*
* @return the annotationType
*/
public Class<? extends Annotation> getAnnotationType() {
return annotationType;
}
/**
* Reads the {@link Annotation} attribute's value from the given
* {@link MethodParameter}.
*
* @param parameter must not be {@literal null}.
* @return
*/
public Object getValueFrom(MethodParameter parameter) {
Assert.notNull(parameter, "MethodParameter must not be null!");
Annotation annotation = parameter.getParameterAnnotation(annotationType);
return annotation == null ? null : getValueFrom(annotation);
}
/**
* Reads the {@link Annotation} attribute's value from the given type.
*
* @param type must not be {@literal null}.
* @return
*/
public Object findValueOn(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
Annotation annotation = AnnotationUtils.findAnnotation(type, annotationType);
return annotation == null ? null : getValueFrom(annotation);
}
public Object findValueOn(Method method) {
Assert.notNull(method, "Method must nor be null!");
Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType);
return annotation == null ? null : getValueFrom(annotation);
}
public Object getValueFrom(AnnotatedElement element) {
Assert.notNull(element, "Annotated element must not be null!");
Annotation annotation = AnnotationUtils.getAnnotation(element, annotationType);
return annotation == null ? null : getValueFrom(annotation);
}
/**
* Returns the {@link Annotation} attribute's value from the given {@link Annotation}.
*
* @param annotation must not be {@literal null}.
* @return
*/
public Object getValueFrom(Annotation annotation) {
Assert.notNull(annotation, "Annotation must not be null!");
return attributeName == null ? AnnotationUtils.getValue(annotation)
: AnnotationUtils.getValue(annotation, attributeName);
}
}

View File

@@ -0,0 +1,170 @@
/*
* 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.
* 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.core;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
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<MethodParameter> parameters;
/**
* Creates a new {@link MethodParameters} from the given {@link Method}.
*
* @param method must not be {@literal null}.
*/
public MethodParameters(Method method) {
this(method, null);
}
/**
* Creates a new {@link MethodParameters} for the given {@link Method} and
* {@link AnnotationAttribute}. If the latter is given, method parameter names will be
* looked up from the annotation attribute if present.
*
* @param method must not be {@literal null}.
* @param namingAnnotation can be {@literal null}.
*/
public MethodParameters(Method method, AnnotationAttribute namingAnnotation) {
Assert.notNull(method);
this.parameters = new ArrayList<MethodParameter>();
for (int i = 0; i < method.getParameterTypes().length; i++) {
MethodParameter parameter = new AnnotationNamingMethodParameter(method, i,
namingAnnotation);
parameter.initParameterNameDiscovery(DISCOVERER);
parameters.add(parameter);
}
}
/**
* Returns all {@link MethodParameter}s.
*
* @return
*/
public List<MethodParameter> getParameters() {
return parameters;
}
/**
* Returns the {@link MethodParameter} with the given name or {@literal null} if none
* found.
*
* @param name must not be {@literal null} or empty.
* @return
*/
public MethodParameter getParameter(String name) {
Assert.hasText(name, "Parameter name must not be null!");
for (MethodParameter parameter : parameters) {
if (name.equals(parameter.getParameterName())) {
return parameter;
}
}
return null;
}
/**
* Returns all {@link MethodParameter}s annotated with the given annotation type.
*
* @param annotation must not be {@literal null}.
* @return
*/
public List<MethodParameter> getParametersWith(Class<? extends Annotation> annotation) {
Assert.notNull(annotation);
List<MethodParameter> result = new ArrayList<MethodParameter>();
for (MethodParameter parameter : getParameters()) {
if (parameter.hasParameterAnnotation(annotation)) {
result.add(parameter);
}
}
return result;
}
/**
* Custom {@link MethodParameter} extension that will favor the name configured in the
* {@link AnnotationAttribute} if set over discovering it.
*
* @author Oliver Gierke
*/
private static class AnnotationNamingMethodParameter extends MethodParameter {
private final AnnotationAttribute attribute;
private String name;
/**
* Creates a new {@link AnnotationNamingMethodParameter} for the given
* {@link Method}'s parameter with the given index.
*
* @param method must not be {@literal null}.
* @param parameterIndex
* @param attribute can be {@literal null}
*/
public AnnotationNamingMethodParameter(Method method, int parameterIndex,
AnnotationAttribute attribute) {
super(method, parameterIndex);
this.attribute = attribute;
}
/*
* (non-Javadoc)
*
* @see org.springframework.core.MethodParameter#getParameterName()
*/
@Override
public String getParameterName() {
if (name != null) {
return name;
}
if (attribute != null) {
Object foundName = attribute.getValueFrom(this);
if (foundName != null) {
name = foundName.toString();
return name;
}
}
name = super.getParameterName();
return name;
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2002-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.core;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/**
* Unit tests for {@link AnnotationAttribute}.
*
* @author Oliver Gierke
*/
public class AnnotationAttributeUnitTests {
AnnotationAttribute valueAttribute = new AnnotationAttribute(MyAnnotation.class);
AnnotationAttribute nonValueAttribute = new AnnotationAttribute(MyAnnotation.class,
"nonValue");
@Test
public void readsAttributesFromType() {
assertThat(valueAttribute.findValueOn(Sample.class), is((Object) "foo"));
assertThat(nonValueAttribute.findValueOn(Sample.class), is((Object) "bar"));
}
@Test
public void findsAttributesFromSubType() {
assertThat(valueAttribute.findValueOn(SampleSub.class), is((Object) "foo"));
}
@Test
public void doesNotGetValueFromSubTyp() {
assertThat(valueAttribute.getValueFrom(SampleSub.class), is(nullValue()));
}
@Retention(RetentionPolicy.RUNTIME)
public static @interface MyAnnotation {
String value() default "";
String nonValue() default "";
}
@MyAnnotation(value = "foo", nonValue = "bar")
static class Sample {
}
static class SampleSub extends Sample {
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.core;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/**
* Unit tests for {@link MethodParameters}.
*
* @author Oliver Gierke
*/
public class MethodParametersUnitTests {
@Test
public void prefersAnnotatedParameterOverDiscovered() throws Exception {
Method method = Sample.class.getMethod("method", String.class, String.class);
MethodParameters parameters = new MethodParameters(method,
new AnnotationAttribute(Qualifier.class));
assertThat(parameters.getParameter("param"), is(notNullValue()));
assertThat(parameters.getParameter("foo"), is(notNullValue()));
assertThat(parameters.getParameter("another"), is(nullValue()));
}
@Retention(RetentionPolicy.RUNTIME)
public static @interface Qualifier {
String value() default "";
}
static class Sample {
public void method(String param, @Qualifier("foo") String another) {
}
}
}

View File

@@ -554,13 +554,42 @@ public class UriComponentsBuilder {
return this;
}
public UriComponentsBuilder with(UriComponentsBuilder builder) {
UriComponents components = builder.build().normalize();
if (StringUtils.hasText(components.getScheme())) {
scheme(components.getScheme());
}
if (StringUtils.hasText(components.getHost())) {
host(components.getHost());
}
if (components.getPort() != -1) {
port(components.getPort());
}
if (StringUtils.hasText(components.getPath())) {
path(components.getPath());
}
if (StringUtils.hasText(components.getQuery())) {
query(components.getQuery());
}
if (StringUtils.hasText(components.getFragment())) {
fragment(components.getFragment());
}
return this;
}
private interface PathComponentBuilder {
PathComponent build();
}
private static class CompositePathComponentBuilder implements PathComponentBuilder {
private final LinkedList<PathComponentBuilder> componentBuilders = new LinkedList<PathComponentBuilder>();

View File

@@ -0,0 +1,160 @@
/*
* 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.
* 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.web.servlet.hypermedia;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.AnnotationAttribute;
import org.springframework.core.MethodParameter;
import org.springframework.core.MethodParameters;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.hypermedia.RecordedInvocationUtils.RecordedMethodInvocation;
import org.springframework.web.util.UriTemplate;
/**
* Value object to allow accessing {@link RecordedMethodInvocation} parameters with the
* configured {@link AnnotationAttribute}.
*
* @author Oliver Gierke
*/
class AnnotatedParametersParameterAccessor {
private final AnnotationAttribute attribute;
/**
* Creates a new {@link AnnotatedParametersParameterAccessor} using the given
* {@link AnnotationAttribute}.
*
* @param attribute must not be {@literal null}.
*/
public AnnotatedParametersParameterAccessor(AnnotationAttribute attribute) {
Assert.notNull(attribute);
this.attribute = attribute;
}
/**
* Returns {@link BoundMethodParameter}s contained in the given
* {@link RecordedMethodInvocation}.
*
* @param invocation must not be {@literal null}.
* @return
*/
public List<BoundMethodParameter> getBoundParameters(RecordedMethodInvocation invocation) {
Assert.notNull(invocation, "RecordedMethodInvocation must not be null!");
MethodParameters parameters = new MethodParameters(invocation.getMethod());
Object[] arguments = invocation.getArguments();
List<BoundMethodParameter> result = new ArrayList<BoundMethodParameter>();
for (MethodParameter parameter : parameters.getParametersWith(attribute.getAnnotationType())) {
result.add(new BoundMethodParameter(parameter,
arguments[parameter.getParameterIndex()], attribute));
}
return result;
}
/**
* Represents a {@link MethodParameter} alongside the value it has been bound to.
*
* @author Oliver Gierke
*/
static class BoundMethodParameter {
private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
private static final TypeDescriptor STRING_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
private final MethodParameter parameter;
private final Object value;
private final AnnotationAttribute attribute;
private final TypeDescriptor parameterTypeDecsriptor;
/**
* Creates a new {@link BoundMethodParameter}
*
* @param parameter
* @param value
* @param attribute
*/
public BoundMethodParameter(MethodParameter parameter, Object value,
AnnotationAttribute attribute) {
Assert.notNull(parameter, "MethodParameter must not be null!");
this.parameter = parameter;
this.value = value;
this.attribute = attribute;
this.parameterTypeDecsriptor = TypeDescriptor.nested(parameter, 0);
}
/**
* Returns the name of the {@link UriTemplate} variable to be bound. The name will
* be derived from the configured {@link AnnotationAttribute} or the
* {@link MethodParameter} name as fallback.
*
* @return
*/
public String getVariableName() {
if (attribute == null) {
return parameter.getParameterName();
}
Annotation annotation = parameter.getParameterAnnotation(attribute.getAnnotationType());
String annotationAttributeValue = attribute.getValueFrom(annotation).toString();
return StringUtils.hasText(annotationAttributeValue) ? annotationAttributeValue
: parameter.getParameterName();
}
/**
* Returns the raw value bound to the {@link MethodParameter}.
*
* @return
*/
public Object getValue() {
return value;
}
/**
* Returns the bound value converted into a {@link String} based on default
* conversion service setup.
*
* @return
*/
public String asString() {
if (value == null) {
return null;
}
return (String) CONVERSION_SERVICE.convert(value, parameterTypeDecsriptor,
STRING_DESCRIPTOR);
}
}
}

View File

@@ -0,0 +1,121 @@
/*
* 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.web.servlet.hypermedia;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.springframework.core.AnnotationAttribute;
import org.springframework.util.Assert;
/**
* {@link MappingDiscoverer} implementation that inspects mappings from a particular
* annotation.
*
* @author Oliver Gierke
*/
class AnnotationMappingDiscoverer {
private final AnnotationAttribute attribute;
/**
* 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<? extends Annotation> annotation) {
this(new AnnotationAttribute(annotation));
}
/**
* 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(AnnotationAttribute attribute) {
Assert.notNull(attribute);
this.attribute = attribute;
}
/**
* 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.
*/
public String getMapping(Class<?> type) {
String[] mapping = getMappingFrom(attribute.findValueOn(type));
if (mapping.length > 1) {
throw new IllegalStateException(String.format(
"Multiple class level mappings defined on class %s!", type.getName()));
}
return mapping.length == 0 ? null : mapping[0];
}
/**
* 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.
*/
public String getMapping(Method method) {
String[] mapping = getMappingFrom(attribute.findValueOn(method));
if (mapping.length > 1) {
throw new IllegalStateException(String.format(
"Multiple method level mappings defined on method %s!",
method.toString()));
}
String typeMapping = getMapping(method.getDeclaringClass());
if (mapping == null || mapping.length == 0) {
return typeMapping;
}
return typeMapping == null || "/".equals(typeMapping) ? mapping[0] : typeMapping
+ mapping[0];
}
private String[] getMappingFrom(Object annotationValue) {
if (annotationValue instanceof String) {
return new String[] { (String) annotationValue };
}
else if (annotationValue instanceof String[]) {
return (String[]) annotationValue;
}
else if (annotationValue == null) {
return new String[0];
}
throw new IllegalStateException(
String.format(
"Unsupported type for the mapping attribute! Support String and String[] but got %s!",
annotationValue.getClass()));
}
}

View File

@@ -0,0 +1,328 @@
/*
* 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.
* 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.web.servlet.hypermedia;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.AnnotationAttribute;
import org.springframework.core.MethodParameter;
import org.springframework.core.MethodParameters;
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.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.hypermedia.AnnotatedParametersParameterAccessor.BoundMethodParameter;
import org.springframework.web.servlet.hypermedia.RecordedInvocationUtils.LastInvocationAware;
import org.springframework.web.servlet.hypermedia.RecordedInvocationUtils.RecordedMethodInvocation;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponents;
//import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
import static org.springframework.web.servlet.hypermedia.RecordedInvocationUtils.*;
/**
* Builder to ease building {@link URI} instances pointing to Spring MVC controllers.
*
* @author Oliver Gierke
*/
public class MvcUriComponentsBuilder extends UriComponentsBuilder {
private static final AnnotationMappingDiscoverer DISCOVERER = new AnnotationMappingDiscoverer(
RequestMapping.class);
private static final AnnotatedParametersParameterAccessor PATH_VARIABLE_ACCESSOR = new AnnotatedParametersParameterAccessor(
new AnnotationAttribute(PathVariable.class));
private static final AnnotatedParametersParameterAccessor REQUEST_PARAM_ACCESSOR = new AnnotatedParametersParameterAccessor(
new AnnotationAttribute(RequestParam.class));
private final List<UriComponentsContributor> contributors;
@Autowired(required = false)
private RequestMappingHandlerAdapter adapter;
/**
* Creates a new {@link LinkBuilderSupport} to grab the
* {@link UriComponentsContributor}s registered in the
* {@link RequestMappingHandlerAdapter}.
*
* @param builder must not be {@literal null}.
*/
MvcUriComponentsBuilder() {
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
List<UriComponentsContributor> contributors = new ArrayList<UriComponentsContributor>();
if (adapter != null) {
for (HandlerMethodArgumentResolver resolver : adapter.getArgumentResolvers()) {
if (resolver instanceof UriComponentsContributor) {
contributors.add((UriComponentsContributor) resolver);
}
}
}
this.contributors = contributors;
}
/**
* Creates a new {@link MvcUriComponentsBuilder} with a base of the mapping annotated
* to the given controller class.
*
* @param controller the class to discover the annotation on, must not be
* {@literal null}.
* @return
*/
public static UriComponentsBuilder from(Class<?> controller) {
return from(controller, new Object[0]);
}
/**
* Creates a new {@link MvcUriComponentsBuilder} with a base of the mapping annotated
* to the given controller class. The additional parameters are used to fill up
* potentially available path variables in the class scop request mapping.
*
* @param controller the class to discover the annotation on, must not be
* {@literal null}.
* @param parameters additional parameters to bind to the URI template declared in the
* annotation, must not be {@literal null}.
* @return
*/
public static UriComponentsBuilder from(Class<?> controller, Object... parameters) {
Assert.notNull(controller);
String mapping = DISCOVERER.getMapping(controller);
UriTemplate template = new UriTemplate(mapping == null ? "/" : mapping);
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(template.expand(parameters));
return getRootBuilder().with(builder);
}
public static UriComponentsBuilder from(Method method, Object... parameters) {
MvcUriComponentsBuilder builder = new MvcUriComponentsBuilder();
return from(method, parameters, builder.contributors);
}
static UriComponentsBuilder from(Method method, Object[] parameters,
List<UriComponentsContributor> contributors) {
UriTemplate template = new UriTemplate(DISCOVERER.getMapping(method));
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(template.expand(parameters));
RecordedMethodInvocation invocation = getInvocation(method, parameters);
UriComponentsBuilder appender = applyUriComponentsContributer(invocation,
builder, contributors);
return getRootBuilder().with(appender);
}
/**
* Creates a {@link MvcUriComponentsBuilder} pointing to a controller method. Hand in
* a dummy method invocation result you can create via
* {@link #methodOn(Class, Object...)} or
* {@link RecordedInvocationUtils#methodOn(Class, Object...)}.
*
* <pre>
* @RequestMapping("/customers")
* class CustomerController {
*
* @RequestMapping("/{id}/addresses")
* HttpEntity&lt;Addresses&gt; showAddresses(@PathVariable Long id) { … }
* }
*
* URI uri = linkTo(methodOn(CustomerController.class).showAddresses(2L)).toURI();
* </pre>
*
* The resulting {@link URI} instance will point to {@code /customers/2/addresses}.
* For more details on the method invocation constraints, see
* {@link RecordedInvocationUtils#methodOn(Class, Object...)}.
*
* @param invocationValue
* @return
*/
/*
* (non-Javadoc)
*
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(java.lang.Object)
*/
public static UriComponentsBuilder from(Object invocationValue) {
MvcUriComponentsBuilder builder = new MvcUriComponentsBuilder();
return from(invocationValue, builder.contributors);
}
static UriComponentsBuilder from(Object invocationValue,
List<? extends UriComponentsContributor> contributors) {
Assert.isInstanceOf(LastInvocationAware.class, invocationValue);
LastInvocationAware invocations = (LastInvocationAware) invocationValue;
RecordedMethodInvocation invocation = invocations.getLastInvocation();
Iterator<Object> classMappingParameters = invocations.getObjectParameters();
Method method = invocation.getMethod();
String mapping = DISCOVERER.getMapping(method);
UriComponentsBuilder builder = getRootBuilder().path(mapping);
UriTemplate template = new UriTemplate(mapping);
Map<String, Object> values = new HashMap<String, Object>();
Iterator<String> names = template.getVariableNames().iterator();
while (classMappingParameters.hasNext()) {
values.put(names.next(), classMappingParameters.next());
}
for (BoundMethodParameter parameter : PATH_VARIABLE_ACCESSOR.getBoundParameters(invocation)) {
values.put(parameter.getVariableName(), parameter.asString());
}
for (BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR.getBoundParameters(invocation)) {
Object value = parameter.getValue();
String key = parameter.getVariableName();
if (value instanceof Collection) {
for (Object element : (Collection<?>) value) {
builder.queryParam(key, element);
}
}
else {
builder.queryParam(key, parameter.asString());
}
}
UriComponents components = applyUriComponentsContributer(invocation, builder,
contributors).buildAndExpand(values);
return UriComponentsBuilder.fromUri(components.toUri());
}
/**
* Wrapper for {@link RecordedInvocationUtils#methodOn(Class, Object...)} to be
* available in case you work with static imports of {@link MvcUriComponentsBuilder}.
*
* @param controller must not be {@literal null}.
* @param parameters parameters to extend template variables in the type level
* mapping.
* @return
*/
public static <T> T methodOn(Class<T> controller, Object... parameters) {
return RecordedInvocationUtils.methodOn(controller, parameters);
}
/**
* Returns a {@link UriComponentsBuilder} obtained from the current servlet mapping
* with the host tweaked in case the request contains an {@code X-Forwarded-Host}
* header.
*
* @return
*/
static UriComponentsBuilder getRootBuilder() {
HttpServletRequest request = getCurrentRequest();
UriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request);
String header = request.getHeader("X-Forwarded-Host");
if (!StringUtils.hasText(header)) {
return builder;
}
String[] hosts = StringUtils.commaDelimitedListToStringArray(header);
String hostToUse = hosts[0];
if (hostToUse.contains(":")) {
String[] hostAndPort = StringUtils.split(hostToUse, ":");
builder.host(hostAndPort[0]);
builder.port(Integer.parseInt(hostAndPort[1]));
}
else {
builder.host(hostToUse);
}
return builder;
}
/**
* 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
*/
private static UriComponentsBuilder applyUriComponentsContributer(
RecordedMethodInvocation invocation, UriComponentsBuilder builder,
Collection<? extends UriComponentsContributor> contributors) {
if (contributors.isEmpty()) {
return builder;
}
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 : contributors) {
if (contributor.supportsParameter(parameter)) {
contributor.enhance(builder, parameter, parameterValue);
}
}
}
return builder;
}
/**
* Copy of {@link ServletUriComponentsBuilder#getCurrentRequest()} until SPR-10110
* gets fixed.
*
* @return
*/
private static HttpServletRequest getCurrentRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
Assert.state(requestAttributes != null,
"Could not find current request via RequestContextHolder");
Assert.isInstanceOf(ServletRequestAttributes.class, requestAttributes);
HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
Assert.state(servletRequest != null, "Could not find current HttpServletRequest");
return servletRequest;
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2002-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.web.servlet.hypermedia;
import java.lang.reflect.Method;
import java.util.List;
import org.springframework.web.util.UriComponentsBuilder;
/**
*
* @author olivergierke
*/
public class MvcUriComponentsBuilderFactory implements MvcUris {
private final List<? extends UriComponentsContributor> contributors;
/**
* @param contributors
*/
public MvcUriComponentsBuilderFactory(
List<? extends UriComponentsContributor> contributors) {
this.contributors = contributors;
}
public UriComponentsBuilder from(Class<?> controller) {
return from(controller, new Object[0]);
}
public UriComponentsBuilder from(Class<?> controller, Object... parameters) {
return MvcUriComponentsBuilder.from(controller, parameters);
}
public UriComponentsBuilder from(Object invocationValue) {
return MvcUriComponentsBuilder.from(invocationValue, contributors);
}
public UriComponentsBuilder from(Method method, Object... parameters) {
return MvcUriComponentsBuilder.from(method, parameters, contributors);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2002-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.web.servlet.hypermedia;
import java.lang.reflect.Method;
import org.springframework.web.util.UriComponentsBuilder;
/**
*
* @author olivergierke
*/
public interface MvcUris {
UriComponentsBuilder from(Class<?> controller);
UriComponentsBuilder from(Class<?> controller, Object... parameters);
UriComponentsBuilder from(Object invocationValue);
UriComponentsBuilder from(Method method, Object... parameters);
}

View File

@@ -0,0 +1,240 @@
/*
* 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.
* 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.web.servlet.hypermedia;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Iterator;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.target.EmptyTargetSource;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Utility methods to capture dummy method invocations.
*
* @author Oliver Gierke
*/
class RecordedInvocationUtils {
private static ObjenesisStd OBJENESIS = new ObjenesisStd(true);
public interface LastInvocationAware {
Iterator<Object> getObjectParameters();
RecordedMethodInvocation 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,
org.springframework.cglib.proxy.MethodInterceptor {
private static final Method GET_INVOCATIONS;
private static final Method GET_OBJECT_PARAMETERS;
private final Object[] objectParameters;
private RecordedMethodInvocation 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.springframework.cglib.proxy.MethodInterceptor#intercept(java.lang.Object,
* java.lang.reflect.Method, java.lang.Object[],
* org.springframework.cglib.proxy.MethodProxy)
*/
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) {
if (GET_INVOCATIONS.equals(method)) {
return getLastInvocation();
}
else if (GET_OBJECT_PARAMETERS.equals(method)) {
return getObjectParameters();
}
else if (ReflectionUtils.isObjectMethod(method)) {
return ReflectionUtils.invokeMethod(method, obj, args);
}
this.invocation = new SimpleRecordedMethodInvocation(method, args);
Class<?> returnType = method.getReturnType();
return returnType.cast(getProxyWithInterceptor(returnType, this));
}
/*
* (non-Javadoc)
*
* @see
* org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept
* .MethodInvocation)
*/
@Override
public Object invoke(org.aopalliance.intercept.MethodInvocation invocation)
throws Throwable {
return intercept(invocation.getThis(), invocation.getMethod(),
invocation.getArguments(), null);
}
/*
* (non-Javadoc)
*
* @see org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware#
* getLastInvocation()
*/
@Override
public RecordedMethodInvocation getLastInvocation() {
return invocation;
}
/*
* (non-Javadoc)
*
* @see org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware#
* getObjectParameters()
*/
@Override
public Iterator<Object> 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.
*
* @param type must not be {@literal null}.
* @param parameters parameters to extend template variables in the type level
* mapping.
* @return
*/
public static <T> T methodOn(Class<T> type, Object... parameters) {
Assert.notNull(type, "Given type must not be null!");
InvocationRecordingMethodInterceptor interceptor = new InvocationRecordingMethodInterceptor(
parameters);
return getProxyWithInterceptor(type, interceptor);
}
static RecordedMethodInvocation getInvocation(Method method, Object[] parameters) {
return new SimpleRecordedMethodInvocation(method, parameters);
}
@SuppressWarnings("unchecked")
private static <T> T getProxyWithInterceptor(Class<?> type,
InvocationRecordingMethodInterceptor interceptor) {
if (type.isInterface()) {
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
factory.addInterface(type);
factory.addInterface(LastInvocationAware.class);
factory.addAdvice(interceptor);
return (T) factory.getProxy();
}
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type);
enhancer.setInterfaces(new Class<?>[] { LastInvocationAware.class });
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
Factory factory = (Factory) OBJENESIS.newInstance(enhancer.createClass());
factory.setCallbacks(new Callback[] { interceptor });
return (T) factory;
}
public interface RecordedMethodInvocation {
Object[] getArguments();
Method getMethod();
}
static class SimpleRecordedMethodInvocation implements RecordedMethodInvocation {
private final Method method;
private final Object[] arguments;
/**
* Creates a new {@link SimpleRecordedMethodInvocation} for the given
* {@link Method} and arguments.
*
* @param method must not be {@literal null}.
* @param arguments must not be {@literal null}.
*/
private SimpleRecordedMethodInvocation(Method method, Object[] arguments) {
Assert.notNull(method, "Method must not be null!");
Assert.notNull(arguments, "Arguments must not be null!");
this.arguments = arguments;
this.method = method;
}
@Override
public Object[] getArguments() {
return arguments;
}
@Override
public Method getMethod() {
return method;
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.web.servlet.hypermedia;
import org.springframework.core.MethodParameter;
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 MvcUriComponentsBuilderFactory#from(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

@@ -0,0 +1,112 @@
/*
* 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.
* 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.web.servlet.hypermedia;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.junit.Test;
import org.springframework.core.AnnotationAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/**
* Unit tests for {@link AnnotationMappingDiscoverer}.
*
* @author Oliver Gierke
*/
public class AnnotationMappingDiscovererUnitTests {
AnnotationMappingDiscoverer discoverer = new AnnotationMappingDiscoverer(
RequestMapping.class);
@Test(expected = IllegalArgumentException.class)
public void rejectsNullAnnotationType() {
new AnnotationMappingDiscoverer((Class<? extends Annotation>) null);
}
@Test(expected = IllegalArgumentException.class)
public void rejectsNullAnnotationAttribute() {
new AnnotationMappingDiscoverer((AnnotationAttribute) 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"));
}
@Test
public void resolvesMethodLevelMappingWithSlashRootMapping() throws Exception {
Method method = SlashRootMapping.class.getMethod("method");
assertThat(discoverer.getMapping(method), is("/method"));
}
/**
* @see #46
*/
@Test
public void treatsMissingMethodMappingAsEmptyMapping() throws Exception {
Method method = MyController.class.getMethod("noMethodMapping");
assertThat(discoverer.getMapping(method), is("/type"));
}
@RequestMapping("/type")
interface MyController {
@RequestMapping("/method")
void method();
@RequestMapping
void noMethodMapping();
}
interface ControllerWithoutTypeLevelMapping {
@RequestMapping("/method")
void method();
}
@RequestMapping("/")
interface SlashRootMapping {
@RequestMapping("/method")
void method();
}
}

View File

@@ -0,0 +1,131 @@
/*
* 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.
* 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.web.servlet.hypermedia;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.http.HttpEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.hypermedia.MvcUriComponentsBuilderUnitTests.PersonControllerImpl;
import org.springframework.web.servlet.hypermedia.MvcUriComponentsBuilderUnitTests.PersonsAddressesController;
import org.springframework.web.util.UriComponentsBuilder;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.web.servlet.hypermedia.MvcUriComponentsBuilder.*;
/**
* Unit tests for {@link MvcUriComponentsBuilderFactory}.
*
* @author Ricardo Gladwell
* @author Oliver Gierke
*/
public class MvcUriComponentsBuilderFactoryUnitTests extends TestUtils {
List<UriComponentsContributor> contributors = Collections.emptyList();
MvcUriComponentsBuilderFactory factory = new MvcUriComponentsBuilderFactory(
contributors);
@Test
public void createsLinkToControllerRoot() {
URI link = factory.from(PersonControllerImpl.class).build().toUri();
assertPointsToMockServer(link);
assertThat(link.toString(), endsWith("/people"));
}
@Test
public void createsLinkToParameterizedControllerRoot() {
URI link = factory.from(PersonsAddressesController.class, 15).build().toUri();
assertPointsToMockServer(link);
assertThat(link.toString(), endsWith("/people/15/addresses"));
}
@Test
public void appliesParameterValueIfContributorConfigured() {
List<? extends UriComponentsContributor> contributors = Arrays.asList(new SampleUriComponentsContributor());
MvcUriComponentsBuilderFactory factory = new MvcUriComponentsBuilderFactory(
contributors);
SpecialType specialType = new SpecialType();
specialType.parameterValue = "value";
URI link = factory.from(
methodOn(SampleController.class).sampleMethod(1L, specialType)).build().toUri();
assertPointsToMockServer(link);
assertThat(link.toString(), endsWith("/sample/1?foo=value"));
}
/**
* @see #57
*/
@Test
public void usesDateTimeFormatForUriBinding() {
DateTime now = DateTime.now();
MvcUriComponentsBuilderFactory factory = new MvcUriComponentsBuilderFactory(
contributors);
URI link = factory.from(methodOn(SampleController.class).sampleMethod(now)).build().toUri();
assertThat(link.toString(),
endsWith("/sample/" + ISODateTimeFormat.date().print(now)));
}
static interface SampleController {
@RequestMapping("/sample/{id}")
HttpEntity<?> sampleMethod(@PathVariable("id") Long id, SpecialType parameter);
@RequestMapping("/sample/{time}")
HttpEntity<?> sampleMethod(
@PathVariable("time") @DateTimeFormat(iso = ISO.DATE) DateTime time);
}
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;
}
}

View File

@@ -0,0 +1,250 @@
/*
* 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.
* 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.web.servlet.hypermedia;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.springframework.http.HttpEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.web.servlet.hypermedia.MvcUriComponentsBuilder.*;
/**
* Unit tests for {@link MvcUriComponentsBuilder}.
*
* @author Oliver Gierke
* @author Dietrich Schulten
*/
public class MvcUriComponentsBuilderUnitTests extends TestUtils {
@Test
public void createsLinkToControllerRoot() {
URI link = from(PersonControllerImpl.class).build().toUri();
assertThat(link.toString(), Matchers.endsWith("/people"));
}
@Test
public void createsLinkToParameterizedControllerRoot() {
URI link = from(PersonsAddressesController.class, 15).build().toUri();
assertThat(link.toString(), endsWith("/people/15/addresses"));
}
/**
* @see #70
*/
@Test
public void createsLinkToMethodOnParameterizedControllerRoot() {
URI link = from(
methodOn(PersonsAddressesController.class, 15).getAddressesForCountry(
"DE")).build().toUri();
assertThat(link.toString(), endsWith("/people/15/addresses/DE"));
}
@Test
public void createsLinkToSubResource() {
URI link = from(PersonControllerImpl.class).pathSegment("something").build().toUri();
assertThat(link.toString(), endsWith("/people/something"));
}
@Test(expected = IllegalStateException.class)
public void rejectsControllerWithMultipleMappings() {
from(InvalidController.class);
}
@Test
public void createsLinkToUnmappedController() {
URI link = from(UnmappedController.class).build().toUri();
assertThat(link.toString(), is("http://localhost/"));
}
@Test
public void appendingNullIsANoOp() {
URI link = from(PersonControllerImpl.class).path(null).build().toUri();
assertThat(link.toString(), endsWith("/people"));
}
@Test
public void linksToMethod() {
URI link = from(methodOn(ControllerWithMethods.class).myMethod(null)).build().toUri();
assertPointsToMockServer(link);
assertThat(link.toString(), endsWith("/something/else"));
}
@Test
public void linksToMethodWithPathVariable() {
URI link = from(methodOn(ControllerWithMethods.class).methodWithPathVariable("1")).build().toUri();
assertPointsToMockServer(link);
assertThat(link.toString(), endsWith("/something/1/foo"));
}
/**
* @see #33
*/
@Test
public void usesForwardedHostAsHostIfHeaderIsSet() {
request.addHeader("X-Forwarded-Host", "somethingDifferent");
URI link = from(PersonControllerImpl.class).build().toUri();
assertThat(link.toString(), startsWith("http://somethingDifferent"));
}
/**
* @see #26, #39
*/
@Test
public void linksToMethodWithPathVariableAndRequestParams() {
URI link = from(
methodOn(ControllerWithMethods.class).methodForNextPage("1", 10, 5)).build().toUri();
UriComponents components = toComponents(link);
assertThat(components.getPath(), is("/something/1/foo"));
MultiValueMap<String, String> queryParams = components.getQueryParams();
assertThat(queryParams.get("limit"), contains("5"));
assertThat(queryParams.get("offset"), contains("10"));
}
/**
* @see #26, #39
*/
@Test
public void linksToMethodWithPathVariableAndMultiValueRequestParams() {
URI link = from(
methodOn(ControllerWithMethods.class).methodWithMultiValueRequestParams(
"1", Arrays.asList(3, 7), 5)).build().toUri();
UriComponents components = toComponents(link);
assertThat(components.getPath(), is("/something/1/foo"));
MultiValueMap<String, String> queryParams = components.getQueryParams();
assertThat(queryParams.get("limit"), contains("5"));
assertThat(queryParams.get("items"), containsInAnyOrder("3", "7"));
}
/**
* @see #90
*/
@Test
public void usesForwardedHostAndPortFromHeader() {
request.addHeader("X-Forwarded-Host", "foobar:8088");
URI link = from(PersonControllerImpl.class).build().toUri();
assertThat(link.toString(), startsWith("http://foobar:8088"));
}
/**
* @see #90
*/
@Test
public void usesFirstHostOfXForwardedHost() {
request.addHeader("X-Forwarded-Host", "barfoo:8888, localhost:8088");
URI link = from(PersonControllerImpl.class).build().toUri();
assertThat(link.toString(), startsWith("http://barfoo:8888"));
}
private static UriComponents toComponents(URI link) {
return UriComponentsBuilder.fromUri(link).build();
}
static class Person {
Long id;
public Long getId() {
return id;
}
}
@RequestMapping("/people")
interface PersonController {
}
class PersonControllerImpl implements PersonController {
}
@RequestMapping("/people/{id}/addresses")
static class PersonsAddressesController {
@RequestMapping("/{country}")
public HttpEntity<Void> getAddressesForCountry(@PathVariable String country) {
return null;
}
}
@RequestMapping({ "/persons", "/people" })
class InvalidController {
}
class UnmappedController {
}
@RequestMapping("/something")
static class ControllerWithMethods {
@RequestMapping("/else")
HttpEntity<Void> myMethod(@RequestBody Object payload) {
return null;
}
@RequestMapping("/{id}/foo")
HttpEntity<Void> methodWithPathVariable(@PathVariable String id) {
return null;
}
@RequestMapping(value = "/{id}/foo")
HttpEntity<Void> methodForNextPage(@PathVariable String id,
@RequestParam Integer offset, @RequestParam Integer limit) {
return null;
}
@RequestMapping(value = "/{id}/foo")
HttpEntity<Void> methodWithMultiValueRequestParams(@PathVariable String id,
@RequestParam List<Integer> items, @RequestParam Integer limit) {
return null;
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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.web.servlet.hypermedia;
import org.junit.Test;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author Oliver Gierke
*/
public class RecordedInvocationUtilsUnitTests extends TestUtils {
@Test
public void test() {
MvcUriComponentsBuilder.from(RecordedInvocationUtils.methodOn(SampleController.class).someMethod(
1L));
}
@RequestMapping("/sample")
static class SampleController {
@RequestMapping("/{id}/foo")
HttpEntity<Void> someMethod(@PathVariable("id") Long id) {
return new ResponseEntity<Void>(HttpStatus.OK);
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.
* 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.web.servlet.hypermedia;
import java.net.URI;
import org.junit.Before;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
/**
* Utility class to ease tesing.
*
* @author Oliver Gierke
*/
public class TestUtils {
protected MockHttpServletRequest request;
@Before
public void setUp() {
request = new MockHttpServletRequest();
ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request);
RequestContextHolder.setRequestAttributes(requestAttributes);
}
protected void assertPointsToMockServer(URI link) {
assertThat(link.toString(), startsWith("http://localhost"));
}
}