From 0934751d7aa625fd098086ce3a5fb489f2edc7e0 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 24 Sep 2014 17:56:24 +0200 Subject: [PATCH] BeanWrapper supports traversal of nested paths with Java 8 Optional declarations Issue: SPR-12241 --- .../beans/BeanWrapperImpl.java | 53 +++++++++++++++---- .../beans/BeanWrapperTests.java | 51 ++++++++++++++++++ .../beanvalidation/ValidatorFactoryTests.java | 24 +++++++++ .../ValidatorFactoryTests.java | 24 +++++++++ 4 files changed, 143 insertions(+), 9 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java index ef51e90b222..56af0f8c618 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java @@ -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(); + } + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java index 705f1f70116..895ffd38933 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java @@ -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) 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) 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 getObject() { + return Optional.ofNullable(this.value); + } + } + + public static class SkipReaderStub { public T[] items; diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java index 33e291325dd..26138af5373 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java @@ -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 getInner() { + return Optional.ofNullable(inner); + } + } + + public static class InnerBean { private String value; diff --git a/spring-orm-hibernate4/src/test/java/org/springframework/validation/hibernatevalidator5/ValidatorFactoryTests.java b/spring-orm-hibernate4/src/test/java/org/springframework/validation/hibernatevalidator5/ValidatorFactoryTests.java index 30633524063..03a42dff761 100644 --- a/spring-orm-hibernate4/src/test/java/org/springframework/validation/hibernatevalidator5/ValidatorFactoryTests.java +++ b/spring-orm-hibernate4/src/test/java/org/springframework/validation/hibernatevalidator5/ValidatorFactoryTests.java @@ -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 getInner() { + return Optional.ofNullable(inner); + } + } + + public static class InnerBean { private String value;