BeanWrapper supports traversal of nested paths with Java 8 Optional declarations

Issue: SPR-12241
This commit is contained in:
Juergen Hoeller 2014-09-24 17:56:24 +02:00
parent 281b243b88
commit 0934751d7a
4 changed files with 143 additions and 9 deletions

View File

@ -33,6 +33,7 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.apache.commons.logging.Log;
@ -44,7 +45,9 @@ import org.springframework.core.convert.ConversionException;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.Property;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.lang.UsesJava8;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@ -91,6 +94,18 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
*/
private static final Log logger = LogFactory.getLog(BeanWrapperImpl.class);
private static Class<?> javaUtilOptionalClass = null;
static {
try {
javaUtilOptionalClass =
ClassUtils.forName("java.util.Optional", BeanWrapperImpl.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
// Java 8 not available - Optional references simply not supported then.
}
}
/** The wrapped object */
private Object object;
@ -209,12 +224,17 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
*/
public void setWrappedInstance(Object object, String nestedPath, Object rootObject) {
Assert.notNull(object, "Bean object must not be null");
this.object = object;
if (object.getClass().equals(javaUtilOptionalClass)) {
this.object = OptionalUnwrapper.unwrap(object);
}
else {
this.object = object;
}
this.nestedPath = (nestedPath != null ? nestedPath : "");
this.rootObject = (!"".equals(this.nestedPath) ? rootObject : object);
this.rootObject = (!"".equals(this.nestedPath) ? rootObject : this.object);
this.nestedBeanWrappers = null;
this.typeConverterDelegate = new TypeConverterDelegate(this, object);
setIntrospectionClass(object.getClass());
this.typeConverterDelegate = new TypeConverterDelegate(this, this.object);
setIntrospectionClass(this.object.getClass());
}
@Override
@ -549,7 +569,8 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty);
String canonicalName = tokens.canonicalName;
Object propertyValue = getPropertyValue(tokens);
if (propertyValue == null) {
if (propertyValue == null ||
(propertyValue.getClass().equals(javaUtilOptionalClass) && OptionalUnwrapper.isEmpty(propertyValue))) {
if (isAutoGrowNestedPaths()) {
propertyValue = setDefaultValue(tokens);
}
@ -1172,10 +1193,6 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
}
//---------------------------------------------------------------------
// Inner class for internal use
//---------------------------------------------------------------------
private static class PropertyTokenHolder {
public String canonicalName;
@ -1185,4 +1202,22 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
public String[] keys;
}
/**
* Inner class to avoid a hard dependency on Java 8.
*/
@UsesJava8
private static class OptionalUnwrapper {
public static Object unwrap(Object optionalObject) {
Optional<?> optional = (Optional<?>) optionalObject;
Assert.isTrue(optional.isPresent(), "Optional value must be present");
return optional.get();
}
public static boolean isEmpty(Object optionalObject) {
return !((Optional<?>) optionalObject).isPresent();
}
}
}

View File

@ -28,6 +28,7 @@ import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.SortedMap;
@ -1537,9 +1538,45 @@ public final class BeanWrapperTests extends AbstractConfigurablePropertyAccessor
BeanWrapperImpl bwi = new BeanWrapperImpl(foo);
bwi.setPropertyValue("object", "a String");
assertEquals("a String", foo.value);
assertTrue(foo.getObject() == 8);
assertEquals(8, bwi.getPropertyValue("object"));
}
@Test
public void testGetterWithOptional() {
GetterWithOptional foo = new GetterWithOptional();
TestBean tb = new TestBean("x");
BeanWrapperImpl bwi = new BeanWrapperImpl(foo);
bwi.setPropertyValue("object", tb);
assertSame(tb, foo.value);
assertSame(tb, foo.getObject().get());
assertSame(tb, ((Optional<String>) bwi.getPropertyValue("object")).get());
assertEquals("x", foo.value.getName());
assertEquals("x", foo.getObject().get().getName());
assertEquals("x", bwi.getPropertyValue("object.name"));
bwi.setPropertyValue("object.name", "y");
assertSame(tb, foo.value);
assertSame(tb, foo.getObject().get());
assertSame(tb, ((Optional<String>) bwi.getPropertyValue("object")).get());
assertEquals("y", foo.value.getName());
assertEquals("y", foo.getObject().get().getName());
assertEquals("y", bwi.getPropertyValue("object.name"));
}
@Test
public void testGetterWithOptionalAndAutoGrowing() {
GetterWithOptional foo = new GetterWithOptional();
BeanWrapperImpl bwi = new BeanWrapperImpl(foo);
bwi.setAutoGrowNestedPaths(true);
bwi.setPropertyValue("object.name", "x");
assertEquals("x", foo.value.getName());
assertEquals("x", foo.getObject().get().getName());
assertEquals("x", bwi.getPropertyValue("object.name"));
}
@Test
public void testGenericArraySetter() {
SkipReaderStub foo = new SkipReaderStub();
@ -1967,6 +2004,20 @@ public final class BeanWrapperTests extends AbstractConfigurablePropertyAccessor
}
public static class GetterWithOptional {
public TestBean value;
public void setObject(TestBean object) {
this.value = object;
}
public Optional<TestBean> getObject() {
return Optional.ofNullable(this.value);
}
}
public static class SkipReaderStub<T> {
public T[] items;

View File

@ -25,6 +25,7 @@ import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
@ -211,6 +212,18 @@ public class ValidatorFactoryTests {
assertNull(rejected);
}
@Test
public void testValidationWithOptionalField() throws Exception {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.afterPropertiesSet();
MainBeanWithOptional mainBean = new MainBeanWithOptional();
Errors errors = new BeanPropertyBindingResult(mainBean, "mainBean");
validator.validate(mainBean, errors);
Object rejected = errors.getFieldValue("inner.value");
assertNull(rejected);
}
@NameAddressValid
public static class ValidPerson {
@ -318,6 +331,17 @@ public class ValidatorFactoryTests {
}
public static class MainBeanWithOptional {
@InnerValid
private InnerBean inner = new InnerBean();
public Optional<InnerBean> getInner() {
return Optional.ofNullable(inner);
}
}
public static class InnerBean {
private String value;

View File

@ -25,6 +25,7 @@ import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
@ -213,6 +214,18 @@ public class ValidatorFactoryTests {
assertNull(rejected);
}
@Test
public void testValidationWithOptionalField() throws Exception {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.afterPropertiesSet();
MainBeanWithOptional mainBean = new MainBeanWithOptional();
Errors errors = new BeanPropertyBindingResult(mainBean, "mainBean");
validator.validate(mainBean, errors);
Object rejected = errors.getFieldValue("inner.value");
assertNull(rejected);
}
@NameAddressValid
public static class ValidPerson {
@ -320,6 +333,17 @@ public class ValidatorFactoryTests {
}
public static class MainBeanWithOptional {
@InnerValid
private InnerBean inner = new InnerBean();
public Optional<InnerBean> getInner() {
return Optional.ofNullable(inner);
}
}
public static class InnerBean {
private String value;