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(); + } + +}