SPR-6745: metadata (annotations) attached to property accessors allowing formatting of values during conversion

git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@2964 50f2f4bb-b051-0410-bef5-90022cba6387
This commit is contained in:
Andy Clement 2010-02-12 01:50:52 +00:00
parent d2a33d488b
commit 878f937f23
15 changed files with 222 additions and 46 deletions

View File

@ -38,15 +38,24 @@ public interface TypeConverter {
*/
boolean canConvert(Class<?> sourceType, Class<?> targetType);
/**
* Return true if the type converter can convert the specified type to the desired target type.
* @param sourceType a type descriptor that describes the source type
* @param targetType a type descriptor that describes the requested result type
* @return true if that conversion can be performed
*/
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
/**
* Convert (may coerce) a value from one type to another, for example from a boolean to a string.
* The typeDescriptor parameter enables support for typed collections - if the caller really wishes they
* can have a List<Integer> for example, rather than simply a List.
* @param value the value to be converted
* @param typeDescriptor a type descriptor that supplies extra information about the requested result type
* @param sourceType a type descriptor that supplies extra information about the source object
* @param targetType a type descriptor that supplies extra information about the requested result type
* @return the converted value
* @throws EvaluationException if conversion is not possible
*/
Object convertValue(Object value, TypeDescriptor typeDescriptor) throws EvaluationException;
Object convertValue(Object value, TypeDescriptor sourceType, TypeDescriptor targetType);
}

View File

@ -19,6 +19,7 @@ package org.springframework.expression.common;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypedValue;
import org.springframework.util.ClassUtils;
/**
@ -40,15 +41,31 @@ public abstract class ExpressionUtils {
* @throws EvaluationException if there is a problem during conversion or conversion of the value to the specified
* type is not supported
*/
@SuppressWarnings("unchecked")
public static <T> T convert(EvaluationContext context, Object value, Class<T> targetType)
throws EvaluationException {
// TODO remove this function over time and use the one it delegates to
return convertTypedValue(context,new TypedValue(value,TypeDescriptor.forObject(value)),targetType);
}
/**
* Determines if there is a type converter available in the specified context and attempts to use it to convert the
* supplied value to the specified type. Throws an exception if conversion is not possible.
* @param context the evaluation context that may define a type converter
* @param typedValue the value to convert and a type descriptor describing it
* @param targetType the type to attempt conversion to
* @return the converted value
* @throws EvaluationException if there is a problem during conversion or conversion of the value to the specified
* type is not supported
*/
@SuppressWarnings("unchecked")
public static <T> T convertTypedValue(EvaluationContext context, TypedValue typedValue, Class<T> targetType) {
Object value = typedValue.getValue();
if (targetType == null || ClassUtils.isAssignableValue(targetType, value)) {
return (T) value;
}
if (context != null) {
return (T) context.getTypeConverter().convertValue(value, TypeDescriptor.valueOf(targetType));
return (T) context.getTypeConverter().convertValue(value, typedValue.getTypeDescriptor(), TypeDescriptor.valueOf(targetType));
}
throw new EvaluationException("Cannot convert value '" + value + "' to type '" + targetType.getName() + "'");
}

View File

@ -137,11 +137,11 @@ public class ExpressionState {
}
public Object convertValue(Object value, TypeDescriptor targetTypeDescriptor) throws EvaluationException {
return this.relatedContext.getTypeConverter().convertValue(value, targetTypeDescriptor);
return this.relatedContext.getTypeConverter().convertValue(value, TypeDescriptor.forObject(value), targetTypeDescriptor);
}
public Object convertValue(TypedValue value, TypeDescriptor targetTypeDescriptor) throws EvaluationException {
return this.relatedContext.getTypeConverter().convertValue(value.getValue(), targetTypeDescriptor);
return this.relatedContext.getTypeConverter().convertValue(value.getValue(), TypeDescriptor.forObject(value.getValue()), targetTypeDescriptor);
}
/*

View File

@ -17,6 +17,7 @@
package org.springframework.expression.spel;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypedValue;
/**
* Represents a node in the Ast for a parsed expression.
@ -33,6 +34,13 @@ public interface SpelNode {
*/
Object getValue(ExpressionState expressionState) throws EvaluationException;
/**
* Evaluate the expression node in the context of the supplied expression state and return the typed value.
* @param expressionState the current expression state (includes the context)
* @return the type value of this node evaluated against the specified state
*/
TypedValue getTypedValue(ExpressionState expressionState) throws EvaluationException;
/**
* Determine if this expression node will support a setValue() call.
*

View File

@ -96,6 +96,15 @@ public abstract class SpelNodeImpl implements SpelNode {
return getValue(new ExpressionState(new StandardEvaluationContext()));
}
}
public final TypedValue getTypedValue(ExpressionState expressionState) throws EvaluationException {
if (expressionState != null) {
return getValueInternal(expressionState);
} else {
// configuration not set - does that matter?
return getTypedValue(new ExpressionState(new StandardEvaluationContext()));
}
}
// by default Ast nodes are not writable
public boolean isWritable(ExpressionState expressionState) throws EvaluationException {

View File

@ -73,14 +73,14 @@ public class SpelExpression implements Expression {
public <T> T getValue(Class<T> expectedResultType) throws EvaluationException {
ExpressionState expressionState = new ExpressionState(getEvaluationContext(), configuration);
Object result = ast.getValue(expressionState);
return ExpressionUtils.convert(expressionState.getEvaluationContext(), result, expectedResultType);
TypedValue typedResultValue = ast.getTypedValue(expressionState);
return ExpressionUtils.convertTypedValue(expressionState.getEvaluationContext(), typedResultValue, expectedResultType);
}
public <T> T getValue(Object rootObject, Class<T> expectedResultType) throws EvaluationException {
ExpressionState expressionState = new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), configuration);
Object result = ast.getValue(expressionState);
return ExpressionUtils.convert(expressionState.getEvaluationContext(), result, expectedResultType);
TypedValue typedResultValue = ast.getTypedValue(expressionState);
return ExpressionUtils.convertTypedValue(expressionState.getEvaluationContext(), typedResultValue, expectedResultType);
}
public Object getValue(EvaluationContext context) throws EvaluationException {
@ -93,30 +93,14 @@ public class SpelExpression implements Expression {
return ast.getValue(new ExpressionState(context, toTypedValue(rootObject), configuration));
}
@SuppressWarnings("unchecked")
public <T> T getValue(EvaluationContext context, Class<T> expectedResultType) throws EvaluationException {
Object result = ast.getValue(new ExpressionState(context, configuration));
if (result != null && expectedResultType != null) {
Class<?> resultType = result.getClass();
if (!expectedResultType.isAssignableFrom(resultType)) {
// Attempt conversion to the requested type, may throw an exception
result = context.getTypeConverter().convertValue(result, TypeDescriptor.valueOf(expectedResultType));
}
}
return (T) result;
TypedValue typedResultValue = ast.getTypedValue(new ExpressionState(context, configuration));
return ExpressionUtils.convertTypedValue(context, typedResultValue, expectedResultType);
}
@SuppressWarnings("unchecked")
public <T> T getValue(EvaluationContext context, Object rootObject, Class<T> expectedResultType) throws EvaluationException {
Object result = ast.getValue(new ExpressionState(context, toTypedValue(rootObject), configuration));
if (result != null && expectedResultType != null) {
Class<?> resultType = result.getClass();
if (!expectedResultType.isAssignableFrom(resultType)) {
// Attempt conversion to the requested type, may throw an exception
result = context.getTypeConverter().convertValue(result, TypeDescriptor.valueOf(expectedResultType));
}
}
return (T) result;
TypedValue typedResultValue = ast.getTypedValue(new ExpressionState(context, toTypedValue(rootObject), configuration));
return ExpressionUtils.convertTypedValue(context, typedResultValue, expectedResultType);
}

View File

@ -0,0 +1,108 @@
/*
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.expression.spel.support;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.ReflectionUtils;
/**
* {@link TypeDescriptor} extension that exposes additional annotations as
* conversion metadata: namely, annotations on other accessor methods
* (getter/setter) and on the underlying field, if found.
*
* org.springframework.beans.BeanTypeDescriptor (beans module) is very
* similar to this but depending on that would introduce a beans
* dependency from the SpEL module.
*
* @author Juergen Hoeller
* @author Andy Clement
* @since 3.0
*/
public class BeanTypeDescriptor extends TypeDescriptor {
private final PropertyDescriptor propertyDescriptor;
private Annotation[] cachedAnnotations;
/**
* Create a new BeanTypeDescriptor for the given bean property.
*
* @param propertyDescriptor
* the corresponding JavaBean PropertyDescriptor
* @param methodParameter
* the target method parameter
* @param type
* the specific type to expose (may be an array/collection
* element)
*/
public BeanTypeDescriptor(PropertyDescriptor propertyDescriptor,
MethodParameter methodParameter, Class type) {
super(methodParameter, type);
this.propertyDescriptor = propertyDescriptor;
}
/**
* Return the underlying PropertyDescriptor.
*/
public PropertyDescriptor getPropertyDescriptor() {
return this.propertyDescriptor;
}
@Override
public Annotation[] getAnnotations() {
Annotation[] anns = this.cachedAnnotations;
if (anns == null) {
Field underlyingField = ReflectionUtils.findField(
getMethodParameter().getMethod().getDeclaringClass(),
this.propertyDescriptor.getName());
Map<Class, Annotation> annMap = new LinkedHashMap<Class, Annotation>();
if (underlyingField != null) {
for (Annotation ann : underlyingField.getAnnotations()) {
annMap.put(ann.annotationType(), ann);
}
}
Method targetMethod = getMethodParameter().getMethod();
Method writeMethod = this.propertyDescriptor.getWriteMethod();
Method readMethod = this.propertyDescriptor.getReadMethod();
if (writeMethod != null && writeMethod != targetMethod) {
for (Annotation ann : writeMethod.getAnnotations()) {
annMap.put(ann.annotationType(), ann);
}
}
if (readMethod != null && readMethod != targetMethod) {
for (Annotation ann : readMethod.getAnnotations()) {
annMap.put(ann.annotationType(), ann);
}
}
for (Annotation ann : targetMethod.getAnnotations()) {
annMap.put(ann.annotationType(), ann);
}
anns = annMap.values().toArray(new Annotation[annMap.size()]);
this.cachedAnnotations = anns;
}
return anns;
}
}

View File

@ -243,7 +243,7 @@ public class ReflectionHelper {
else {
targetType = requiredParameterTypes[argPosition];
}
arguments[argPosition] = converter.convertValue(arguments[argPosition], TypeDescriptor.valueOf(targetType));
arguments[argPosition] = converter.convertValue(arguments[argPosition], TypeDescriptor.forObject(arguments[argPosition]), TypeDescriptor.valueOf(targetType));
}
}
@ -283,7 +283,7 @@ public class ReflectionHelper {
if (converter == null) {
throw new SpelEvaluationException(SpelMessage.TYPE_CONVERSION_ERROR, arguments[i].getClass().getName(),targetType);
}
arguments[i] = converter.convertValue(arguments[i], TypeDescriptor.valueOf(targetType));
arguments[i] = converter.convertValue(arguments[i], TypeDescriptor.forObject(arguments[i]), TypeDescriptor.valueOf(targetType));
}
}
catch (EvaluationException ex) {

View File

@ -58,7 +58,7 @@ class ReflectiveConstructorExecutor implements ConstructorExecutor {
}
ReflectionUtils.makeAccessible(this.ctor);
return new TypedValue(this.ctor.newInstance(arguments),
TypeDescriptor.valueOf(this.ctor.getClass()));
TypeDescriptor.valueOf(this.ctor.getDeclaringClass()));
}
catch (Exception ex) {
throw new AccessException("Problem invoking constructor: " + this.ctor, ex);

View File

@ -16,6 +16,8 @@
package org.springframework.expression.spel.support;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
@ -77,7 +79,15 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
}
Method method = findGetterForProperty(name, type, target instanceof Class);
if (method != null) {
TypeDescriptor typeDescriptor = new TypeDescriptor(new MethodParameter(method,-1));
// Treat it like a property
PropertyDescriptor propertyDescriptor = null;
try {
// The readerCache will only contain gettable properties (let's not worry about setters for now)
propertyDescriptor = new PropertyDescriptor(name,method,null);
} catch (IntrospectionException ex) {
throw new AccessException("Unable to access property '" + name + "' through getter "+method, ex);
}
TypeDescriptor typeDescriptor = new BeanTypeDescriptor(propertyDescriptor, new MethodParameter(method,-1), method.getReturnType());
this.readerCache.put(cacheKey, new InvokerPair(method,typeDescriptor));
this.typeDescriptorCache.put(cacheKey, typeDescriptor);
return true;
@ -118,7 +128,17 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
if (method == null) {
method = findGetterForProperty(name, type, target instanceof Class);
if (method != null) {
invoker = new InvokerPair(method,new TypeDescriptor(new MethodParameter(method,-1)));
// TODO remove the duplication here between canRead and read
// Treat it like a property
PropertyDescriptor propertyDescriptor = null;
try {
// The readerCache will only contain gettable properties (let's not worry about setters for now)
propertyDescriptor = new PropertyDescriptor(name,method,null);
} catch (IntrospectionException ex) {
throw new AccessException("Unable to access property '" + name + "' through getter "+method, ex);
}
TypeDescriptor typeDescriptor = new BeanTypeDescriptor(propertyDescriptor, new MethodParameter(method,-1), method.getReturnType());
invoker = new InvokerPair(method,typeDescriptor);
this.readerCache.put(cacheKey, invoker);
}
}
@ -173,8 +193,17 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
}
Method method = findSetterForProperty(name, type, target instanceof Class);
if (method != null) {
// Treat it like a property
PropertyDescriptor propertyDescriptor = null;
try {
propertyDescriptor = new PropertyDescriptor(name,null,method);
} catch (IntrospectionException ex) {
throw new AccessException("Unable to access property '" + name + "' through setter "+method, ex);
}
MethodParameter mp = new MethodParameter(method,0);
TypeDescriptor typeDescriptor = new BeanTypeDescriptor(propertyDescriptor,mp,mp.getParameterType());
this.writerCache.put(cacheKey, method);
this.typeDescriptorCache.put(cacheKey, new TypeDescriptor(new MethodParameter(method,0)));
this.typeDescriptorCache.put(cacheKey, typeDescriptor);
return true;
}
else {
@ -198,7 +227,7 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
TypeDescriptor typeDescriptor = getTypeDescriptor(context, target, name);
if (typeDescriptor != null) {
try {
possiblyConvertedNewValue = context.getTypeConverter().convertValue(newValue, typeDescriptor);
possiblyConvertedNewValue = context.getTypeConverter().convertValue(newValue, TypeDescriptor.forObject(newValue), typeDescriptor);
} catch (EvaluationException evaluationException) {
throw new AccessException("Type conversion failure",evaluationException);
}

View File

@ -21,7 +21,6 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
@ -62,17 +61,21 @@ public class StandardTypeConverter implements TypeConverter {
return this.conversionService.canConvert(sourceType, targetType);
}
public Object convertValue(Object value, TypeDescriptor typeDescriptor) throws EvaluationException {
public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
return this.conversionService.canConvert(sourceType, targetType);
}
public Object convertValue(Object value, TypeDescriptor sourceType, TypeDescriptor targetType) {
try {
return this.conversionService.convert(value, TypeDescriptor.forObject(value), typeDescriptor);
return this.conversionService.convert(value, sourceType, targetType);
}
catch (ConverterNotFoundException cenfe) {
throw new SpelEvaluationException(cenfe, SpelMessage.TYPE_CONVERSION_ERROR,
(value != null ? value.getClass() : null), typeDescriptor.asString());
sourceType.toString(), targetType.asString());
}
catch (ConversionException ce) {
throw new SpelEvaluationException(ce, SpelMessage.TYPE_CONVERSION_ERROR,
(value != null ? value.getClass() : null), typeDescriptor.asString());
sourceType.toString(), targetType.asString());
}
}

View File

@ -108,6 +108,14 @@ public class ExpressionTestsUsingCoreConversionService extends ExpressionTestCas
public Object convertValue(Object value, TypeDescriptor typeDescriptor) throws EvaluationException {
return this.service.convert(value, TypeDescriptor.forObject(value), typeDescriptor);
}
public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
return this.service.canConvert(sourceType, targetType);
}
public Object convertValue(Object value, TypeDescriptor sourceType, TypeDescriptor targetType) throws EvaluationException {
return this.service.convert(value, sourceType, targetType);
}
}
}

View File

@ -188,7 +188,7 @@ public class PropertyAccessTests extends ExpressionTestCase {
if (!name.equals("flibbles"))
throw new RuntimeException("Assertion Failed! name should be flibbles");
try {
flibbles = (Integer) context.getTypeConverter().convertValue(newValue, TypeDescriptor.valueOf(Integer.class));
flibbles = (Integer) context.getTypeConverter().convertValue(newValue, TypeDescriptor.forObject(newValue), TypeDescriptor.valueOf(Integer.class));
}catch (EvaluationException e) {
throw new AccessException("Cannot set flibbles to an object of type '" + newValue.getClass() + "'");
}

View File

@ -72,7 +72,7 @@ public class StandardComponentsTests {
@Test
public void testStandardTypeConverter() throws EvaluationException {
TypeConverter tc = new StandardTypeConverter();
tc.convertValue(3, TypeDescriptor.valueOf(Double.class));
tc.convertValue(3, TypeDescriptor.forObject(3), TypeDescriptor.valueOf(Double.class));
}
}

View File

@ -57,7 +57,8 @@ public class EvalTagTests extends AbstractTagTests {
assertEquals(Tag.EVAL_BODY_INCLUDE, action);
action = tag.doEndTag();
assertEquals(Tag.EVAL_PAGE, action);
//assertEquals("25%", ((MockHttpServletResponse)context.getResponse()).getContentAsString());
assertEquals("25%", ((MockHttpServletResponse) context.getResponse())
.getContentAsString());
}
public static class Bean {