diff --git a/build.gradle b/build.gradle index 75c51e2e02..1aa68b418f 100644 --- a/build.gradle +++ b/build.gradle @@ -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")) diff --git a/spring-core/src/main/java/org/springframework/core/AnnotationAttribute.java b/spring-core/src/main/java/org/springframework/core/AnnotationAttribute.java new file mode 100644 index 0000000000..96e44189fa --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/AnnotationAttribute.java @@ -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 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 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 annotationType, + String attributeName) { + + Assert.notNull(annotationType); + + this.annotationType = annotationType; + this.attributeName = attributeName; + } + + /** + * Returns the annotation type. + * + * @return the annotationType + */ + public Class 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); + } +} diff --git a/spring-core/src/main/java/org/springframework/core/MethodParameters.java b/spring-core/src/main/java/org/springframework/core/MethodParameters.java new file mode 100644 index 0000000000..61f36f219d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/MethodParameters.java @@ -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 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(); + + 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 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 getParametersWith(Class annotation) { + + Assert.notNull(annotation); + List result = new ArrayList(); + + 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; + } + } +} diff --git a/spring-core/src/test/java/org/springframework/core/AnnotationAttributeUnitTests.java b/spring-core/src/test/java/org/springframework/core/AnnotationAttributeUnitTests.java new file mode 100644 index 0000000000..a4dd43f055 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/AnnotationAttributeUnitTests.java @@ -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 { + + } +} diff --git a/spring-core/src/test/java/org/springframework/core/MethodParametersUnitTests.java b/spring-core/src/test/java/org/springframework/core/MethodParametersUnitTests.java new file mode 100644 index 0000000000..1517cb1059 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/MethodParametersUnitTests.java @@ -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) { + } + } +} diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index fa5e7aeba0..d3a4c93faa 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -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 componentBuilders = new LinkedList(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/AnnotatedParametersParameterAccessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/AnnotatedParametersParameterAccessor.java new file mode 100644 index 0000000000..78d74766da --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/AnnotatedParametersParameterAccessor.java @@ -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 getBoundParameters(RecordedMethodInvocation invocation) { + + Assert.notNull(invocation, "RecordedMethodInvocation must not be null!"); + + MethodParameters parameters = new MethodParameters(invocation.getMethod()); + Object[] arguments = invocation.getArguments(); + List result = new ArrayList(); + + 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); + } + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/AnnotationMappingDiscoverer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/AnnotationMappingDiscoverer.java new file mode 100644 index 0000000000..26e87a3d53 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/AnnotationMappingDiscoverer.java @@ -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 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())); + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilder.java new file mode 100755 index 0000000000..0fd76c6ab5 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilder.java @@ -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 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 contributors = new ArrayList(); + + 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 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...)}. + * + *
+	 * @RequestMapping("/customers")
+	 * class CustomerController {
+	 * 
+	 *   @RequestMapping("/{id}/addresses")
+	 *   HttpEntity<Addresses> showAddresses(@PathVariable Long id) { … } 
+	 * }
+	 * 
+	 * URI uri = linkTo(methodOn(CustomerController.class).showAddresses(2L)).toURI();
+	 * 
+ * + * 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 contributors) { + + Assert.isInstanceOf(LastInvocationAware.class, invocationValue); + LastInvocationAware invocations = (LastInvocationAware) invocationValue; + + RecordedMethodInvocation invocation = invocations.getLastInvocation(); + Iterator classMappingParameters = invocations.getObjectParameters(); + Method method = invocation.getMethod(); + + String mapping = DISCOVERER.getMapping(method); + UriComponentsBuilder builder = getRootBuilder().path(mapping); + + UriTemplate template = new UriTemplate(mapping); + Map values = new HashMap(); + + Iterator 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 methodOn(Class 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 contributors) { + + if (contributors.isEmpty()) { + return builder; + } + + MethodParameters parameters = new MethodParameters(invocation.getMethod()); + Iterator 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; + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderFactory.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderFactory.java new file mode 100644 index 0000000000..5ee28624e6 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderFactory.java @@ -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 contributors; + + /** + * @param contributors + */ + public MvcUriComponentsBuilderFactory( + List 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); + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUris.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUris.java new file mode 100644 index 0000000000..6815547f0b --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUris.java @@ -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); +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/RecordedInvocationUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/RecordedInvocationUtils.java new file mode 100644 index 0000000000..bdf9a42111 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/RecordedInvocationUtils.java @@ -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 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 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 methodOn(Class 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 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; + } + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/UriComponentsContributor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/UriComponentsContributor.java new file mode 100644 index 0000000000..796d2692f0 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/UriComponentsContributor.java @@ -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); +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/AnnotationMappingDiscovererUnitTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/AnnotationMappingDiscovererUnitTests.java new file mode 100644 index 0000000000..4e68cbcf45 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/AnnotationMappingDiscovererUnitTests.java @@ -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) 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(); + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderFactoryUnitTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderFactoryUnitTests.java new file mode 100644 index 0000000000..5e52559e99 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderFactoryUnitTests.java @@ -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 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 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; + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderUnitTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderUnitTests.java new file mode 100644 index 0000000000..f3ec765d32 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderUnitTests.java @@ -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 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 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 getAddressesForCountry(@PathVariable String country) { + return null; + } + } + + @RequestMapping({ "/persons", "/people" }) + class InvalidController { + + } + + class UnmappedController { + + } + + @RequestMapping("/something") + static class ControllerWithMethods { + + @RequestMapping("/else") + HttpEntity myMethod(@RequestBody Object payload) { + return null; + } + + @RequestMapping("/{id}/foo") + HttpEntity methodWithPathVariable(@PathVariable String id) { + return null; + } + + @RequestMapping(value = "/{id}/foo") + HttpEntity methodForNextPage(@PathVariable String id, + @RequestParam Integer offset, @RequestParam Integer limit) { + return null; + } + + @RequestMapping(value = "/{id}/foo") + HttpEntity methodWithMultiValueRequestParams(@PathVariable String id, + @RequestParam List items, @RequestParam Integer limit) { + return null; + } + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/RecordedInvocationUtilsUnitTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/RecordedInvocationUtilsUnitTests.java new file mode 100644 index 0000000000..9e24f124f1 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/RecordedInvocationUtilsUnitTests.java @@ -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 someMethod(@PathVariable("id") Long id) { + return new ResponseEntity(HttpStatus.OK); + } + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/TestUtils.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/TestUtils.java new file mode 100644 index 0000000000..742707decf --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/TestUtils.java @@ -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")); + } +}