Skip to content

Commit 0934751

Browse files
committed
BeanWrapper supports traversal of nested paths with Java 8 Optional declarations
Issue: SPR-12241
1 parent 281b243 commit 0934751

File tree

4 files changed

+143
-9
lines changed

4 files changed

+143
-9
lines changed

spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Iterator;
3434
import java.util.List;
3535
import java.util.Map;
36+
import java.util.Optional;
3637
import java.util.Set;
3738

3839
import org.apache.commons.logging.Log;
@@ -44,7 +45,9 @@
4445
import org.springframework.core.convert.ConverterNotFoundException;
4546
import org.springframework.core.convert.Property;
4647
import org.springframework.core.convert.TypeDescriptor;
48+
import org.springframework.lang.UsesJava8;
4749
import org.springframework.util.Assert;
50+
import org.springframework.util.ClassUtils;
4851
import org.springframework.util.ObjectUtils;
4952
import org.springframework.util.StringUtils;
5053

@@ -91,6 +94,18 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
9194
*/
9295
private static final Log logger = LogFactory.getLog(BeanWrapperImpl.class);
9396

97+
private static Class<?> javaUtilOptionalClass = null;
98+
99+
static {
100+
try {
101+
javaUtilOptionalClass =
102+
ClassUtils.forName("java.util.Optional", BeanWrapperImpl.class.getClassLoader());
103+
}
104+
catch (ClassNotFoundException ex) {
105+
// Java 8 not available - Optional references simply not supported then.
106+
}
107+
}
108+
94109

95110
/** The wrapped object */
96111
private Object object;
@@ -209,12 +224,17 @@ public void setWrappedInstance(Object object) {
209224
*/
210225
public void setWrappedInstance(Object object, String nestedPath, Object rootObject) {
211226
Assert.notNull(object, "Bean object must not be null");
212-
this.object = object;
227+
if (object.getClass().equals(javaUtilOptionalClass)) {
228+
this.object = OptionalUnwrapper.unwrap(object);
229+
}
230+
else {
231+
this.object = object;
232+
}
213233
this.nestedPath = (nestedPath != null ? nestedPath : "");
214-
this.rootObject = (!"".equals(this.nestedPath) ? rootObject : object);
234+
this.rootObject = (!"".equals(this.nestedPath) ? rootObject : this.object);
215235
this.nestedBeanWrappers = null;
216-
this.typeConverterDelegate = new TypeConverterDelegate(this, object);
217-
setIntrospectionClass(object.getClass());
236+
this.typeConverterDelegate = new TypeConverterDelegate(this, this.object);
237+
setIntrospectionClass(this.object.getClass());
218238
}
219239

220240
@Override
@@ -549,7 +569,8 @@ private BeanWrapperImpl getNestedBeanWrapper(String nestedProperty) {
549569
PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty);
550570
String canonicalName = tokens.canonicalName;
551571
Object propertyValue = getPropertyValue(tokens);
552-
if (propertyValue == null) {
572+
if (propertyValue == null ||
573+
(propertyValue.getClass().equals(javaUtilOptionalClass) && OptionalUnwrapper.isEmpty(propertyValue))) {
553574
if (isAutoGrowNestedPaths()) {
554575
propertyValue = setDefaultValue(tokens);
555576
}
@@ -1172,10 +1193,6 @@ public String toString() {
11721193
}
11731194

11741195

1175-
//---------------------------------------------------------------------
1176-
// Inner class for internal use
1177-
//---------------------------------------------------------------------
1178-
11791196
private static class PropertyTokenHolder {
11801197

11811198
public String canonicalName;
@@ -1185,4 +1202,22 @@ private static class PropertyTokenHolder {
11851202
public String[] keys;
11861203
}
11871204

1205+
1206+
/**
1207+
* Inner class to avoid a hard dependency on Java 8.
1208+
*/
1209+
@UsesJava8
1210+
private static class OptionalUnwrapper {
1211+
1212+
public static Object unwrap(Object optionalObject) {
1213+
Optional<?> optional = (Optional<?>) optionalObject;
1214+
Assert.isTrue(optional.isPresent(), "Optional value must be present");
1215+
return optional.get();
1216+
}
1217+
1218+
public static boolean isEmpty(Object optionalObject) {
1219+
return !((Optional<?>) optionalObject).isPresent();
1220+
}
1221+
}
1222+
11881223
}

spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.LinkedList;
2929
import java.util.List;
3030
import java.util.Map;
31+
import java.util.Optional;
3132
import java.util.Properties;
3233
import java.util.Set;
3334
import java.util.SortedMap;
@@ -1537,9 +1538,45 @@ public void testPropertyTypeMismatch() {
15371538
BeanWrapperImpl bwi = new BeanWrapperImpl(foo);
15381539
bwi.setPropertyValue("object", "a String");
15391540
assertEquals("a String", foo.value);
1541+
assertTrue(foo.getObject() == 8);
15401542
assertEquals(8, bwi.getPropertyValue("object"));
15411543
}
15421544

1545+
@Test
1546+
public void testGetterWithOptional() {
1547+
GetterWithOptional foo = new GetterWithOptional();
1548+
TestBean tb = new TestBean("x");
1549+
BeanWrapperImpl bwi = new BeanWrapperImpl(foo);
1550+
1551+
bwi.setPropertyValue("object", tb);
1552+
assertSame(tb, foo.value);
1553+
assertSame(tb, foo.getObject().get());
1554+
assertSame(tb, ((Optional<String>) bwi.getPropertyValue("object")).get());
1555+
assertEquals("x", foo.value.getName());
1556+
assertEquals("x", foo.getObject().get().getName());
1557+
assertEquals("x", bwi.getPropertyValue("object.name"));
1558+
1559+
bwi.setPropertyValue("object.name", "y");
1560+
assertSame(tb, foo.value);
1561+
assertSame(tb, foo.getObject().get());
1562+
assertSame(tb, ((Optional<String>) bwi.getPropertyValue("object")).get());
1563+
assertEquals("y", foo.value.getName());
1564+
assertEquals("y", foo.getObject().get().getName());
1565+
assertEquals("y", bwi.getPropertyValue("object.name"));
1566+
}
1567+
1568+
@Test
1569+
public void testGetterWithOptionalAndAutoGrowing() {
1570+
GetterWithOptional foo = new GetterWithOptional();
1571+
BeanWrapperImpl bwi = new BeanWrapperImpl(foo);
1572+
bwi.setAutoGrowNestedPaths(true);
1573+
1574+
bwi.setPropertyValue("object.name", "x");
1575+
assertEquals("x", foo.value.getName());
1576+
assertEquals("x", foo.getObject().get().getName());
1577+
assertEquals("x", bwi.getPropertyValue("object.name"));
1578+
}
1579+
15431580
@Test
15441581
public void testGenericArraySetter() {
15451582
SkipReaderStub foo = new SkipReaderStub();
@@ -1967,6 +2004,20 @@ public Integer getObject() {
19672004
}
19682005

19692006

2007+
public static class GetterWithOptional {
2008+
2009+
public TestBean value;
2010+
2011+
public void setObject(TestBean object) {
2012+
this.value = object;
2013+
}
2014+
2015+
public Optional<TestBean> getObject() {
2016+
return Optional.ofNullable(this.value);
2017+
}
2018+
}
2019+
2020+
19702021
public static class SkipReaderStub<T> {
19712022

19722023
public T[] items;

spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.LinkedHashSet;
2626
import java.util.LinkedList;
2727
import java.util.List;
28+
import java.util.Optional;
2829
import java.util.Set;
2930
import javax.validation.Constraint;
3031
import javax.validation.ConstraintValidator;
@@ -211,6 +212,18 @@ public void testInnerBeanValidation() throws Exception {
211212
assertNull(rejected);
212213
}
213214

215+
@Test
216+
public void testValidationWithOptionalField() throws Exception {
217+
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
218+
validator.afterPropertiesSet();
219+
220+
MainBeanWithOptional mainBean = new MainBeanWithOptional();
221+
Errors errors = new BeanPropertyBindingResult(mainBean, "mainBean");
222+
validator.validate(mainBean, errors);
223+
Object rejected = errors.getFieldValue("inner.value");
224+
assertNull(rejected);
225+
}
226+
214227

215228
@NameAddressValid
216229
public static class ValidPerson {
@@ -318,6 +331,17 @@ public InnerBean getInner() {
318331
}
319332

320333

334+
public static class MainBeanWithOptional {
335+
336+
@InnerValid
337+
private InnerBean inner = new InnerBean();
338+
339+
public Optional<InnerBean> getInner() {
340+
return Optional.ofNullable(inner);
341+
}
342+
}
343+
344+
321345
public static class InnerBean {
322346

323347
private String value;

spring-orm-hibernate4/src/test/java/org/springframework/validation/hibernatevalidator5/ValidatorFactoryTests.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.LinkedHashSet;
2626
import java.util.LinkedList;
2727
import java.util.List;
28+
import java.util.Optional;
2829
import java.util.Set;
2930
import javax.validation.Constraint;
3031
import javax.validation.ConstraintValidator;
@@ -213,6 +214,18 @@ public void testInnerBeanValidation() throws Exception {
213214
assertNull(rejected);
214215
}
215216

217+
@Test
218+
public void testValidationWithOptionalField() throws Exception {
219+
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
220+
validator.afterPropertiesSet();
221+
222+
MainBeanWithOptional mainBean = new MainBeanWithOptional();
223+
Errors errors = new BeanPropertyBindingResult(mainBean, "mainBean");
224+
validator.validate(mainBean, errors);
225+
Object rejected = errors.getFieldValue("inner.value");
226+
assertNull(rejected);
227+
}
228+
216229

217230
@NameAddressValid
218231
public static class ValidPerson {
@@ -320,6 +333,17 @@ public InnerBean getInner() {
320333
}
321334

322335

336+
public static class MainBeanWithOptional {
337+
338+
@InnerValid
339+
private InnerBean inner = new InnerBean();
340+
341+
public Optional<InnerBean> getInner() {
342+
return Optional.ofNullable(inner);
343+
}
344+
}
345+
346+
323347
public static class InnerBean {
324348

325349
private String value;

0 commit comments

Comments
 (0)