From bafc73f1477330ef1bdb292608deea6e06ef2bed Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 20 Oct 2013 10:01:54 -0400 Subject: [PATCH] Integrate suggested support for creating MVC URLs The key contract is MvcUrls. An instance is automatically created with the Spring MVC namespace and the MVC Java config but can also be easily created in any configuration. Some example tests can be found in DefaultMvcUrlsTests. Issue: SPR-10665, SPR-8826 --- build.gradle | 1 + .../core/AnnotationAttribute.java | 125 ------- .../core/MethodParameters.java | 170 --------- .../core/AnnotationAttributeUnitTests.java | 72 ---- .../core/MethodParametersUnitTests.java | 58 ---- .../RequestParamMethodArgumentResolver.java | 46 ++- .../support/UriComponentsContributor.java | 56 +++ .../web/util/UriComponentsBuilder.java | 69 ++-- .../AnnotationDrivenBeanDefinitionParser.java | 8 + .../config/DefaultMvcUrlsFactoryBean.java | 89 +++++ .../WebMvcConfigurationSupport.java | 11 + .../AnnotatedParametersParameterAccessor.java | 160 --------- .../AnnotationMappingDiscoverer.java | 121 ------- .../hypermedia/MvcUriComponentsBuilder.java | 328 ------------------ .../MvcUriComponentsBuilderFactory.java | 55 --- .../web/servlet/hypermedia/MvcUris.java | 36 -- .../hypermedia/RecordedInvocationUtils.java | 240 ------------- .../hypermedia/UriComponentsContributor.java | 51 --- .../PathVariableMethodArgumentResolver.java | 29 +- .../RequestMappingHandlerAdapter.java | 5 +- .../servlet/mvc/support/DefaultMvcUrls.java | 181 ++++++++++ .../web/servlet/mvc/support/MvcUrlUtils.java | 207 +++++++++++ .../web/servlet/mvc/support/MvcUrls.java | 121 +++++++ .../support/ServletUriComponentsBuilder.java | 12 +- .../web/servlet/config/MvcNamespaceTests.java | 38 +- .../WebMvcConfigurationSupportTests.java | 111 ++++-- .../AnnotationMappingDiscovererUnitTests.java | 112 ------ ...cUriComponentsBuilderFactoryUnitTests.java | 131 ------- .../MvcUriComponentsBuilderUnitTests.java | 250 ------------- .../RecordedInvocationUtilsUnitTests.java | 47 --- .../web/servlet/hypermedia/TestUtils.java | 49 --- .../mvc/support/DefaultMvcUrlsTests.java | 290 ++++++++++++++++ .../servlet/mvc/support/MvcUrlUtilsTests.java | 122 +++++++ 33 files changed, 1313 insertions(+), 2088 deletions(-) delete mode 100644 spring-core/src/main/java/org/springframework/core/AnnotationAttribute.java delete mode 100644 spring-core/src/main/java/org/springframework/core/MethodParameters.java delete mode 100644 spring-core/src/test/java/org/springframework/core/AnnotationAttributeUnitTests.java delete mode 100644 spring-core/src/test/java/org/springframework/core/MethodParametersUnitTests.java create mode 100644 spring-web/src/main/java/org/springframework/web/method/support/UriComponentsContributor.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/config/DefaultMvcUrlsFactoryBean.java delete mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/AnnotatedParametersParameterAccessor.java delete mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/AnnotationMappingDiscoverer.java delete mode 100755 spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilder.java delete mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderFactory.java delete mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUris.java delete mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/RecordedInvocationUtils.java delete mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/UriComponentsContributor.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultMvcUrls.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/MvcUrlUtils.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/MvcUrls.java delete mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/AnnotationMappingDiscovererUnitTests.java delete mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderFactoryUnitTests.java delete mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderUnitTests.java delete mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/RecordedInvocationUtilsUnitTests.java delete mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/TestUtils.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultMvcUrlsTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/MvcUrlUtilsTests.java diff --git a/build.gradle b/build.gradle index 1aa68b418f..000e28dafe 100644 --- a/build.gradle +++ b/build.gradle @@ -689,6 +689,7 @@ project("spring-webmvc") { testCompile("commons-io:commons-io:1.3") testCompile("org.hibernate:hibernate-validator:4.3.0.Final") testCompile("org.apache.httpcomponents:httpclient:4.3-beta2") + testCompile("joda-time:joda-time:2.2") } // pick up DispatcherServlet.properties in src/main diff --git a/spring-core/src/main/java/org/springframework/core/AnnotationAttribute.java b/spring-core/src/main/java/org/springframework/core/AnnotationAttribute.java deleted file mode 100644 index 96e44189fa..0000000000 --- a/spring-core/src/main/java/org/springframework/core/AnnotationAttribute.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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 deleted file mode 100644 index 61f36f219d..0000000000 --- a/spring-core/src/main/java/org/springframework/core/MethodParameters.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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 deleted file mode 100644 index a4dd43f055..0000000000 --- a/spring-core/src/test/java/org/springframework/core/AnnotationAttributeUnitTests.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 deleted file mode 100644 index 1517cb1059..0000000000 --- a/spring-core/src/test/java/org/springframework/core/MethodParametersUnitTests.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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/method/annotation/RequestParamMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java index 5ec3acbbe1..1adce271ba 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java @@ -29,6 +29,8 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.GenericCollectionTypeResolver; import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -38,10 +40,12 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.ValueConstants; import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.UriComponentsContributor; import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartResolver; +import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.WebUtils; /** @@ -69,7 +73,10 @@ import org.springframework.web.util.WebUtils; * @since 3.1 * @see RequestParamMapMethodArgumentResolver */ -public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { +public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver + implements UriComponentsContributor { + + private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class); private final boolean useDefaultResolution; @@ -83,7 +90,8 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod * request parameter name is derived from the method parameter name. */ public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory, - boolean useDefaultResolution) { + boolean useDefaultResolution) { + super(beanFactory); this.useDefaultResolution = useDefaultResolution; } @@ -218,6 +226,39 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod throw new MissingServletRequestParameterException(paramName, parameter.getParameterType().getSimpleName()); } + @Override + public void contributeMethodArgument(MethodParameter parameter, Object value, + UriComponentsBuilder builder, Map uriVariables, ConversionService conversionService) { + + Class paramType = parameter.getParameterType(); + if (Map.class.isAssignableFrom(paramType) || MultipartFile.class.equals(paramType) || + "javax.servlet.http.Part".equals(paramType.getName())) { + return; + } + + RequestParam annot = parameter.getParameterAnnotation(RequestParam.class); + String name = StringUtils.isEmpty(annot.value()) ? parameter.getParameterName() : annot.value(); + + if (value == null) { + builder.queryParam(name); + } + else if (value instanceof Collection) { + for (Object v : (Collection) value) { + v = formatUriValue(conversionService, TypeDescriptor.nested(parameter, 1), v); + builder.queryParam(name, v); + } + } + else { + builder.queryParam(name, formatUriValue(conversionService, new TypeDescriptor(parameter), value)); + } + } + + protected String formatUriValue(ConversionService cs, TypeDescriptor sourceType, Object value) { + return (cs != null) ? + (String) cs.convert(value, sourceType, STRING_TYPE_DESCRIPTOR) : null; + } + + private class RequestParamNamedValueInfo extends NamedValueInfo { private RequestParamNamedValueInfo() { @@ -228,4 +269,5 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod super(annotation.value(), annotation.required(), annotation.defaultValue()); } } + } diff --git a/spring-web/src/main/java/org/springframework/web/method/support/UriComponentsContributor.java b/spring-web/src/main/java/org/springframework/web/method/support/UriComponentsContributor.java new file mode 100644 index 0000000000..1fdb3d5810 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/method/support/UriComponentsContributor.java @@ -0,0 +1,56 @@ +/* + * 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.method.support; + +import java.util.Map; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Strategy for contributing to the building of a {@link UriComponents} by + * looking at a method parameter and an argument value and deciding what + * part of the target URL should be updated. + * + * @author Oliver Gierke + * @author Rossen Stoyanchev + * @since 4.0 + */ +public interface UriComponentsContributor { + + /** + * Whether this contributor supports the given method parameter. + */ + boolean supportsParameter(MethodParameter parameter); + + /** + * Process the given method argument and either update the + * {@link UriComponentsBuilder} or add to the map with URI variables to use to + * expand the URI after all arguments are processed. + * + * @param parameter the controller method parameter, never {@literal null}. + * @param value the argument value, possibly {@literal null}. + * @param builder the builder to update, never {@literal null}. + * @param uriVariables a map to add URI variables to, never {@literal null}. + * @param conversionService a ConversionService to format values as Strings + */ + void contributeMethodArgument(MethodParameter parameter, Object value, UriComponentsBuilder builder, + Map uriVariables, ConversionService conversionService); + +} 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 d3a4c93faa..fe530ff35f 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 @@ -248,7 +248,7 @@ public class UriComponentsBuilder { throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL"); } } - + // build methods @@ -362,6 +362,43 @@ public class UriComponentsBuilder { return this; } + /** + * Initializes all components of this URI builder from the given {@link UriComponents}. + * @param uriComponents the UriComponents instance + * @return this UriComponentsBuilder + */ + public UriComponentsBuilder uriComponents(UriComponents uriComponents) { + Assert.notNull(uriComponents, "'uriComponents' must not be null"); + this.scheme = uriComponents.getScheme(); + if (uriComponents instanceof OpaqueUriComponents) { + this.ssp = uriComponents.getSchemeSpecificPart(); + resetHierarchicalComponents(); + } + else { + if (uriComponents.getUserInfo() != null) { + this.userInfo = uriComponents.getUserInfo(); + } + if (uriComponents.getHost() != null) { + this.host = uriComponents.getHost(); + } + if (uriComponents.getPort() != -1) { + this.port = uriComponents.getPort(); + } + if (StringUtils.hasLength(uriComponents.getPath())) { + this.pathBuilder = new CompositePathComponentBuilder(uriComponents.getPath()); + } + if (!uriComponents.getQueryParams().isEmpty()) { + this.queryParams.clear(); + this.queryParams.putAll(uriComponents.getQueryParams()); + } + resetSchemeSpecificPart(); + } + if (uriComponents.getFragment() != null) { + this.fragment = uriComponents.getFragment(); + } + return this; + } + /** * Set the URI scheme-specific-part. When invoked, this method overwrites * {@linkplain #userInfo(String) user-info}, {@linkplain #host(String) host}, @@ -554,36 +591,6 @@ 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 { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index 9a18935981..dcf8afee92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -207,6 +207,13 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { handlerAdapterDef.getPropertyValues().add("deferredResultInterceptors", deferredResultInterceptors); String handlerAdapterName = parserContext.getReaderContext().registerWithGeneratedName(handlerAdapterDef); + String mvcUrlsName = "mvcUrls"; + RootBeanDefinition mvcUrlsDef = new RootBeanDefinition(DefaultMvcUrlsFactoryBean.class); + mvcUrlsDef.setSource(source); + mvcUrlsDef.getPropertyValues().addPropertyValue("handlerAdapter", handlerAdapterDef); + mvcUrlsDef.getPropertyValues().addPropertyValue("conversionService", conversionService); + parserContext.getReaderContext().getRegistry().registerBeanDefinition(mvcUrlsName, mvcUrlsDef); + RootBeanDefinition csInterceptorDef = new RootBeanDefinition(ConversionServiceExposingInterceptor.class); csInterceptorDef.setSource(source); csInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, conversionService); @@ -242,6 +249,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, methodMappingName)); parserContext.registerComponent(new BeanComponentDefinition(handlerAdapterDef, handlerAdapterName)); + parserContext.registerComponent(new BeanComponentDefinition(mvcUrlsDef, mvcUrlsName)); parserContext.registerComponent(new BeanComponentDefinition(exceptionHandlerExceptionResolver, methodExceptionResolverName)); parserContext.registerComponent(new BeanComponentDefinition(responseStatusExceptionResolver, responseStatusExceptionResolverName)); parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExceptionResolverName)); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/DefaultMvcUrlsFactoryBean.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/DefaultMvcUrlsFactoryBean.java new file mode 100644 index 0000000000..34c207e9bc --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/DefaultMvcUrlsFactoryBean.java @@ -0,0 +1,89 @@ +/* + * 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.config; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.convert.ConversionService; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.UriComponentsContributor; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.support.DefaultMvcUrls; +import org.springframework.web.servlet.mvc.support.MvcUrls; + + +/** + * A factory bean for creating an instance of {@link MvcUrls} that discovers + * {@link UriComponentsContributor}s by obtaining the + * {@link HandlerMethodArgumentResolver}s configured in a + * {@link RequestMappingHandlerAdapter}. + *

+ * This is mainly provided as a convenience in XML configuration. Otherwise call the + * constructors of {@link DefaultMvcUrls} directly. Also note the MVC Java config and + * XML namespace already create an instance of {@link MvcUrls} and when using either + * of them this {@code FactoryBean} is not needed. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class DefaultMvcUrlsFactoryBean implements InitializingBean, FactoryBean { + + private RequestMappingHandlerAdapter handlerAdapter; + + private ConversionService conversionService; + + private MvcUrls mvcUrls; + + + /** + * Provide a {@link RequestMappingHandlerAdapter} from which to obtain + * the list of configured {@link HandlerMethodArgumentResolver}s. This + * is provided for ease of configuration in XML. + */ + public void setHandlerAdapter(RequestMappingHandlerAdapter handlerAdapter) { + this.handlerAdapter = handlerAdapter; + } + + /** + * Configure the {@link ConversionService} instance that {@link MvcUrls} should + * use to format Object values being added to a URI. + */ + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.mvcUrls = new DefaultMvcUrls(this.handlerAdapter.getArgumentResolvers(), this.conversionService); + } + + @Override + public MvcUrls getObject() throws Exception { + return this.mvcUrls; + } + + @Override + public Class getObjectType() { + return MvcUrls.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 5ac4c22ff7..c1f3cab17c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -78,6 +78,8 @@ import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExc import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.support.DefaultMvcUrls; +import org.springframework.web.servlet.mvc.support.MvcUrls; /** * This is the main class providing the configuration behind the MVC Java config. @@ -563,6 +565,15 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv public void configureAsyncSupport(AsyncSupportConfigurer configurer) { } + /** + * Return an instance of {@link MvcUrls} for injection into controllers to create + * URLs by referencing controllers and controller methods. + */ + @Bean + public MvcUrls mvcUrls() { + return new DefaultMvcUrls(requestMappingHandlerAdapter().getArgumentResolvers(), mvcConversionService()); + } + /** * Returns a {@link HttpRequestHandlerAdapter} for processing requests * with {@link HttpRequestHandler}s. 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 deleted file mode 100644 index 78d74766da..0000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/AnnotatedParametersParameterAccessor.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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 deleted file mode 100644 index 26e87a3d53..0000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/AnnotationMappingDiscoverer.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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 deleted file mode 100755 index 0fd76c6ab5..0000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilder.java +++ /dev/null @@ -1,328 +0,0 @@ -/* - * 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 deleted file mode 100644 index 5ee28624e6..0000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderFactory.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 deleted file mode 100644 index 6815547f0b..0000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/MvcUris.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 deleted file mode 100644 index bdf9a42111..0000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/RecordedInvocationUtils.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * 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 deleted file mode 100644 index 796d2692f0..0000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/hypermedia/UriComponentsContributor.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java index efc48fd704..8cdac3db14 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java @@ -21,6 +21,8 @@ import java.util.HashMap; import java.util.Map; import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.util.StringUtils; import org.springframework.web.bind.ServletRequestBindingException; @@ -32,8 +34,10 @@ import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver; import org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.method.support.UriComponentsContributor; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.View; +import org.springframework.web.util.UriComponentsBuilder; /** * Resolves method arguments annotated with an @{@link PathVariable}. @@ -59,7 +63,11 @@ import org.springframework.web.servlet.View; * @author Arjen Poutsma * @since 3.1 */ -public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { +public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver + implements UriComponentsContributor { + + private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class); + public PathVariableMethodArgumentResolver() { super(null); @@ -114,6 +122,25 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod pathVars.put(name, arg); } + @Override + public void contributeMethodArgument(MethodParameter parameter, Object value, + UriComponentsBuilder builder, Map uriVariables, ConversionService conversionService) { + + if (Map.class.isAssignableFrom(parameter.getParameterType())) { + return; + } + + PathVariable annot = parameter.getParameterAnnotation(PathVariable.class); + String name = StringUtils.isEmpty(annot.value()) ? parameter.getParameterName() : annot.value(); + + if (conversionService != null) { + value = conversionService.convert(value, new TypeDescriptor(parameter), STRING_TYPE_DESCRIPTOR); + } + + uriVariables.put(name, value); + } + + private static class PathVariableNamedValueInfo extends NamedValueInfo { private PathVariableNamedValueInfo(PathVariable annotation) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index bf5a17505e..eb11922001 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -26,6 +26,7 @@ import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -219,7 +220,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i * not initialized yet via {@link #afterPropertiesSet()}. */ public List getArgumentResolvers() { - return this.argumentResolvers.getResolvers(); + return (this.argumentResolvers != null) ? this.argumentResolvers.getResolvers() : null; } /** @@ -240,7 +241,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i * {@code null} if not initialized yet via {@link #afterPropertiesSet()}. */ public List getInitBinderArgumentResolvers() { - return this.initBinderArgumentResolvers.getResolvers(); + return (this.initBinderArgumentResolvers != null) ? this.initBinderArgumentResolvers.getResolvers() : null; } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultMvcUrls.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultMvcUrls.java new file mode 100644 index 0000000000..f5749619d5 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultMvcUrls.java @@ -0,0 +1,181 @@ +/* + * 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.mvc.support; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.UriComponentsContributor; +import org.springframework.web.servlet.mvc.support.MvcUrlUtils.ControllerMethodValues; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriTemplate; + +/** + * A default {@link MvcUrls} implementation. + * + * @author Oliver Gierke + * @author Rossen Stoyanchev + * + * @since 4.0 + */ +public class DefaultMvcUrls implements MvcUrls { + + private static final ParameterNameDiscoverer parameterNameDiscoverer = + new LocalVariableTableParameterNameDiscoverer(); + + + private final List contributors = new ArrayList(); + + private final ConversionService conversionService; + + + /** + * Create an instance providing a collection of {@link UriComponentsContributor}s or + * {@link HandlerMethodArgumentResolver}s. Since both of these tend to be implemented + * by the same class, the most convenient option is to obtain the configured + * {@code HandlerMethodArgumentResolvers} in the {@code RequestMappingHandlerAdapter} + * and provide that to this contstructor. + * + * @param uriComponentsContributors a collection of {@link UriComponentsContributor} + * or {@link HandlerMethodArgumentResolver}s. + */ + public DefaultMvcUrls(Collection uriComponentsContributors) { + this(uriComponentsContributors, null); + } + + /** + * Create an instance providing a collection of {@link UriComponentsContributor}s or + * {@link HandlerMethodArgumentResolver}s. Since both of these tend to be implemented + * by the same class, the most convenient option is to obtain the configured + * {@code HandlerMethodArgumentResolvers} in the {@code RequestMappingHandlerAdapter} + * and provide that to this contstructor. + *

+ * If the {@link ConversionService} argument is {@code null}, + * {@link DefaultFormattingConversionService} will be used by default. + * + * @param uriComponentsContributors a collection of {@link UriComponentsContributor} + * or {@link HandlerMethodArgumentResolver}s. + * @param conversionService a ConversionService to use when method argument values + * need to be formatted as Strings before being added to the URI + */ + public DefaultMvcUrls(Collection uriComponentsContributors, ConversionService conversionService) { + + Assert.notNull(uriComponentsContributors, "'uriComponentsContributors' must not be null"); + + for (Object contributor : uriComponentsContributors) { + if (contributor instanceof UriComponentsContributor) { + this.contributors.add((UriComponentsContributor) contributor); + } + } + + this.conversionService = (conversionService != null) ? + conversionService : new DefaultFormattingConversionService(); + } + + + @Override + public UriComponentsBuilder linkToController(Class controllerClass) { + String mapping = MvcUrlUtils.getTypeLevelMapping(controllerClass); + return ServletUriComponentsBuilder.fromCurrentServletMapping().path(mapping); + } + + @Override + public UriComponents linkToMethod(Method method, Object... argumentValues) { + String mapping = MvcUrlUtils.getMethodMapping(method); + UriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentServletMapping().path(mapping); + Map uriVars = new HashMap(); + return applyContributers(builder, method, argumentValues, uriVars); + } + + private UriComponents applyContributers(UriComponentsBuilder builder, Method method, + Object[] argumentValues, Map uriVars) { + + if (this.contributors.isEmpty()) { + return builder.buildAndExpand(uriVars); + } + + int paramCount = method.getParameters().length; + int argCount = argumentValues.length; + + Assert.isTrue(paramCount == argCount, "Number of method parameters " + paramCount + + " does not match number of argument values " + argCount); + + for (int i=0; i < paramCount; i++) { + MethodParameter param = new MethodParameter(method, i); + param.initParameterNameDiscovery(parameterNameDiscoverer); + for (UriComponentsContributor c : this.contributors) { + if (c.supportsParameter(param)) { + c.contributeMethodArgument(param, argumentValues[i], builder, uriVars, this.conversionService); + break; + } + } + } + + return builder.buildAndExpand(uriVars); + } + + @Override + public UriComponents linkToMethodOn(Object mockController) { + + Assert.isInstanceOf(ControllerMethodValues.class, mockController); + ControllerMethodValues controllerMethodValues = (ControllerMethodValues) mockController; + + Method method = controllerMethodValues.getControllerMethod(); + Object[] argumentValues = controllerMethodValues.getArgumentValues(); + + Map uriVars = new HashMap(); + addTypeLevelUriVaris(controllerMethodValues, uriVars); + + String mapping = MvcUrlUtils.getMethodMapping(method); + UriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentServletMapping().path(mapping); + + return applyContributers(builder, method, argumentValues, uriVars); + } + + private void addTypeLevelUriVaris(ControllerMethodValues info, Map uriVariables) { + + Object[] values = info.getTypeLevelUriVariables(); + if (!ObjectUtils.isEmpty(values)) { + + String mapping = MvcUrlUtils.getTypeLevelMapping(info.getControllerMethod().getDeclaringClass()); + + List names = new UriTemplate(mapping).getVariableNames(); + Assert.isTrue(names.size() == values.length, "The provided type-level URI template variables " + + Arrays.toString(values) + " do not match the template " + mapping); + + for (int i=0; i < names.size(); i++) { + uriVariables.put(names.get(i), values[i]); + } + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/MvcUrlUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/MvcUrlUtils.java new file mode 100644 index 0000000000..11567e2068 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/MvcUrlUtils.java @@ -0,0 +1,207 @@ +/* + * 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.mvc.support; + +import java.lang.reflect.Method; +import java.util.Set; + +import org.aopalliance.intercept.MethodInterceptor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.core.annotation.AnnotationUtils; +import org.springframework.objenesis.ObjenesisStd; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; +import org.springframework.web.util.UriComponents; + +/** + * Utility methods to support the creation URLs to Spring MVC controllers and controller + * methods. + * + * @author Oliver Gierke + * @author Rossen Stoyanchev + * + * @since 4.0 + */ +public class MvcUrlUtils { + + private static Log logger = LogFactory.getLog(MvcUrlUtils.class); + + private final static ObjenesisStd OBJENESIS = new ObjenesisStd(true); + + + /** + * Extract the type-level URL mapping or return an empty String. If multiple mappings + * are found, the first one is used. + */ + public static String getTypeLevelMapping(Class controllerType) { + Assert.notNull(controllerType, "'controllerType' must not be null"); + RequestMapping annot = AnnotationUtils.findAnnotation(controllerType, RequestMapping.class); + if ((annot == null) || ObjectUtils.isEmpty(annot.value())) { + return "/"; + } + if (annot.value().length > 1) { + logger.warn("Multiple class level mappings on " + controllerType.getName() + ", using the first one"); + } + return annot.value()[0]; + + } + + /** + * Extract the mapping from the given controller method, including both type and + * method-level mappings. If multiple mappings are found, the first one is used. + */ + public static String getMethodMapping(Method method) { + RequestMapping methodAnnot = AnnotationUtils.findAnnotation(method, RequestMapping.class); + Assert.notNull(methodAnnot, "No mappings on " + method.toGenericString()); + PatternsRequestCondition condition = new PatternsRequestCondition(methodAnnot.value()); + + RequestMapping typeAnnot = AnnotationUtils.findAnnotation(method.getDeclaringClass(), RequestMapping.class); + if (typeAnnot != null) { + condition = new PatternsRequestCondition(typeAnnot.value()).combine(condition); + } + + Set patterns = condition.getPatterns(); + if (patterns.size() > 1) { + logger.warn("Multiple mappings on " + method.toGenericString() + ", using the first one"); + } + + return (patterns.size() == 0) ? "/" : patterns.iterator().next(); + } + + /** + * Return a "mock" controller instance. When a controller method is invoked, the + * invoked method and argument values are remembered, and a "mock" value is returned + * so it can be used to help prepare a {@link UriComponents} through + * {@link MvcUrls#linkToMethodOn(Object)}. + * + * @param controllerType the type of controller to mock, must not be {@literal null}. + * @param typeLevelUriVariables URI variables to expand into the type-level mapping + * @return the created controller instance + */ + public static T controller(Class controllerType, Object... typeLevelUriVariables) { + Assert.notNull(controllerType, "'type' must not be null"); + return initProxy(controllerType, new ControllerMethodInvocationInterceptor(typeLevelUriVariables)); + } + + @SuppressWarnings("unchecked") + private static T initProxy(Class type, ControllerMethodInvocationInterceptor interceptor) { + + if (type.isInterface()) { + ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); + factory.addInterface(type); + factory.addInterface(ControllerMethodValues.class); + factory.addAdvice(interceptor); + return (T) factory.getProxy(); + } + else { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(type); + enhancer.setInterfaces(new Class[] { ControllerMethodValues.class }); + enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class); + + Factory factory = (Factory) OBJENESIS.newInstance(enhancer.createClass()); + factory.setCallbacks(new Callback[] { interceptor }); + return (T) factory; + } + } + + + private static class ControllerMethodInvocationInterceptor + implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor { + + private static final Method getTypeLevelUriVariables = + ReflectionUtils.findMethod(ControllerMethodValues.class, "getTypeLevelUriVariables"); + + private static final Method getControllerMethod = + ReflectionUtils.findMethod(ControllerMethodValues.class, "getControllerMethod"); + + private static final Method getArgumentValues = + ReflectionUtils.findMethod(ControllerMethodValues.class, "getArgumentValues"); + + + private final Object[] typeLevelUriVariables; + + private Method controllerMethod; + + private Object[] argumentValues; + + + public ControllerMethodInvocationInterceptor(Object... typeLevelUriVariables) { + this.typeLevelUriVariables = typeLevelUriVariables.clone(); + } + + + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) { + + if (getTypeLevelUriVariables.equals(method)) { + return this.typeLevelUriVariables; + } + else if (getControllerMethod.equals(method)) { + return this.controllerMethod; + } + else if (getArgumentValues.equals(method)) { + return this.argumentValues; + } + else if (ReflectionUtils.isObjectMethod(method)) { + return ReflectionUtils.invokeMethod(method, obj, args); + } + else { + this.controllerMethod = method; + this.argumentValues = args; + + Class returnType = method.getReturnType(); + return void.class.equals(returnType) ? null : returnType.cast(initProxy(returnType, this)); + } + } + + @Override + public Object invoke(org.aopalliance.intercept.MethodInvocation inv) throws Throwable { + return intercept(inv.getThis(), inv.getMethod(), inv.getArguments(), null); + } + } + + /** + * Provides information about a controller method that can be used to prepare a URL + * including type-level URI template variables, a method reference, and argument + * values collected through the invocation of a "mock" controller. + *

+ * Instances of this interface are returned from + * {@link MvcUrlUtils#controller(Class, Object...) controller(Class, Object...)} and + * are needed for {@link MvcUrls#linkToMethodOn(ControllerMethodValues)}. + */ + public interface ControllerMethodValues { + + Object[] getTypeLevelUriVariables(); + + Method getControllerMethod(); + + Object[] getArgumentValues(); + + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/MvcUrls.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/MvcUrls.java new file mode 100644 index 0000000000..23bc4aa011 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/MvcUrls.java @@ -0,0 +1,121 @@ +/* + * 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.mvc.support; + +import java.lang.reflect.Method; + +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.method.support.UriComponentsContributor; +import org.springframework.web.servlet.config.DefaultMvcUrlsFactoryBean; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * A contract for creating URLs by referencing Spring MVC controllers and methods. + *

+ * The MVC Java config and the MVC namespace automatically create an instance of this + * contract for use in controllers and anywhere else during the processing of a request. + * The best way for access it is to have it autowired, or otherwise injected either by + * type or also qualified by name ("mvcUrls") if necessary. + *

+ * If not using either option, with explicit configuration it's easy to create an instance + * of {@link DefaultMvcUrls} in Java config or in XML configuration, use + * {@link DefaultMvcUrlsFactoryBean}. + * + * @author Oliver Gierke + * @author Rossen Stoyanchev + * + * @since 4.0 + */ +public interface MvcUrls { + + /** + * Creates a new {@link UriComponentsBuilder} by pointing to a controller class. The + * resulting builder contains all the current request information up to and including + * the Servlet mapping as well as the portion of the path matching to the controller + * level request mapping. If the controller contains multiple mappings, the + * {@link DefaultMvcUrls} will use the first one. + * + * @param controllerType the controller type to create a URL to + * + * @return a builder that can be used to further build the {@link UriComponents}. + */ + UriComponentsBuilder linkToController(Class controllerType); + + /** + * Create a {@link UriComponents} by pointing to a controller method along with method + * argument values. + *

+ * Type and method-level mappings of the controller method are extracted and the + * resulting {@link UriComponents} is further enriched with method argument values from + * {@link PathVariable} and {@link RequestParam} parameters. Any other arguments not + * relevant to the building of the URL can be provided as {@literal null} and will be + * ignored. Support for additional custom arguments can be added through a + * {@link UriComponentsContributor}. + * + * FIXME Type-level URI template variables? + * + * @param method the target controller method + * @param argumentValues argument values matching to method parameters + * + * @return UriComponents instance, never {@literal null} + */ + UriComponents linkToMethod(Method method, Object... argumentValues); + + /** + * Create a {@link UriComponents} by invoking a method on a "mock" controller similar + * to how test frameworks provide mock objects and record method invocations. The + * static method {@link MvcUrlUtils#controller(Class, Object...)} can be used to + * create a "mock" controller: + * + *

+	 * @RequestMapping("/people/{id}/addresses")
+	 * class AddressController {
+	 *
+	 *   @RequestMapping("/{country}")
+	 *   public HttpEntity getAddressesForCountry(@PathVariable String country) { … }
+	 *
+	 *   @RequestMapping(value="/", method=RequestMethod.POST)
+	 *   public void addAddress(Address address) { … }
+	 * }
+	 *
+	 * // short-hand style with static import of MvcUrlUtils.controller
+	 *
+	 * mvcUrls.linkToMethodOn(controller(CustomerController.class, 1).showAddresses("US"));
+	 *
+	 * // longer style, required for void controller methods
+	 *
+	 * CustomerController controller = MvcUrlUtils.controller(CustomController.class, 1);
+	 * controller.addAddress(null);
+	 *
+	 * mvcUrls.linkToMethodOn(controller);
+	 *
+	 * 
+ * + * The above mechanism supports {@link PathVariable} and {@link RequestParam} method + * arguments. Any other arguments can be provided as {@literal null} and will be + * ignored. Additional custom arguments can be added through an implementation of + * {@link UriComponentsContributor}. + * + * @param mockController created via {@link MvcUrlUtils#controller(Class, Object...)} + * + * @return UriComponents instance, never {@literal null} + */ + UriComponents linkToMethodOn(Object mockController); + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java index ff9a9bee07..d3d02c6bea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java @@ -100,16 +100,18 @@ public class ServletUriComponentsBuilder extends UriComponentsBuilder { int port = request.getServerPort(); String host = request.getServerName(); - String xForwardedHostHeader = request.getHeader("X-Forwarded-Host"); + String header = request.getHeader("X-Forwarded-Host"); - if (StringUtils.hasText(xForwardedHostHeader)) { - if (StringUtils.countOccurrencesOf(xForwardedHostHeader, ":") == 1) { - String[] hostAndPort = StringUtils.split(xForwardedHostHeader, ":"); + if (StringUtils.hasText(header)) { + String[] hosts = StringUtils.commaDelimitedListToStringArray(header); + String hostToUse = hosts[0]; + if (hostToUse.contains(":")) { + String[] hostAndPort = StringUtils.split(hostToUse, ":"); host = hostAndPort[0]; port = Integer.parseInt(hostAndPort[1]); } else { - host = xForwardedHostHeader; + host = hostToUse; } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index 367ab9f700..19d38dc35e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -56,6 +56,8 @@ import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.async.CallableProcessingInterceptor; import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; @@ -76,11 +78,14 @@ import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.servlet.mvc.support.MvcUrls; import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; import org.springframework.web.servlet.theme.ThemeChangeInterceptor; +import org.springframework.web.util.UriComponents; import static org.junit.Assert.*; +import static org.springframework.web.servlet.mvc.support.MvcUrlUtils.*; /** * @author Keith Donald @@ -108,7 +113,7 @@ public class MvcNamespaceTests { @Test public void testDefaultConfig() throws Exception { - loadBeanDefinitions("mvc-config.xml", 12); + loadBeanDefinitions("mvc-config.xml", 13); RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class); assertNotNull(mapping); @@ -147,11 +152,26 @@ public class MvcNamespaceTests { adapter.handle(request, response, handlerMethod); assertTrue(handler.recordedValidationError); + + // MvcUrls + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest())); + try { + Date now = new Date(); + TestController testController = controller(TestController.class); + testController.testBind(now, null, null); + MvcUrls mvcUrls = this.appContext.getBean(MvcUrls.class); + UriComponents uriComponents = mvcUrls.linkToMethodOn(testController); + + assertEquals("http://localhost/?date=2013-10-21", uriComponents.toUriString()); + } + finally { + RequestContextHolder.resetRequestAttributes(); + } } @Test(expected=TypeMismatchException.class) public void testCustomConversionService() throws Exception { - loadBeanDefinitions("mvc-config-custom-conversion-service.xml", 12); + loadBeanDefinitions("mvc-config-custom-conversion-service.xml", 13); RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class); assertNotNull(mapping); @@ -177,7 +197,7 @@ public class MvcNamespaceTests { @Test public void testCustomValidator() throws Exception { - loadBeanDefinitions("mvc-config-custom-validator.xml", 12); + loadBeanDefinitions("mvc-config-custom-validator.xml", 13); RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class); assertNotNull(mapping); @@ -199,7 +219,7 @@ public class MvcNamespaceTests { @Test public void testInterceptors() throws Exception { - loadBeanDefinitions("mvc-config-interceptors.xml", 17); + loadBeanDefinitions("mvc-config-interceptors.xml", 18); RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class); assertNotNull(mapping); @@ -328,7 +348,7 @@ public class MvcNamespaceTests { @Test public void testBeanDecoration() throws Exception { - loadBeanDefinitions("mvc-config-bean-decoration.xml", 14); + loadBeanDefinitions("mvc-config-bean-decoration.xml", 15); RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class); assertNotNull(mapping); @@ -349,7 +369,7 @@ public class MvcNamespaceTests { @Test public void testViewControllers() throws Exception { - loadBeanDefinitions("mvc-config-view-controllers.xml", 15); + loadBeanDefinitions("mvc-config-view-controllers.xml", 16); RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class); assertNotNull(mapping); @@ -409,7 +429,7 @@ public class MvcNamespaceTests { /** WebSphere gives trailing servlet path slashes by default!! */ @Test public void testViewControllersOnWebSphere() throws Exception { - loadBeanDefinitions("mvc-config-view-controllers.xml", 15); + loadBeanDefinitions("mvc-config-view-controllers.xml", 16); SimpleUrlHandlerMapping mapping2 = appContext.getBean(SimpleUrlHandlerMapping.class); SimpleControllerHandlerAdapter adapter = appContext.getBean(SimpleControllerHandlerAdapter.class); @@ -462,7 +482,7 @@ public class MvcNamespaceTests { @Test public void testContentNegotiationManager() throws Exception { - loadBeanDefinitions("mvc-config-content-negotiation-manager.xml", 12); + loadBeanDefinitions("mvc-config-content-negotiation-manager.xml", 13); RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class); ContentNegotiationManager manager = mapping.getContentNegotiationManager(); @@ -474,7 +494,7 @@ public class MvcNamespaceTests { @Test public void testAsyncSupportOptions() throws Exception { - loadBeanDefinitions("mvc-config-async-support.xml", 13); + loadBeanDefinitions("mvc-config-async-support.xml", 14); RequestMappingHandlerAdapter adapter = appContext.getBean(RequestMappingHandlerAdapter.class); assertNotNull(adapter); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java index 68c4460b58..8d5f086e5a 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java @@ -16,28 +16,33 @@ package org.springframework.web.servlet.config.annotation; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import java.util.ArrayList; import java.util.List; import javax.servlet.http.HttpServletRequest; +import org.joda.time.DateTime; +import org.joda.time.format.ISODateTimeFormat; import org.junit.Before; import org.junit.Test; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.ConversionService; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.support.FormattingConversionService; -import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.HttpEntity; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockServletContext; import org.springframework.stereotype.Controller; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; -import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.handler.AbstractHandlerMapping; @@ -49,6 +54,11 @@ import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExc import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.support.MvcUrls; +import org.springframework.web.util.UriComponents; + +import static org.junit.Assert.*; +import static org.springframework.web.servlet.mvc.support.MvcUrlUtils.*; /** * A test fixture with an {@link WebMvcConfigurationSupport} instance. @@ -57,27 +67,26 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv */ public class WebMvcConfigurationSupportTests { - private WebMvcConfigurationSupport mvcConfiguration; - - private StaticWebApplicationContext wac; + private WebApplicationContext wac; @Before public void setUp() { - this.wac = new StaticWebApplicationContext(); - this.mvcConfiguration = new WebMvcConfigurationSupport(); - this.mvcConfiguration.setApplicationContext(wac); + + AnnotationConfigWebApplicationContext cxt = new AnnotationConfigWebApplicationContext(); + cxt.setServletContext(new MockServletContext()); + cxt.register(TestConfig.class); + cxt.refresh(); + + this.wac = cxt; } @Test public void requestMappingHandlerMapping() throws Exception { - this.wac.registerSingleton("controller", TestController.class); - RequestMappingHandlerMapping handlerMapping = mvcConfiguration.requestMappingHandlerMapping(); + RequestMappingHandlerMapping handlerMapping = this.wac.getBean(RequestMappingHandlerMapping.class); assertEquals(0, handlerMapping.getOrder()); - handlerMapping.setApplicationContext(this.wac); - handlerMapping.afterPropertiesSet(); HandlerExecutionChain chain = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/")); assertNotNull(chain.getInterceptors()); assertEquals(ConversionServiceExposingInterceptor.class, chain.getInterceptors()[0].getClass()); @@ -85,7 +94,10 @@ public class WebMvcConfigurationSupportTests { @Test public void emptyViewControllerHandlerMapping() { - AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) mvcConfiguration.viewControllerHandlerMapping(); + + AbstractHandlerMapping handlerMapping = this.wac.getBean( + "viewControllerHandlerMapping", AbstractHandlerMapping.class); + assertNotNull(handlerMapping); assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder()); assertTrue(handlerMapping.getClass().getName().endsWith("EmptyHandlerMapping")); @@ -93,16 +105,13 @@ public class WebMvcConfigurationSupportTests { @Test public void beanNameHandlerMapping() throws Exception { - StaticWebApplicationContext cxt = new StaticWebApplicationContext(); - cxt.registerSingleton("/controller", TestController.class); - HttpServletRequest request = new MockHttpServletRequest("GET", "/controller"); - - BeanNameUrlHandlerMapping handlerMapping = mvcConfiguration.beanNameHandlerMapping(); + BeanNameUrlHandlerMapping handlerMapping = this.wac.getBean(BeanNameUrlHandlerMapping.class); assertEquals(2, handlerMapping.getOrder()); - handlerMapping.setApplicationContext(cxt); + HttpServletRequest request = new MockHttpServletRequest("GET", "/testController"); HandlerExecutionChain chain = handlerMapping.getHandler(request); + assertNotNull(chain.getInterceptors()); assertEquals(2, chain.getInterceptors().length); assertEquals(ConversionServiceExposingInterceptor.class, chain.getInterceptors()[1].getClass()); @@ -110,8 +119,10 @@ public class WebMvcConfigurationSupportTests { @Test public void emptyResourceHandlerMapping() { - mvcConfiguration.setApplicationContext(new StaticWebApplicationContext()); - AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) mvcConfiguration.resourceHandlerMapping(); + + AbstractHandlerMapping handlerMapping = this.wac.getBean( + "resourceHandlerMapping", AbstractHandlerMapping.class); + assertNotNull(handlerMapping); assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder()); assertTrue(handlerMapping.getClass().getName().endsWith("EmptyHandlerMapping")); @@ -119,8 +130,10 @@ public class WebMvcConfigurationSupportTests { @Test public void emptyDefaultServletHandlerMapping() { - mvcConfiguration.setServletContext(new MockServletContext()); - AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) mvcConfiguration.defaultServletHandlerMapping(); + + AbstractHandlerMapping handlerMapping = this.wac.getBean( + "defaultServletHandlerMapping", AbstractHandlerMapping.class); + assertNotNull(handlerMapping); assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder()); assertTrue(handlerMapping.getClass().getName().endsWith("EmptyHandlerMapping")); @@ -128,11 +141,10 @@ public class WebMvcConfigurationSupportTests { @Test public void requestMappingHandlerAdapter() throws Exception { - RequestMappingHandlerAdapter adapter = mvcConfiguration.requestMappingHandlerAdapter(); - List> expectedConverters = new ArrayList>(); - mvcConfiguration.addDefaultHttpMessageConverters(expectedConverters); - assertEquals(expectedConverters.size(), adapter.getMessageConverters().size()); + RequestMappingHandlerAdapter adapter = this.wac.getBean(RequestMappingHandlerAdapter.class); + + assertEquals(9, adapter.getMessageConverters().size()); ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) adapter.getWebBindingInitializer(); assertNotNull(initializer); @@ -146,10 +158,27 @@ public class WebMvcConfigurationSupportTests { assertTrue(validator instanceof LocalValidatorFactoryBean); } + @Test + public void mvcUrls() throws Exception { + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest())); + try { + DateTime now = DateTime.now(); + MvcUrls mvcUrls = this.wac.getBean(MvcUrls.class); + UriComponents uriComponents = mvcUrls.linkToMethodOn(controller( + TestController.class).methodWithTwoPathVariables(1, now)); + + assertEquals("/foo/1/bar/" + ISODateTimeFormat.date().print(now), uriComponents.getPath()); + } + finally { + RequestContextHolder.resetRequestAttributes(); + } + } + @Test public void handlerExceptionResolver() throws Exception { + HandlerExceptionResolverComposite compositeResolver = - (HandlerExceptionResolverComposite) mvcConfiguration.handlerExceptionResolver(); + this.wac.getBean("handlerExceptionResolver", HandlerExceptionResolverComposite.class); assertEquals(0, compositeResolver.getOrder()); @@ -164,12 +193,28 @@ public class WebMvcConfigurationSupportTests { } + @EnableWebMvc + @Configuration + public static class TestConfig { + + @Bean(name={"/testController"}) + public TestController testController() { + return new TestController(); + } + } + @Controller private static class TestController { @RequestMapping("/") public void handle() { } + + @RequestMapping("/foo/{id}/bar/{date}") + public HttpEntity methodWithTwoPathVariables(@PathVariable Integer id, + @DateTimeFormat(iso = ISO.DATE) @PathVariable DateTime date) { + return null; + } } } 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 deleted file mode 100644 index 4e68cbcf45..0000000000 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/AnnotationMappingDiscovererUnitTests.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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 deleted file mode 100644 index 5e52559e99..0000000000 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderFactoryUnitTests.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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 deleted file mode 100644 index f3ec765d32..0000000000 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/MvcUriComponentsBuilderUnitTests.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * 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 deleted file mode 100644 index 9e24f124f1..0000000000 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/RecordedInvocationUtilsUnitTests.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 deleted file mode 100644 index 742707decf..0000000000 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/hypermedia/TestUtils.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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")); - } -} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultMvcUrlsTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultMvcUrlsTests.java new file mode 100644 index 0000000000..c5954a71c3 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultMvcUrlsTests.java @@ -0,0 +1,290 @@ +/* + * 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.mvc.support; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.hamcrest.Matchers; +import org.joda.time.DateTime; +import org.joda.time.format.ISODateTimeFormat; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.http.HttpEntity; +import org.springframework.mock.web.test.MockHttpServletRequest; +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.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver; +import org.springframework.web.util.UriComponents; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.web.servlet.mvc.support.MvcUrlUtils.*; + +/** + * Unit tests for {@link DefaultMvcUrls}. + * + * @author Oliver Gierke + * @author Dietrich Schulten + * @author Rossen Stoyanchev + */ +public class DefaultMvcUrlsTests { + + private MockHttpServletRequest request; + + private MvcUrls mvcUrls; + + + @Before + public void setUp() { + this.request = new MockHttpServletRequest(); + ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(requestAttributes); + + List resolvers = new ArrayList<>(); + resolvers.add(new PathVariableMethodArgumentResolver()); + resolvers.add(new RequestParamMethodArgumentResolver(null, false)); + + this.mvcUrls = new DefaultMvcUrls(resolvers, null); + } + + @After + public void teardown() { + RequestContextHolder.resetRequestAttributes(); + } + + + @Test + public void linkToControllerRoot() { + UriComponents uriComponents = this.mvcUrls.linkToController(PersonControllerImpl.class).build(); + assertThat(uriComponents.toUriString(), Matchers.endsWith("/people")); + } + + @Test + public void linkToParameterizedControllerRoot() { + UriComponents uriComponents = this.mvcUrls.linkToController( + PersonsAddressesController.class).buildAndExpand(15); + + assertThat(uriComponents.toUriString(), endsWith("/people/15/addresses")); + } + + @Test + public void linkToMethodOnParameterizedControllerRoot() { + UriComponents uriComponents = this.mvcUrls.linkToMethodOn( + controller(PersonsAddressesController.class, 15).getAddressesForCountry("DE")); + + assertThat(uriComponents.toUriString(), endsWith("/people/15/addresses/DE")); + } + + @Test + public void linkToSubResource() { + UriComponents uriComponents = + this.mvcUrls.linkToController(PersonControllerImpl.class).pathSegment("something").build(); + + assertThat(uriComponents.toUriString(), endsWith("/people/something")); + } + + @Test + public void linkToControllerWithMultipleMappings() { + UriComponents uriComponents = this.mvcUrls.linkToController(InvalidController.class).build(); + assertThat(uriComponents.toUriString(), is("http://localhost/persons")); + } + + @Test + public void linkToControllerNotMapped() { + UriComponents uriComponents = this.mvcUrls.linkToController(UnmappedController.class).build(); + assertThat(uriComponents.toUriString(), is("http://localhost/")); + } + + @Test + public void linkToMethodRefWithPathVar() throws Exception { + Method method = ControllerWithMethods.class.getDeclaredMethod("methodWithPathVariable", String.class); + UriComponents uriComponents = this.mvcUrls.linkToMethod(method, new Object[] { "1" }); + + assertThat(uriComponents.toUriString(), is("http://localhost/something/1/foo")); + } + + @Test + public void linkToMethodRefWithTwoPathVars() throws Exception { + DateTime now = DateTime.now(); + Method method = ControllerWithMethods.class.getDeclaredMethod( + "methodWithTwoPathVariables", Integer.class, DateTime.class); + UriComponents uriComponents = this.mvcUrls.linkToMethod(method, new Object[] { 1, now }); + + assertThat(uriComponents.getPath(), is("/something/1/foo/" + ISODateTimeFormat.date().print(now))); + } + + @Test + public void linkToMethodRefWithPathVarAndRequestParam() throws Exception { + Method method = ControllerWithMethods.class.getDeclaredMethod("methodForNextPage", String.class, Integer.class, Integer.class); + UriComponents uriComponents = this.mvcUrls.linkToMethod(method, new Object[] {"1", 10, 5}); + + assertThat(uriComponents.getPath(), is("/something/1/foo")); + + MultiValueMap queryParams = uriComponents.getQueryParams(); + assertThat(queryParams.get("limit"), contains("5")); + assertThat(queryParams.get("offset"), contains("10")); + } + + @Test + public void linkToMethod() { + UriComponents uriComponents = this.mvcUrls.linkToMethodOn( + controller(ControllerWithMethods.class).myMethod(null)); + + assertThat(uriComponents.toUriString(), startsWith("http://localhost")); + assertThat(uriComponents.toUriString(), endsWith("/something/else")); + } + + @Test + public void linkToMethodWithPathVar() { + UriComponents uriComponents = this.mvcUrls.linkToMethodOn( + controller(ControllerWithMethods.class).methodWithPathVariable("1")); + + assertThat(uriComponents.toUriString(), startsWith("http://localhost")); + assertThat(uriComponents.toUriString(), endsWith("/something/1/foo")); + } + + @Test + public void linkToMethodWithPathVarAndRequestParams() { + UriComponents uriComponents = this.mvcUrls.linkToMethodOn( + controller(ControllerWithMethods.class).methodForNextPage("1", 10, 5)); + + assertThat(uriComponents.getPath(), is("/something/1/foo")); + + MultiValueMap queryParams = uriComponents.getQueryParams(); + assertThat(queryParams.get("limit"), contains("5")); + assertThat(queryParams.get("offset"), contains("10")); + } + + @Test + public void linkToMethodWithPathVarAndMultiValueRequestParams() { + UriComponents uriComponents = this.mvcUrls.linkToMethodOn( + controller(ControllerWithMethods.class).methodWithMultiValueRequestParams( + "1", Arrays.asList(3, 7), 5)); + + assertThat(uriComponents.getPath(), is("/something/1/foo")); + + MultiValueMap queryParams = uriComponents.getQueryParams(); + assertThat(queryParams.get("limit"), contains("5")); + assertThat(queryParams.get("items"), containsInAnyOrder("3", "7")); + } + + @Test + public void usesForwardedHostAsHostIfHeaderIsSet() { + this.request.addHeader("X-Forwarded-Host", "somethingDifferent"); + UriComponents uriComponents = this.mvcUrls.linkToController(PersonControllerImpl.class).build(); + + assertThat(uriComponents.toUriString(), startsWith("http://somethingDifferent")); + } + + @Test + public void usesForwardedHostAndPortFromHeader() { + request.addHeader("X-Forwarded-Host", "foobar:8088"); + UriComponents uriComponents = this.mvcUrls.linkToController(PersonControllerImpl.class).build(); + + assertThat(uriComponents.toUriString(), startsWith("http://foobar:8088")); + } + + @Test + public void usesFirstHostOfXForwardedHost() { + request.addHeader("X-Forwarded-Host", "barfoo:8888, localhost:8088"); + UriComponents uriComponents = this.mvcUrls.linkToController(PersonControllerImpl.class).build(); + + assertThat(uriComponents.toUriString(), startsWith("http://barfoo:8888")); + } + + + 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("/{id}/foo/{date}") + HttpEntity methodWithTwoPathVariables( + @PathVariable Integer id, @DateTimeFormat(iso = ISO.DATE) @PathVariable DateTime date) { + 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/mvc/support/MvcUrlUtilsTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/MvcUrlUtilsTests.java new file mode 100644 index 0000000000..60e3e4ccd8 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/MvcUrlUtilsTests.java @@ -0,0 +1,122 @@ +/* + * 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.mvc.support; + +import java.lang.reflect.Method; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.servlet.mvc.support.MvcUrlUtils.ControllerMethodValues; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +/** + * Test fixture for {@link MvcUrlUtils}. + * + * @author Oliver Gierke + * @author Rossen Stoyanchev + */ +public class MvcUrlUtilsTests { + + private MockHttpServletRequest request; + + + @Before + public void setUp() { + this.request = new MockHttpServletRequest(); + ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(requestAttributes); + } + + @After + public void teardown() { + RequestContextHolder.resetRequestAttributes(); + } + + @Test + public void methodOn() { + HttpEntity result = MvcUrlUtils.controller(SampleController.class).someMethod(1L); + + assertTrue(result instanceof ControllerMethodValues); + assertEquals("someMethod", ((ControllerMethodValues) result).getControllerMethod().getName()); + } + + @Test + public void typeLevelMapping() { + assertThat(MvcUrlUtils.getTypeLevelMapping(MyController.class), is("/type")); + } + + @Test + public void typeLevelMappingNone() { + assertThat(MvcUrlUtils.getTypeLevelMapping(ControllerWithoutTypeLevelMapping.class), is("/")); + } + + @Test + public void methodLevelMapping() throws Exception { + Method method = MyController.class.getMethod("method"); + assertThat(MvcUrlUtils.getMethodMapping(method), is("/type/method")); + } + + @Test + public void methodLevelMappingWithoutTypeLevelMapping() throws Exception { + Method method = ControllerWithoutTypeLevelMapping.class.getMethod("method"); + assertThat(MvcUrlUtils.getMethodMapping(method), is("/method")); + } + + @Test + public void methodMappingWithControllerMappingOnly() throws Exception { + Method method = MyController.class.getMethod("noMethodMapping"); + assertThat(MvcUrlUtils.getMethodMapping(method), is("/type")); + } + + + @RequestMapping("/sample") + static class SampleController { + + @RequestMapping("/{id}/foo") + HttpEntity someMethod(@PathVariable("id") Long id) { + return new ResponseEntity(HttpStatus.OK); + } + } + + @RequestMapping("/type") + interface MyController { + + @RequestMapping("/method") + void method(); + + @RequestMapping + void noMethodMapping(); + } + + interface ControllerWithoutTypeLevelMapping { + + @RequestMapping("/method") + void method(); + } + +}