Skip to content

Commit cfc821d

Browse files
committed
DataBinder unwraps Optional objects and allows for proper handling of Optional.empty()
Issue: SPR-12241
1 parent 070642c commit cfc821d

File tree

4 files changed

+151
-37
lines changed

4 files changed

+151
-37
lines changed

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -568,11 +568,10 @@ private BeanWrapperImpl getNestedBeanWrapper(String nestedProperty) {
568568
// Get value of bean property.
569569
PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty);
570570
String canonicalName = tokens.canonicalName;
571-
Object propertyValue = getPropertyValue(tokens);
572-
if (propertyValue == null ||
573-
(propertyValue.getClass().equals(javaUtilOptionalClass) && OptionalUnwrapper.isEmpty(propertyValue))) {
571+
Object value = getPropertyValue(tokens);
572+
if (value == null || (value.getClass().equals(javaUtilOptionalClass) && OptionalUnwrapper.isEmpty(value))) {
574573
if (isAutoGrowNestedPaths()) {
575-
propertyValue = setDefaultValue(tokens);
574+
value = setDefaultValue(tokens);
576575
}
577576
else {
578577
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName);
@@ -581,11 +580,12 @@ private BeanWrapperImpl getNestedBeanWrapper(String nestedProperty) {
581580

582581
// Lookup cached sub-BeanWrapper, create new one if not found.
583582
BeanWrapperImpl nestedBw = this.nestedBeanWrappers.get(canonicalName);
584-
if (nestedBw == null || nestedBw.getWrappedInstance() != propertyValue) {
583+
if (nestedBw == null || nestedBw.getWrappedInstance() !=
584+
(value.getClass().equals(javaUtilOptionalClass) ? OptionalUnwrapper.unwrap(value) : value)) {
585585
if (logger.isTraceEnabled()) {
586586
logger.trace("Creating new nested BeanWrapper for property '" + canonicalName + "'");
587587
}
588-
nestedBw = newNestedBeanWrapper(propertyValue, this.nestedPath + canonicalName + NESTED_PROPERTY_SEPARATOR);
588+
nestedBw = newNestedBeanWrapper(value, this.nestedPath + canonicalName + NESTED_PROPERTY_SEPARATOR);
589589
// Inherit all type-specific PropertyEditors.
590590
copyDefaultEditorsTo(nestedBw);
591591
copyCustomEditorsTo(nestedBw, canonicalName);
@@ -1212,7 +1212,9 @@ private static class OptionalUnwrapper {
12121212
public static Object unwrap(Object optionalObject) {
12131213
Optional<?> optional = (Optional<?>) optionalObject;
12141214
Assert.isTrue(optional.isPresent(), "Optional value must be present");
1215-
return optional.get();
1215+
Object result = optional.get();
1216+
Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported");
1217+
return result;
12161218
}
12171219

12181220
public static boolean isEmpty(Object optionalObject) {

spring-context/src/main/java/org/springframework/validation/DataBinder.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.HashMap;
2525
import java.util.List;
2626
import java.util.Map;
27+
import java.util.Optional;
2728

2829
import org.apache.commons.logging.Log;
2930
import org.apache.commons.logging.LogFactory;
@@ -41,7 +42,9 @@
4142
import org.springframework.beans.TypeMismatchException;
4243
import org.springframework.core.MethodParameter;
4344
import org.springframework.core.convert.ConversionService;
45+
import org.springframework.lang.UsesJava8;
4446
import org.springframework.util.Assert;
47+
import org.springframework.util.ClassUtils;
4548
import org.springframework.util.ObjectUtils;
4649
import org.springframework.util.PatternMatchUtils;
4750
import org.springframework.util.StringUtils;
@@ -119,6 +122,19 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
119122
*/
120123
protected static final Log logger = LogFactory.getLog(DataBinder.class);
121124

125+
private static Class<?> javaUtilOptionalClass = null;
126+
127+
static {
128+
try {
129+
javaUtilOptionalClass =
130+
ClassUtils.forName("java.util.Optional", DataBinder.class.getClassLoader());
131+
}
132+
catch (ClassNotFoundException ex) {
133+
// Java 8 not available - Optional references simply not supported then.
134+
}
135+
}
136+
137+
122138
private final Object target;
123139

124140
private final String objectName;
@@ -165,7 +181,12 @@ public DataBinder(Object target) {
165181
* @param objectName the name of the target object
166182
*/
167183
public DataBinder(Object target, String objectName) {
168-
this.target = target;
184+
if (target != null && target.getClass().equals(javaUtilOptionalClass)) {
185+
this.target = OptionalUnwrapper.unwrap(target);
186+
}
187+
else {
188+
this.target = target;
189+
}
169190
this.objectName = objectName;
170191
}
171192

@@ -779,4 +800,22 @@ else if (validator != null) {
779800
return getBindingResult().getModel();
780801
}
781802

803+
804+
/**
805+
* Inner class to avoid a hard dependency on Java 8.
806+
*/
807+
@UsesJava8
808+
private static class OptionalUnwrapper {
809+
810+
public static Object unwrap(Object optionalObject) {
811+
Optional<?> optional = (Optional<?>) optionalObject;
812+
if (!optional.isPresent()) {
813+
return null;
814+
}
815+
Object result = optional.get();
816+
Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported");
817+
return result;
818+
}
819+
}
820+
782821
}

spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121

2222
import org.apache.commons.logging.Log;
2323
import org.apache.commons.logging.LogFactory;
24+
2425
import org.springframework.beans.BeanUtils;
2526
import org.springframework.core.MethodParameter;
2627
import org.springframework.core.annotation.AnnotationUtils;
@@ -58,6 +59,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
5859

5960
private final boolean annotationNotRequired;
6061

62+
6163
/**
6264
* @param annotationNotRequired if "true", non-simple method arguments and
6365
* return values are considered model attributes with or without a
@@ -67,6 +69,7 @@ public ModelAttributeMethodProcessor(boolean annotationNotRequired) {
6769
this.annotationNotRequired = annotationNotRequired;
6870
}
6971

72+
7073
/**
7174
* @return true if the parameter is annotated with {@link ModelAttribute}
7275
* or in default resolution mode also if it is not a simple type.
@@ -94,18 +97,16 @@ else if (this.annotationNotRequired) {
9497
* @throws Exception if WebDataBinder initialization fails.
9598
*/
9699
@Override
97-
public final Object resolveArgument(
98-
MethodParameter parameter, ModelAndViewContainer mavContainer,
99-
NativeWebRequest request, WebDataBinderFactory binderFactory)
100-
throws Exception {
100+
public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
101+
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
101102

102103
String name = ModelFactory.getNameForParameter(parameter);
103-
Object attribute = (mavContainer.containsAttribute(name)) ?
104-
mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);
104+
Object attribute = (mavContainer.containsAttribute(name) ?
105+
mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, webRequest));
105106

106-
WebDataBinder binder = binderFactory.createBinder(request, attribute, name);
107+
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
107108
if (binder.getTarget() != null) {
108-
bindRequestParameters(binder, request);
109+
bindRequestParameters(binder, webRequest);
109110
validateIfApplicable(binder, parameter);
110111
if (binder.getBindingResult().hasErrors()) {
111112
if (isBindExceptionRequired(binder, parameter)) {
@@ -120,17 +121,17 @@ public final Object resolveArgument(
120121
mavContainer.removeAttributes(bindingResultModel);
121122
mavContainer.addAllAttributes(bindingResultModel);
122123

123-
return binder.getTarget();
124+
return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
124125
}
125126

126127
/**
127128
* Extension point to create the model attribute if not found in the model.
128129
* The default implementation uses the default constructor.
129-
* @param attributeName the name of the attribute, never {@code null}
130+
* @param attributeName the name of the attribute (never {@code null})
130131
* @param parameter the method parameter
131132
* @param binderFactory for creating WebDataBinder instance
132133
* @param request the current request
133-
* @return the created model attribute, never {@code null}
134+
* @return the created model attribute (never {@code null})
134135
*/
135136
protected Object createAttribute(String attributeName, MethodParameter parameter,
136137
WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
@@ -155,9 +156,9 @@ protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest requ
155156
*/
156157
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
157158
Annotation[] annotations = parameter.getParameterAnnotations();
158-
for (Annotation annot : annotations) {
159-
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
160-
Object hints = AnnotationUtils.getValue(annot);
159+
for (Annotation ann : annotations) {
160+
if (ann.annotationType().getSimpleName().startsWith("Valid")) {
161+
Object hints = AnnotationUtils.getValue(ann);
161162
binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
162163
break;
163164
}
@@ -199,14 +200,13 @@ else if (this.annotationNotRequired) {
199200
* Add non-null return values to the {@link ModelAndViewContainer}.
200201
*/
201202
@Override
202-
public void handleReturnValue(
203-
Object returnValue, MethodParameter returnType,
204-
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
205-
throws Exception {
203+
public void handleReturnValue(Object returnValue, MethodParameter returnType,
204+
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
206205

207206
if (returnValue != null) {
208207
String name = ModelFactory.getNameForReturnValue(returnValue, returnType);
209208
mavContainer.addAttribute(name, returnValue);
210209
}
211210
}
211+
212212
}

0 commit comments

Comments
 (0)