From 57a42ababd248413d0c267fa29ffcda658e337d0 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Sat, 10 Jan 2015 17:20:36 +0100 Subject: [PATCH] DATACMNS-630 - Added HandlerMethodArgumentResolver to create proxies for interfaces. We now ship a ProxyingHandlerMethodArgumentResolver that gets registered when @EnableSpringDataWebSupport is activated. It creates Map-based proxy instances for interfaces used as Spring MVC controller method parameters. --- .../data/web/MapDataBinder.java | 301 ++++++++++++++++++ ...ProxyingHandlerMethodArgumentResolver.java | 93 ++++++ .../config/SpringDataWebConfiguration.java | 12 +- ...apAccessingMethodInterceptorUnitTests.java | 2 +- .../data/web/MapDataBinderUnitTests.java | 142 +++++++++ ...eSpringDataWebSupportIntegrationTests.java | 31 ++ .../data/web/config/SampleController.java | 74 +++++ 7 files changed, 650 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/springframework/data/web/MapDataBinder.java create mode 100644 src/main/java/org/springframework/data/web/ProxyingHandlerMethodArgumentResolver.java create mode 100644 src/test/java/org/springframework/data/web/MapDataBinderUnitTests.java create mode 100644 src/test/java/org/springframework/data/web/config/SampleController.java diff --git a/src/main/java/org/springframework/data/web/MapDataBinder.java b/src/main/java/org/springframework/data/web/MapDataBinder.java new file mode 100644 index 000000000..26c63fe9f --- /dev/null +++ b/src/main/java/org/springframework/data/web/MapDataBinder.java @@ -0,0 +1,301 @@ +/* + * Copyright 2015 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.data.web; + +import java.beans.PropertyDescriptor; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.AbstractPropertyAccessor; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.ConfigurablePropertyAccessor; +import org.springframework.beans.NotWritablePropertyException; +import org.springframework.beans.PropertyAccessor; +import org.springframework.context.expression.MapAccessor; +import org.springframework.core.CollectionFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.PropertyReferenceException; +import org.springframework.data.util.TypeInformation; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.SpelParserConfiguration; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeConverter; +import org.springframework.util.Assert; +import org.springframework.web.bind.WebDataBinder; + +/** + * A {@link WebDataBinder} that automatically binds all properties exposed in the given type using a {@link Map}. + * + * @author Oliver Gierke + * @since 1.10 + */ +class MapDataBinder extends WebDataBinder { + + private final Class type; + private final ConversionService conversionService; + + /** + * Creates a new {@link MapDataBinder} for the given type and {@link ConversionService}. + * + * @param type target type to detect property that need to be bound. + * @param conversionService the {@link ConversionService} to be used to preprocess values. + */ + public MapDataBinder(Class type, ConversionService conversionService) { + + super(new HashMap()); + + this.type = type; + this.conversionService = conversionService; + } + + /* + * (non-Javadoc) + * @see org.springframework.validation.DataBinder#getTarget() + */ + @Override + @SuppressWarnings("unchecked") + public Map getTarget() { + return (Map) super.getTarget(); + } + + /* + * (non-Javadoc) + * @see org.springframework.validation.DataBinder#getPropertyAccessor() + */ + @Override + protected ConfigurablePropertyAccessor getPropertyAccessor() { + return new MapPropertyAccessor(type, getTarget(), conversionService); + } + + /** + * {@link PropertyAccessor} to store and retrieve values in a {@link Map}. Uses Spring Expression language to create + * deeply nested Map structures. + * + * @author Oliver Gierke + * @since 1.10 + */ + private static class MapPropertyAccessor extends AbstractPropertyAccessor { + + private static final SpelExpressionParser PARSER = new SpelExpressionParser( + new SpelParserConfiguration(false, true)); + + private final Class type; + private final Map map; + private final ConversionService conversionService; + + /** + * Creates a new {@link MapPropertyAccessor} for the given type, map and {@link ConversionService}. + * + * @param type must not be {@literal null}. + * @param map must not be {@literal null}. + * @param conversionService must not be {@literal null}. + */ + public MapPropertyAccessor(Class type, Map map, ConversionService conversionService) { + + Assert.notNull(type, "Type must not be null!"); + Assert.notNull(map, "Map must not be null!"); + Assert.notNull(conversionService, "ConversionService must not be null!"); + + this.type = type; + this.map = map; + this.conversionService = conversionService; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.PropertyAccessor#isReadableProperty(java.lang.String) + */ + @Override + public boolean isReadableProperty(String propertyName) { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.PropertyAccessor#isWritableProperty(java.lang.String) + */ + @Override + public boolean isWritableProperty(String propertyName) { + + try { + return getPropertyPath(propertyName) != null; + } catch (PropertyReferenceException o_O) { + return false; + } + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.PropertyAccessor#getPropertyTypeDescriptor(java.lang.String) + */ + @Override + public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.AbstractPropertyAccessor#getPropertyValue(java.lang.String) + */ + @Override + public Object getPropertyValue(String propertyName) throws BeansException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.AbstractPropertyAccessor#setPropertyValue(java.lang.String, java.lang.Object) + */ + @Override + public void setPropertyValue(String propertyName, Object value) throws BeansException { + + if (!isWritableProperty(propertyName)) { + throw new NotWritablePropertyException(type, propertyName); + } + + StandardEvaluationContext context = new StandardEvaluationContext(); + context.addPropertyAccessor(new PropertyTraversingMapAccessor(type, new DefaultConversionService())); + context.setTypeConverter(new StandardTypeConverter(conversionService)); + context.setRootObject(map); + + Expression expression = PARSER.parseExpression(propertyName); + + PropertyPath leafProperty = getPropertyPath(propertyName).getLeafProperty(); + TypeInformation owningType = leafProperty.getOwningType(); + TypeInformation propertyType = owningType.getProperty(leafProperty.getSegment()); + + propertyType = propertyName.endsWith("]") ? propertyType.getActualType() : propertyType; + + if (conversionRequired(value, propertyType.getType())) { + + PropertyDescriptor descriptor = BeanUtils + .getPropertyDescriptor(owningType.getType(), leafProperty.getSegment()); + MethodParameter methodParameter = new MethodParameter(descriptor.getReadMethod(), -1); + TypeDescriptor typeDescriptor = TypeDescriptor.nested(methodParameter, 0); + + value = conversionService.convert(value, TypeDescriptor.forObject(value), typeDescriptor); + } + + expression.setValue(context, value); + } + + private boolean conversionRequired(Object source, Class targetType) { + + if (targetType.isInstance(source)) { + return false; + } + + return conversionService.canConvert(source.getClass(), targetType); + } + + private PropertyPath getPropertyPath(String propertyName) { + + String plainPropertyPath = propertyName.replaceAll("\\[.*?\\]", ""); + return PropertyPath.from(plainPropertyPath, type); + } + + /** + * A special {@link MapAccessor} that traverses properties on the configured type to automatically create nested Map + * and collection values as necessary. + * + * @author Oliver Gierke + * @since 1.10 + */ + private static final class PropertyTraversingMapAccessor extends MapAccessor { + + private final ConversionService conversionService; + private Class type; + + /** + * Creates a new {@link PropertyTraversingMapAccessor} for the given type and {@link ConversionService}. + * + * @param type must not be {@literal null}. + * @param conversionService must not be {@literal null}. + */ + public PropertyTraversingMapAccessor(Class type, ConversionService conversionService) { + + Assert.notNull(type, "Type must not be null!"); + Assert.notNull(conversionService, "ConversionService must not be null!"); + + this.type = type; + this.conversionService = conversionService; + } + + /* + * (non-Javadoc) + * @see org.springframework.context.expression.MapAccessor#canRead(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String) + */ + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + return true; + } + + /* + * (non-Javadoc) + * @see org.springframework.context.expression.MapAccessor#read(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String) + */ + @Override + @SuppressWarnings("unchecked") + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + + PropertyPath path = PropertyPath.from(name, type); + + try { + return super.read(context, target, name); + } catch (AccessException o_O) { + + Object emptyResult = path.isCollection() ? CollectionFactory.createCollection(List.class, 0) + : CollectionFactory.createMap(Map.class, 0); + + ((Map) target).put(name, emptyResult); + + return new TypedValue(emptyResult, getDescriptor(path, emptyResult)); + } finally { + this.type = path.getType(); + } + } + + /** + * Returns the type descriptor for the given {@link PropertyPath} and empty value for that path. + * + * @param path must not be {@literal null}. + * @param emptyValue must not be {@literal null}. + * @return + */ + private TypeDescriptor getDescriptor(PropertyPath path, Object emptyValue) { + + Class actualPropertyType = path.getType(); + + TypeDescriptor valueDescriptor = conversionService.canConvert(String.class, actualPropertyType) ? TypeDescriptor + .valueOf(String.class) : TypeDescriptor.valueOf(HashMap.class); + + return path.isCollection() ? TypeDescriptor.collection(emptyValue.getClass(), valueDescriptor) : TypeDescriptor + .map(emptyValue.getClass(), TypeDescriptor.valueOf(String.class), valueDescriptor); + + } + } + } +} diff --git a/src/main/java/org/springframework/data/web/ProxyingHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/ProxyingHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..2f736f0e0 --- /dev/null +++ b/src/main/java/org/springframework/data/web/ProxyingHandlerMethodArgumentResolver.java @@ -0,0 +1,93 @@ +/* + * Copyright 2015 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.data.web; + +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.annotation.ModelAttributeMethodProcessor; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; + +/** + * {@link HandlerMethodArgumentResolver} to create Proxy instances for interface based controller method parameters. + * + * @author Oliver Gierke + * @since 1.10 + */ +public class ProxyingHandlerMethodArgumentResolver extends ModelAttributeMethodProcessor implements BeanFactoryAware { + + private final SpelAwareProxyProjectionFactory proxyFactory; + private final ConversionService conversionService; + + /** + * Creates a new {@link PageableHandlerMethodArgumentResolver} using the given {@link ConversionService}. + * + * @param conversionService must not be {@literal null}. + */ + public ProxyingHandlerMethodArgumentResolver(ConversionService conversionService) { + + super(true); + + this.proxyFactory = new SpelAwareProxyProjectionFactory(); + this.conversionService = conversionService; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory) + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.proxyFactory.setBeanFactory(beanFactory); + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().isInterface(); + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.annotation.ModelAttributeMethodProcessor#createAttribute(java.lang.String, org.springframework.core.MethodParameter, org.springframework.web.bind.support.WebDataBinderFactory, org.springframework.web.context.request.NativeWebRequest) + */ + @Override + protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, + NativeWebRequest request) throws Exception { + + MapDataBinder binder = new MapDataBinder(parameter.getParameterType(), conversionService); + binder.bind(new MutablePropertyValues(request.getParameterMap())); + + return proxyFactory.createProjection(parameter.getParameterType(), binder.getTarget()); + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.annotation.ModelAttributeMethodProcessor#bindRequestParameters(org.springframework.web.bind.WebDataBinder, org.springframework.web.context.request.NativeWebRequest) + */ + @Override + protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {} +} diff --git a/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java b/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java index 15033b943..413679654 100644 --- a/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java +++ b/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java @@ -17,14 +17,18 @@ package org.springframework.data.web.config; import java.util.List; +import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.ConversionService; import org.springframework.data.geo.format.DistanceFormatter; import org.springframework.data.geo.format.PointFormatter; import org.springframework.data.repository.support.DomainClassConverter; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.data.web.ProxyingHandlerMethodArgumentResolver; import org.springframework.data.web.SortHandlerMethodArgumentResolver; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; @@ -42,6 +46,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter public class SpringDataWebConfiguration extends WebMvcConfigurerAdapter { @Autowired private ApplicationContext context; + @Autowired @Qualifier("mvcConversionService") ObjectFactory conversionService; /* * (non-Javadoc) @@ -75,10 +80,7 @@ public class SpringDataWebConfiguration extends WebMvcConfigurerAdapter { return; } - registerDomainClassConverterFor((FormattingConversionService) registry); - } - - private void registerDomainClassConverterFor(FormattingConversionService conversionService) { + FormattingConversionService conversionService = (FormattingConversionService) registry; DomainClassConverter converter = new DomainClassConverter( conversionService); @@ -94,5 +96,7 @@ public class SpringDataWebConfiguration extends WebMvcConfigurerAdapter { argumentResolvers.add(sortResolver()); argumentResolvers.add(pageableResolver()); + + argumentResolvers.add(new ProxyingHandlerMethodArgumentResolver(conversionService.getObject())); } } diff --git a/src/test/java/org/springframework/data/projection/MapAccessingMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/projection/MapAccessingMethodInterceptorUnitTests.java index 4752160c8..ae6be604f 100644 --- a/src/test/java/org/springframework/data/projection/MapAccessingMethodInterceptorUnitTests.java +++ b/src/test/java/org/springframework/data/projection/MapAccessingMethodInterceptorUnitTests.java @@ -120,7 +120,7 @@ public class MapAccessingMethodInterceptorUnitTests { new MapAccessingMethodInterceptor(Collections. emptyMap()).invoke(invocation); } - public static interface Sample { + interface Sample { String getName(); diff --git a/src/test/java/org/springframework/data/web/MapDataBinderUnitTests.java b/src/test/java/org/springframework/data/web/MapDataBinderUnitTests.java new file mode 100644 index 000000000..a26494f45 --- /dev/null +++ b/src/test/java/org/springframework/data/web/MapDataBinderUnitTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2015 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.data.web; + +import static java.util.Collections.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyValues; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.format.support.DefaultFormattingConversionService; + +/** + * Unit tests for {@link MapDataBinder}. + * + * @author Oliver Gierke + */ +public class MapDataBinderUnitTests { + + /** + * @see DATACMNS-630 + */ + @Test + public void honorsFormattingAnnotationOnAccessor() { + + Date reference = new Date(); + + MutablePropertyValues values = new MutablePropertyValues(); + values.add("foo.date", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(reference)); + + Map nested = new HashMap(); + nested.put("date", reference); + + assertThat(bind(values), hasEntry("foo", (Object) nested)); + } + + /** + * @see DATACMNS-630 + */ + @Test + @SuppressWarnings("rawtypes") + public void bindsNestedCollectionElement() { + + MutablePropertyValues values = new MutablePropertyValues(); + values.add("foo.bar.fooBar[0]", "String"); + + Map result = bind(values); + + List list = new ArrayList(); + list.add("String"); + + assertThat(result, is((Map) singletonMap("foo", singletonMap("bar", singletonMap("fooBar", list))))); + } + + /** + * @see DATACMNS-630 + */ + @Test + @SuppressWarnings("rawtypes") + public void bindsNestedPrimitive() { + + MutablePropertyValues values = new MutablePropertyValues(); + values.add("foo.firstname", "Dave"); + values.add("foo.lastname", "Matthews"); + + Map result = bind(values); + + Map dave = new HashMap(); + dave.put("firstname", "Dave"); + dave.put("lastname", "Matthews"); + + assertThat(result, is((Map) singletonMap("foo", dave))); + } + + /** + * @see DATACMNS-630 + */ + @Test + public void skipsPropertyNotExposedByTheTypeHierarchy() { + + MutablePropertyValues values = new MutablePropertyValues(); + values.add("somethingWeird", "Value"); + + assertThat(bind(values), is(Collections. emptyMap())); + } + + private static Map bind(PropertyValues values) { + + MapDataBinder binder = new MapDataBinder(Root.class, new DefaultFormattingConversionService()); + binder.bind(values); + + return binder.getTarget(); + } + + interface Root { + + Foo getFoo(); + + Bar getBar(); + } + + interface Foo { + + Bar getBar(); + + String getLastname(); + + String getFirstname(); + + @DateTimeFormat(iso = ISO.DATE_TIME) + Date getDate(); + } + + interface Bar { + Collection getFooBar(); + } +} diff --git a/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java index 7af4fb17c..6fa6c1506 100644 --- a/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java +++ b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java @@ -17,6 +17,8 @@ package org.springframework.data.web.config; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.lang.reflect.Field; import java.util.ArrayList; @@ -27,6 +29,7 @@ import org.hamcrest.Matcher; import org.junit.After; import org.junit.Test; import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.ConversionService; import org.springframework.data.geo.Distance; @@ -36,10 +39,14 @@ import org.springframework.data.web.PagedResourcesAssemblerArgumentResolver; import org.springframework.data.web.SortHandlerMethodArgumentResolver; import org.springframework.data.web.WebTestUtils; import org.springframework.data.web.config.EnableSpringDataWebSupport.SpringDataWebConfigurationImportSelector; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.util.ReflectionUtils; +import org.springframework.web.context.WebApplicationContext; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.util.UriComponentsBuilder; /** * Integration tests for {@link EnableSpringDataWebSupport}. @@ -57,6 +64,9 @@ public class EnableSpringDataWebSupportIntegrationTests { @EnableSpringDataWebSupport static class SampleConfig { + public @Bean SampleController controller() { + return new SampleController(); + } } @After @@ -141,6 +151,27 @@ public class EnableSpringDataWebSupportIntegrationTests { assertThat(conversionService.canConvert(Point.class, String.class), is(true)); } + /** + * @see DATACMNS-630 + */ + @Test + public void createsProxyForInterfaceBasedControllerMethodParameter() throws Exception { + + WebApplicationContext applicationContext = WebTestUtils.createApplicationContext(SampleConfig.class); + MockMvc mvc = MockMvcBuilders.webAppContextSetup(applicationContext).build(); + + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("/proxy"); + builder.queryParam("name", "Foo"); + builder.queryParam("shippingAddresses[0].zipCode", "ZIP"); + builder.queryParam("shippingAddresses[0].city", "City"); + builder.queryParam("billingAddress.zipCode", "ZIP"); + builder.queryParam("billingAddress.city", "City"); + builder.queryParam("date", "2014-01-11"); + + mvc.perform(post(builder.build().toString())).// + andExpect(status().isOk()); + } + @SuppressWarnings("unchecked") private static void assertResolversRegistered(ApplicationContext context, Class... resolverTypes) { diff --git a/src/test/java/org/springframework/data/web/config/SampleController.java b/src/test/java/org/springframework/data/web/config/SampleController.java new file mode 100644 index 000000000..d5fda0c3b --- /dev/null +++ b/src/test/java/org/springframework/data/web/config/SampleController.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015 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.data.web.config; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Collection; +import java.util.Date; + +import org.springframework.data.web.config.SampleController.SampleDto.Address; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * @author Oliver Gierke + */ +@Controller +public class SampleController { + + @RequestMapping("/proxy") + public String someMethod(SampleDto sampleDto) { + + assertThat(sampleDto, is(notNullValue())); + assertThat(sampleDto.getName(), is("Foo")); + assertThat(sampleDto.getDate(), is(notNullValue())); + + Collection
shippingAddresses = sampleDto.getShippingAddresses(); + + assertThat(shippingAddresses, is(hasSize(1))); + assertThat(shippingAddresses.iterator().next().getZipCode(), is("ZIP")); + assertThat(shippingAddresses.iterator().next().getCity(), is("City")); + + assertThat(sampleDto.getBillingAddress(), is(notNullValue())); + assertThat(sampleDto.getBillingAddress().getZipCode(), is("ZIP")); + assertThat(sampleDto.getBillingAddress().getCity(), is("City")); + + return "view"; + } + + interface SampleDto { + + String getName(); + + @DateTimeFormat(iso = ISO.DATE) + Date getDate(); + + Address getBillingAddress(); + + Collection
getShippingAddresses(); + + interface Address { + + String getZipCode(); + + String getCity(); + } + } +}