Browse Source
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.pull/113/head
7 changed files with 650 additions and 5 deletions
@ -0,0 +1,301 @@
@@ -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<String, Object>()); |
||||
|
||||
this.type = type; |
||||
this.conversionService = conversionService; |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.validation.DataBinder#getTarget() |
||||
*/ |
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public Map<String, Object> getTarget() { |
||||
return (Map<String, Object>) 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<String, Object> 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<String, Object> 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<String, Object>) 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); |
||||
|
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,93 @@
@@ -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) {} |
||||
} |
||||
@ -0,0 +1,142 @@
@@ -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<String, Object> nested = new HashMap<String, Object>(); |
||||
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<String, Object> result = bind(values); |
||||
|
||||
List<String> list = new ArrayList<String>(); |
||||
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<String, Object> result = bind(values); |
||||
|
||||
Map<String, Object> dave = new HashMap<String, Object>(); |
||||
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.<String, Object> emptyMap())); |
||||
} |
||||
|
||||
private static Map<String, Object> 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<String> getFooBar(); |
||||
} |
||||
} |
||||
@ -0,0 +1,74 @@
@@ -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<Address> 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<Address> getShippingAddresses(); |
||||
|
||||
interface Address { |
||||
|
||||
String getZipCode(); |
||||
|
||||
String getCity(); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue