Skip to content

Commit 117b138

Browse files
committed
SPR-6745: metadata (annotations) attached to property accessors allowing formatting of values during conversion
1 parent 0f65a0f commit 117b138

File tree

15 files changed

+222
-46
lines changed

15 files changed

+222
-46
lines changed

org.springframework.expression/src/main/java/org/springframework/expression/TypeConverter.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,24 @@ public interface TypeConverter {
3838
*/
3939
boolean canConvert(Class<?> sourceType, Class<?> targetType);
4040

41+
/**
42+
* Return true if the type converter can convert the specified type to the desired target type.
43+
* @param sourceType a type descriptor that describes the source type
44+
* @param targetType a type descriptor that describes the requested result type
45+
* @return true if that conversion can be performed
46+
*/
47+
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
48+
4149
/**
4250
* Convert (may coerce) a value from one type to another, for example from a boolean to a string.
4351
* The typeDescriptor parameter enables support for typed collections - if the caller really wishes they
4452
* can have a List<Integer> for example, rather than simply a List.
4553
* @param value the value to be converted
46-
* @param typeDescriptor a type descriptor that supplies extra information about the requested result type
54+
* @param sourceType a type descriptor that supplies extra information about the source object
55+
* @param targetType a type descriptor that supplies extra information about the requested result type
4756
* @return the converted value
4857
* @throws EvaluationException if conversion is not possible
4958
*/
50-
Object convertValue(Object value, TypeDescriptor typeDescriptor) throws EvaluationException;
51-
59+
Object convertValue(Object value, TypeDescriptor sourceType, TypeDescriptor targetType);
60+
5261
}

org.springframework.expression/src/main/java/org/springframework/expression/common/ExpressionUtils.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.springframework.core.convert.TypeDescriptor;
2020
import org.springframework.expression.EvaluationContext;
2121
import org.springframework.expression.EvaluationException;
22+
import org.springframework.expression.TypedValue;
2223
import org.springframework.util.ClassUtils;
2324

2425
/**
@@ -40,15 +41,31 @@ public abstract class ExpressionUtils {
4041
* @throws EvaluationException if there is a problem during conversion or conversion of the value to the specified
4142
* type is not supported
4243
*/
43-
@SuppressWarnings("unchecked")
4444
public static <T> T convert(EvaluationContext context, Object value, Class<T> targetType)
4545
throws EvaluationException {
46+
// TODO remove this function over time and use the one it delegates to
47+
return convertTypedValue(context,new TypedValue(value,TypeDescriptor.forObject(value)),targetType);
48+
}
4649

50+
/**
51+
* Determines if there is a type converter available in the specified context and attempts to use it to convert the
52+
* supplied value to the specified type. Throws an exception if conversion is not possible.
53+
* @param context the evaluation context that may define a type converter
54+
* @param typedValue the value to convert and a type descriptor describing it
55+
* @param targetType the type to attempt conversion to
56+
* @return the converted value
57+
* @throws EvaluationException if there is a problem during conversion or conversion of the value to the specified
58+
* type is not supported
59+
*/
60+
@SuppressWarnings("unchecked")
61+
public static <T> T convertTypedValue(EvaluationContext context, TypedValue typedValue, Class<T> targetType) {
62+
Object value = typedValue.getValue();
63+
4764
if (targetType == null || ClassUtils.isAssignableValue(targetType, value)) {
4865
return (T) value;
4966
}
5067
if (context != null) {
51-
return (T) context.getTypeConverter().convertValue(value, TypeDescriptor.valueOf(targetType));
68+
return (T) context.getTypeConverter().convertValue(value, typedValue.getTypeDescriptor(), TypeDescriptor.valueOf(targetType));
5269
}
5370
throw new EvaluationException("Cannot convert value '" + value + "' to type '" + targetType.getName() + "'");
5471
}

org.springframework.expression/src/main/java/org/springframework/expression/spel/ExpressionState.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,11 @@ public Class<?> findType(String type) throws EvaluationException {
137137
}
138138

139139
public Object convertValue(Object value, TypeDescriptor targetTypeDescriptor) throws EvaluationException {
140-
return this.relatedContext.getTypeConverter().convertValue(value, targetTypeDescriptor);
140+
return this.relatedContext.getTypeConverter().convertValue(value, TypeDescriptor.forObject(value), targetTypeDescriptor);
141141
}
142142

143143
public Object convertValue(TypedValue value, TypeDescriptor targetTypeDescriptor) throws EvaluationException {
144-
return this.relatedContext.getTypeConverter().convertValue(value.getValue(), targetTypeDescriptor);
144+
return this.relatedContext.getTypeConverter().convertValue(value.getValue(), TypeDescriptor.forObject(value.getValue()), targetTypeDescriptor);
145145
}
146146

147147
/*

org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelNode.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.expression.spel;
1818

1919
import org.springframework.expression.EvaluationException;
20+
import org.springframework.expression.TypedValue;
2021

2122
/**
2223
* Represents a node in the Ast for a parsed expression.
@@ -33,6 +34,13 @@ public interface SpelNode {
3334
*/
3435
Object getValue(ExpressionState expressionState) throws EvaluationException;
3536

37+
/**
38+
* Evaluate the expression node in the context of the supplied expression state and return the typed value.
39+
* @param expressionState the current expression state (includes the context)
40+
* @return the type value of this node evaluated against the specified state
41+
*/
42+
TypedValue getTypedValue(ExpressionState expressionState) throws EvaluationException;
43+
3644
/**
3745
* Determine if this expression node will support a setValue() call.
3846
*

org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ public final Object getValue(ExpressionState expressionState) throws EvaluationE
9696
return getValue(new ExpressionState(new StandardEvaluationContext()));
9797
}
9898
}
99+
100+
public final TypedValue getTypedValue(ExpressionState expressionState) throws EvaluationException {
101+
if (expressionState != null) {
102+
return getValueInternal(expressionState);
103+
} else {
104+
// configuration not set - does that matter?
105+
return getTypedValue(new ExpressionState(new StandardEvaluationContext()));
106+
}
107+
}
99108

100109
// by default Ast nodes are not writable
101110
public boolean isWritable(ExpressionState expressionState) throws EvaluationException {

org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,14 @@ public Object getValue(Object rootObject) throws EvaluationException {
7373

7474
public <T> T getValue(Class<T> expectedResultType) throws EvaluationException {
7575
ExpressionState expressionState = new ExpressionState(getEvaluationContext(), configuration);
76-
Object result = ast.getValue(expressionState);
77-
return ExpressionUtils.convert(expressionState.getEvaluationContext(), result, expectedResultType);
76+
TypedValue typedResultValue = ast.getTypedValue(expressionState);
77+
return ExpressionUtils.convertTypedValue(expressionState.getEvaluationContext(), typedResultValue, expectedResultType);
7878
}
7979

8080
public <T> T getValue(Object rootObject, Class<T> expectedResultType) throws EvaluationException {
8181
ExpressionState expressionState = new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), configuration);
82-
Object result = ast.getValue(expressionState);
83-
return ExpressionUtils.convert(expressionState.getEvaluationContext(), result, expectedResultType);
82+
TypedValue typedResultValue = ast.getTypedValue(expressionState);
83+
return ExpressionUtils.convertTypedValue(expressionState.getEvaluationContext(), typedResultValue, expectedResultType);
8484
}
8585

8686
public Object getValue(EvaluationContext context) throws EvaluationException {
@@ -93,30 +93,14 @@ public Object getValue(EvaluationContext context, Object rootObject) throws Eval
9393
return ast.getValue(new ExpressionState(context, toTypedValue(rootObject), configuration));
9494
}
9595

96-
@SuppressWarnings("unchecked")
9796
public <T> T getValue(EvaluationContext context, Class<T> expectedResultType) throws EvaluationException {
98-
Object result = ast.getValue(new ExpressionState(context, configuration));
99-
if (result != null && expectedResultType != null) {
100-
Class<?> resultType = result.getClass();
101-
if (!expectedResultType.isAssignableFrom(resultType)) {
102-
// Attempt conversion to the requested type, may throw an exception
103-
result = context.getTypeConverter().convertValue(result, TypeDescriptor.valueOf(expectedResultType));
104-
}
105-
}
106-
return (T) result;
97+
TypedValue typedResultValue = ast.getTypedValue(new ExpressionState(context, configuration));
98+
return ExpressionUtils.convertTypedValue(context, typedResultValue, expectedResultType);
10799
}
108100

109-
@SuppressWarnings("unchecked")
110101
public <T> T getValue(EvaluationContext context, Object rootObject, Class<T> expectedResultType) throws EvaluationException {
111-
Object result = ast.getValue(new ExpressionState(context, toTypedValue(rootObject), configuration));
112-
if (result != null && expectedResultType != null) {
113-
Class<?> resultType = result.getClass();
114-
if (!expectedResultType.isAssignableFrom(resultType)) {
115-
// Attempt conversion to the requested type, may throw an exception
116-
result = context.getTypeConverter().convertValue(result, TypeDescriptor.valueOf(expectedResultType));
117-
}
118-
}
119-
return (T) result;
102+
TypedValue typedResultValue = ast.getTypedValue(new ExpressionState(context, toTypedValue(rootObject), configuration));
103+
return ExpressionUtils.convertTypedValue(context, typedResultValue, expectedResultType);
120104
}
121105

122106

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2002-2009 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.expression.spel.support;
18+
19+
import java.beans.PropertyDescriptor;
20+
import java.lang.annotation.Annotation;
21+
import java.lang.reflect.Field;
22+
import java.lang.reflect.Method;
23+
import java.util.LinkedHashMap;
24+
import java.util.Map;
25+
26+
import org.springframework.core.MethodParameter;
27+
import org.springframework.core.convert.TypeDescriptor;
28+
import org.springframework.util.ReflectionUtils;
29+
30+
/**
31+
* {@link TypeDescriptor} extension that exposes additional annotations as
32+
* conversion metadata: namely, annotations on other accessor methods
33+
* (getter/setter) and on the underlying field, if found.
34+
*
35+
* org.springframework.beans.BeanTypeDescriptor (beans module) is very
36+
* similar to this but depending on that would introduce a beans
37+
* dependency from the SpEL module.
38+
*
39+
* @author Juergen Hoeller
40+
* @author Andy Clement
41+
* @since 3.0
42+
*/
43+
public class BeanTypeDescriptor extends TypeDescriptor {
44+
45+
private final PropertyDescriptor propertyDescriptor;
46+
47+
private Annotation[] cachedAnnotations;
48+
49+
/**
50+
* Create a new BeanTypeDescriptor for the given bean property.
51+
*
52+
* @param propertyDescriptor
53+
* the corresponding JavaBean PropertyDescriptor
54+
* @param methodParameter
55+
* the target method parameter
56+
* @param type
57+
* the specific type to expose (may be an array/collection
58+
* element)
59+
*/
60+
public BeanTypeDescriptor(PropertyDescriptor propertyDescriptor,
61+
MethodParameter methodParameter, Class type) {
62+
super(methodParameter, type);
63+
this.propertyDescriptor = propertyDescriptor;
64+
}
65+
66+
/**
67+
* Return the underlying PropertyDescriptor.
68+
*/
69+
public PropertyDescriptor getPropertyDescriptor() {
70+
return this.propertyDescriptor;
71+
}
72+
73+
@Override
74+
public Annotation[] getAnnotations() {
75+
Annotation[] anns = this.cachedAnnotations;
76+
if (anns == null) {
77+
Field underlyingField = ReflectionUtils.findField(
78+
getMethodParameter().getMethod().getDeclaringClass(),
79+
this.propertyDescriptor.getName());
80+
Map<Class, Annotation> annMap = new LinkedHashMap<Class, Annotation>();
81+
if (underlyingField != null) {
82+
for (Annotation ann : underlyingField.getAnnotations()) {
83+
annMap.put(ann.annotationType(), ann);
84+
}
85+
}
86+
Method targetMethod = getMethodParameter().getMethod();
87+
Method writeMethod = this.propertyDescriptor.getWriteMethod();
88+
Method readMethod = this.propertyDescriptor.getReadMethod();
89+
if (writeMethod != null && writeMethod != targetMethod) {
90+
for (Annotation ann : writeMethod.getAnnotations()) {
91+
annMap.put(ann.annotationType(), ann);
92+
}
93+
}
94+
if (readMethod != null && readMethod != targetMethod) {
95+
for (Annotation ann : readMethod.getAnnotations()) {
96+
annMap.put(ann.annotationType(), ann);
97+
}
98+
}
99+
for (Annotation ann : targetMethod.getAnnotations()) {
100+
annMap.put(ann.annotationType(), ann);
101+
}
102+
anns = annMap.values().toArray(new Annotation[annMap.size()]);
103+
this.cachedAnnotations = anns;
104+
}
105+
return anns;
106+
}
107+
108+
}

org.springframework.expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ public static void convertArguments(Class[] requiredParameterTypes, boolean isVa
243243
else {
244244
targetType = requiredParameterTypes[argPosition];
245245
}
246-
arguments[argPosition] = converter.convertValue(arguments[argPosition], TypeDescriptor.valueOf(targetType));
246+
arguments[argPosition] = converter.convertValue(arguments[argPosition], TypeDescriptor.forObject(arguments[argPosition]), TypeDescriptor.valueOf(targetType));
247247
}
248248
}
249249

@@ -283,7 +283,7 @@ public static void convertAllArguments(Class[] parameterTypes, boolean isVarargs
283283
if (converter == null) {
284284
throw new SpelEvaluationException(SpelMessage.TYPE_CONVERSION_ERROR, arguments[i].getClass().getName(),targetType);
285285
}
286-
arguments[i] = converter.convertValue(arguments[i], TypeDescriptor.valueOf(targetType));
286+
arguments[i] = converter.convertValue(arguments[i], TypeDescriptor.forObject(arguments[i]), TypeDescriptor.valueOf(targetType));
287287
}
288288
}
289289
catch (EvaluationException ex) {

org.springframework.expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorExecutor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public TypedValue execute(EvaluationContext context, Object... arguments) throws
5858
}
5959
ReflectionUtils.makeAccessible(this.ctor);
6060
return new TypedValue(this.ctor.newInstance(arguments),
61-
TypeDescriptor.valueOf(this.ctor.getClass()));
61+
TypeDescriptor.valueOf(this.ctor.getDeclaringClass()));
6262
}
6363
catch (Exception ex) {
6464
throw new AccessException("Problem invoking constructor: " + this.ctor, ex);

org.springframework.expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.expression.spel.support;
1818

19+
import java.beans.IntrospectionException;
20+
import java.beans.PropertyDescriptor;
1921
import java.lang.reflect.Array;
2022
import java.lang.reflect.Field;
2123
import java.lang.reflect.Member;
@@ -77,7 +79,15 @@ public boolean canRead(EvaluationContext context, Object target, String name) th
7779
}
7880
Method method = findGetterForProperty(name, type, target instanceof Class);
7981
if (method != null) {
80-
TypeDescriptor typeDescriptor = new TypeDescriptor(new MethodParameter(method,-1));
82+
// Treat it like a property
83+
PropertyDescriptor propertyDescriptor = null;
84+
try {
85+
// The readerCache will only contain gettable properties (let's not worry about setters for now)
86+
propertyDescriptor = new PropertyDescriptor(name,method,null);
87+
} catch (IntrospectionException ex) {
88+
throw new AccessException("Unable to access property '" + name + "' through getter "+method, ex);
89+
}
90+
TypeDescriptor typeDescriptor = new BeanTypeDescriptor(propertyDescriptor, new MethodParameter(method,-1), method.getReturnType());
8191
this.readerCache.put(cacheKey, new InvokerPair(method,typeDescriptor));
8292
this.typeDescriptorCache.put(cacheKey, typeDescriptor);
8393
return true;
@@ -118,7 +128,17 @@ public TypedValue read(EvaluationContext context, Object target, String name) th
118128
if (method == null) {
119129
method = findGetterForProperty(name, type, target instanceof Class);
120130
if (method != null) {
121-
invoker = new InvokerPair(method,new TypeDescriptor(new MethodParameter(method,-1)));
131+
// TODO remove the duplication here between canRead and read
132+
// Treat it like a property
133+
PropertyDescriptor propertyDescriptor = null;
134+
try {
135+
// The readerCache will only contain gettable properties (let's not worry about setters for now)
136+
propertyDescriptor = new PropertyDescriptor(name,method,null);
137+
} catch (IntrospectionException ex) {
138+
throw new AccessException("Unable to access property '" + name + "' through getter "+method, ex);
139+
}
140+
TypeDescriptor typeDescriptor = new BeanTypeDescriptor(propertyDescriptor, new MethodParameter(method,-1), method.getReturnType());
141+
invoker = new InvokerPair(method,typeDescriptor);
122142
this.readerCache.put(cacheKey, invoker);
123143
}
124144
}
@@ -173,8 +193,17 @@ public boolean canWrite(EvaluationContext context, Object target, String name) t
173193
}
174194
Method method = findSetterForProperty(name, type, target instanceof Class);
175195
if (method != null) {
196+
// Treat it like a property
197+
PropertyDescriptor propertyDescriptor = null;
198+
try {
199+
propertyDescriptor = new PropertyDescriptor(name,null,method);
200+
} catch (IntrospectionException ex) {
201+
throw new AccessException("Unable to access property '" + name + "' through setter "+method, ex);
202+
}
203+
MethodParameter mp = new MethodParameter(method,0);
204+
TypeDescriptor typeDescriptor = new BeanTypeDescriptor(propertyDescriptor,mp,mp.getParameterType());
176205
this.writerCache.put(cacheKey, method);
177-
this.typeDescriptorCache.put(cacheKey, new TypeDescriptor(new MethodParameter(method,0)));
206+
this.typeDescriptorCache.put(cacheKey, typeDescriptor);
178207
return true;
179208
}
180209
else {
@@ -198,7 +227,7 @@ public void write(EvaluationContext context, Object target, String name, Object
198227
TypeDescriptor typeDescriptor = getTypeDescriptor(context, target, name);
199228
if (typeDescriptor != null) {
200229
try {
201-
possiblyConvertedNewValue = context.getTypeConverter().convertValue(newValue, typeDescriptor);
230+
possiblyConvertedNewValue = context.getTypeConverter().convertValue(newValue, TypeDescriptor.forObject(newValue), typeDescriptor);
202231
} catch (EvaluationException evaluationException) {
203232
throw new AccessException("Type conversion failure",evaluationException);
204233
}

0 commit comments

Comments
 (0)