From c9057fd1daae0d01e1e5072e09ae15e1d93630ab Mon Sep 17 00:00:00 2001 From: Andy Clement Date: Tue, 13 Oct 2009 18:11:34 +0000 Subject: [PATCH] SPR-6230: SpEL improvements --- .../expression/EvaluationContext.java | 2 +- .../expression/Expression.java | 132 ++++++++++- .../expression/ExpressionException.java | 2 +- .../expression/PropertyAccessor.java | 5 - .../expression/TypedValue.java | 7 +- .../common/CompositeStringExpression.java | 62 +++++ .../expression/common/LiteralExpression.java | 61 +++++ .../expression/spel/ExpressionState.java | 60 +++-- .../expression/spel/SpelExpression.java | 114 ++++++++- .../expression/spel/ast/Indexer.java | 1 - .../expression/spel/ast/MethodReference.java | 1 - .../expression/spel/ast/Projection.java | 1 - .../spel/ast/PropertyOrFieldReference.java | 11 +- .../support/ReflectivePropertyResolver.java | 220 ++++++++++++++++-- .../support/StandardEvaluationContext.java | 55 ++++- .../expression/spel/ExpressionStateTests.java | 27 ++- .../expression/spel/HelperTests.java | 77 ++++++ .../spel/LiteralExpressionTests.java | 32 +++ .../spel/TemplateExpressionParsingTests.java | 55 +++++ 19 files changed, 848 insertions(+), 77 deletions(-) diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/EvaluationContext.java b/org.springframework.expression/src/main/java/org/springframework/expression/EvaluationContext.java index 9930e2131f6..80f28197417 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/EvaluationContext.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/EvaluationContext.java @@ -33,7 +33,7 @@ import java.util.List; public interface EvaluationContext { /** - * @return the root context object against which unqualified properties/methods/etc should be resolved + * @return the default root context object against which unqualified properties/methods/etc should be resolved. This can be overridden when evaluating an expression. */ TypedValue getRootObject(); diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/Expression.java b/org.springframework.expression/src/main/java/org/springframework/expression/Expression.java index e50d029d3d2..6fe9277362b 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/Expression.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/Expression.java @@ -36,8 +36,17 @@ public interface Expression { */ public Object getValue() throws EvaluationException; + /** + * Evaluate this expression against the specified root object + * + * @param rootObject the root object against which properties/etc will be resolved + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + public Object getValue(Object rootObject) throws EvaluationException; + /** - * Evaluate the expression in the default standard context. If the result of the evaluation does not match (and + * Evaluate the expression in the default context. If the result of the evaluation does not match (and * cannot be converted to) the expected result type then an exception will be returned. * * @param desiredResultType the class the caller would like the result to be @@ -45,6 +54,18 @@ public interface Expression { * @throws EvaluationException if there is a problem during evaluation */ public T getValue(Class desiredResultType) throws EvaluationException; + + /** + * Evaluate the expression in the default context against the specified root object. If the + * result of the evaluation does not match (and cannot be converted to) the expected result type + * then an exception will be returned. + * + * @param rootObject the root object against which properties/etc will be resolved + * @param desiredResultType the class the caller would like the result to be + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + public T getValue(Object rootObject, Class desiredResultType) throws EvaluationException; /** * Evaluate this expression in the provided context and return the result of evaluation. @@ -55,6 +76,17 @@ public interface Expression { */ public Object getValue(EvaluationContext context) throws EvaluationException; + /** + * Evaluate this expression in the provided context and return the result of evaluation, but use + * the supplied root context as an override for any default root object specified in the context. + * + * @param context the context in which to evaluate the expression + * @param rootObject the root object against which properties/etc will be resolved + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + public Object getValue(EvaluationContext context, Object rootObject) throws EvaluationException; + /** * Evaluate the expression in a specified context which can resolve references to properties, methods, types, etc - * the type of the evaluation result is expected to be of a particular class and an exception will be thrown if it @@ -67,6 +99,20 @@ public interface Expression { */ public T getValue(EvaluationContext context, Class desiredResultType) throws EvaluationException; + /** + * Evaluate the expression in a specified context which can resolve references to properties, methods, types, etc - + * the type of the evaluation result is expected to be of a particular class and an exception will be thrown if it + * is not and cannot be converted to that type. The supplied root object overrides any default specified on the + * supplied context. + * + * @param context the context in which to evaluate the expression + * @param rootObject the root object against which properties/etc will be resolved + * @param desiredResultType the class the caller would like the result to be + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + public T getValue(EvaluationContext context, Object rootObject, Class desiredResultType) throws EvaluationException; + /** * Returns the most general type that can be passed to the {@link #setValue(EvaluationContext, Object)} method using * the default context. @@ -76,6 +122,16 @@ public interface Expression { */ public Class getValueType() throws EvaluationException; + /** + * Returns the most general type that can be passed to the {@link #setValue(EvaluationContext, Object)} method using + * the default context. + * + * @param rootObject the root object against which to evaluate the expression + * @return the most general type of value that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + public Class getValueType(Object rootObject) throws EvaluationException; + /** * Returns the most general type that can be passed to the {@link #setValue(EvaluationContext, Object)} method for * the given context. @@ -85,6 +141,17 @@ public interface Expression { * @throws EvaluationException if there is a problem determining the type */ public Class getValueType(EvaluationContext context) throws EvaluationException; + + /** + * Returns the most general type that can be passed to the {@link #setValue(EvaluationContext, Object)} method for + * the given context. The supplied root object overrides any specified in the context. + * + * @param context the context in which to evaluate the expression + * @param rootObject the root object against which to evaluate the expression + * @return the most general type of value that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + public Class getValueType(EvaluationContext context, Object rootObject) throws EvaluationException; /** * Returns the most general type that can be passed to the {@link #setValue(EvaluationContext, Object)} method using @@ -94,6 +161,16 @@ public interface Expression { * @throws EvaluationException if there is a problem determining the type */ public TypeDescriptor getValueTypeDescriptor() throws EvaluationException; + + /** + * Returns the most general type that can be passed to the {@link #setValue(EvaluationContext, Object)} method using + * the default context. + * + * @param rootObject the root object against which to evaluate the expression + * @return a type descriptor for the most general type of value that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + public TypeDescriptor getValueTypeDescriptor(Object rootObject) throws EvaluationException; /** * Returns the most general type that can be passed to the {@link #setValue(EvaluationContext, Object)} method for @@ -105,6 +182,17 @@ public interface Expression { */ public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException; + /** + * Returns the most general type that can be passed to the {@link #setValue(EvaluationContext, Object)} method for + * the given context. The supplied root object overrides any specified in the context. + * + * @param context the context in which to evaluate the expression + * @param rootObject the root object against which to evaluate the expression + * @return a type descriptor for the most general type of value that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, Object rootObject) throws EvaluationException; + /** * Determine if an expression can be written to, i.e. setValue() can be called. * @@ -113,7 +201,27 @@ public interface Expression { * @throws EvaluationException if there is a problem determining if it is writable */ public boolean isWritable(EvaluationContext context) throws EvaluationException; - + + /** + * Determine if an expression can be written to, i.e. setValue() can be called. + * The supplied root object overrides any specified in the context. + * + * @param context the context in which the expression should be checked + * @param rootObject the root object against which to evaluate the expression + * @return true if the expression is writable + * @throws EvaluationException if there is a problem determining if it is writable + */ + public boolean isWritable(EvaluationContext context, Object rootObject) throws EvaluationException; + + /** + * Determine if an expression can be written to, i.e. setValue() can be called. + * + * @param rootObject the root object against which to evaluate the expression + * @return true if the expression is writable + * @throws EvaluationException if there is a problem determining if it is writable + */ + public boolean isWritable(Object rootObject) throws EvaluationException; + /** * Set this expression in the provided context to the value provided. * @@ -122,7 +230,27 @@ public interface Expression { * @throws EvaluationException if there is a problem during evaluation */ public void setValue(EvaluationContext context, Object value) throws EvaluationException; + + /** + * Set this expression in the provided context to the value provided. + * + * @param rootObject the root object against which to evaluate the expression + * @param value the new value + * @throws EvaluationException if there is a problem during evaluation + */ + public void setValue(Object rootObject, Object value) throws EvaluationException; + /** + * Set this expression in the provided context to the value provided. + * The supplied root object overrides any specified in the context. + * + * @param context the context in which to set the value of the expression + * @param rootObject the root object against which to evaluate the expression + * @param value the new value + * @throws EvaluationException if there is a problem during evaluation + */ + public void setValue(EvaluationContext context, Object rootObject, Object value) throws EvaluationException; + /** * Returns the original string used to create this expression, unmodified. * diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/ExpressionException.java b/org.springframework.expression/src/main/java/org/springframework/expression/ExpressionException.java index 1aaa566c3c5..b02852ddc0f 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/ExpressionException.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/ExpressionException.java @@ -23,7 +23,7 @@ package org.springframework.expression; * @author Andy Clement * @since 3.0 */ -public class ExpressionException extends Exception { +public class ExpressionException extends RuntimeException { protected String expressionString; protected int position; // -1 if not known - but should be known in all reasonable cases diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/PropertyAccessor.java b/org.springframework.expression/src/main/java/org/springframework/expression/PropertyAccessor.java index 30bc8f31c4b..6b080ea3736 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/PropertyAccessor.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/PropertyAccessor.java @@ -25,11 +25,6 @@ package org.springframework.expression; * to determine if it can read or write them. Property resolvers are considered to be ordered and each will be called in * turn. The only rule that affects the call order is that any naming the target class directly in * getSpecifiedTargetClasses() will be called first, before the general resolvers. - * - *

If the cost of locating the property is expensive, in relation to actually retrieving its value, consider extending - * CacheablePropertyAccessor rather than directly implementing PropertyAccessor. A CacheablePropertyAccessor enables the - * discovery (resolution) of the property to be done once and then an object (an executor) returned and cached by the - * infrastructure that can be used repeatedly to retrieve the property value. * * @author Andy Clement * @since 3.0 diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/TypedValue.java b/org.springframework.expression/src/main/java/org/springframework/expression/TypedValue.java index 08c71b75065..838a25ba4f5 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/TypedValue.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/TypedValue.java @@ -33,7 +33,7 @@ public class TypedValue { private final Object value; - private final TypeDescriptor typeDescriptor; + private TypeDescriptor typeDescriptor; /** @@ -43,7 +43,7 @@ public class TypedValue { */ public TypedValue(Object value) { this.value = value; - this.typeDescriptor = TypeDescriptor.forObject(value); + this.typeDescriptor = null;// initialized when/if requested } /** @@ -62,6 +62,9 @@ public class TypedValue { } public TypeDescriptor getTypeDescriptor() { + if (this.typeDescriptor==null) { + this.typeDescriptor = TypeDescriptor.forObject(value); + } return this.typeDescriptor; } diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java b/org.springframework.expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java index f78c8b13e9a..8a1667f900d 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java @@ -70,6 +70,15 @@ public class CompositeStringExpression implements Expression { } return sb.toString(); } + + public String getValue(Object rootObject) throws EvaluationException { + StringBuilder sb = new StringBuilder(); + for (Expression expression : this.expressions) { + sb.append(ObjectUtils.getDisplayString(expression.getValue(rootObject))); + } + return sb.toString(); + } + public Class getValueType(EvaluationContext context) { return String.class; @@ -109,4 +118,57 @@ public class CompositeStringExpression implements Expression { return expressions; } + + public T getValue(Object rootObject, Class desiredResultType) throws EvaluationException { + Object value = getValue(rootObject); + return ExpressionUtils.convert(null, value, desiredResultType); + } + + public String getValue(EvaluationContext context, Object rootObject) throws EvaluationException { + StringBuilder sb = new StringBuilder(); + for (Expression expression : this.expressions) { + sb.append(ObjectUtils.getDisplayString(expression.getValue(context,rootObject))); + } + return sb.toString(); + } + + public T getValue(EvaluationContext context, Object rootObject, Class desiredResultType) + throws EvaluationException { + Object value = getValue(context,rootObject); + return ExpressionUtils.convert(context, value, desiredResultType); + } + + public Class getValueType(Object rootObject) throws EvaluationException { + return String.class; + } + + public Class getValueType(EvaluationContext context, Object rootObject) throws EvaluationException { + return String.class; + } + + public TypeDescriptor getValueTypeDescriptor(Object rootObject) throws EvaluationException { + return TypeDescriptor.valueOf(String.class); + } + + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, Object rootObject) + throws EvaluationException { + return TypeDescriptor.valueOf(String.class); + } + + public boolean isWritable(EvaluationContext context, Object rootObject) throws EvaluationException { + return false; + } + + public void setValue(EvaluationContext context, Object rootObject, Object value) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call setValue on a composite expression"); + } + + public boolean isWritable(Object rootObject) throws EvaluationException { + return false; + } + + public void setValue(Object rootObject, Object value) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call setValue on a composite expression"); + } + } diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/common/LiteralExpression.java b/org.springframework.expression/src/main/java/org/springframework/expression/common/LiteralExpression.java index dda45b663df..d8ce5fc1d17 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/common/LiteralExpression.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/common/LiteralExpression.java @@ -51,6 +51,10 @@ public class LiteralExpression implements Expression { public String getValue(EvaluationContext context) { return this.literalValue; } + + public String getValue(Object rootObject) { + return this.literalValue; + } public Class getValueType(EvaluationContext context) { return String.class; @@ -86,4 +90,61 @@ public class LiteralExpression implements Expression { return String.class; } + + public T getValue(Object rootObject, Class desiredResultType) throws EvaluationException { + Object value = getValue(rootObject); + return ExpressionUtils.convert(null, value, desiredResultType); + } + + + public String getValue(EvaluationContext context, Object rootObject) throws EvaluationException { + return this.literalValue; + } + + + public T getValue(EvaluationContext context, Object rootObject, Class desiredResultType) + throws EvaluationException { + Object value = getValue(context, rootObject); + return ExpressionUtils.convert(null, value, desiredResultType); + } + + + public Class getValueType(Object rootObject) throws EvaluationException { + return String.class; + } + + + public Class getValueType(EvaluationContext context, Object rootObject) throws EvaluationException { + return String.class; + } + + + public TypeDescriptor getValueTypeDescriptor(Object rootObject) throws EvaluationException { + return TypeDescriptor.valueOf(String.class); + } + + + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, Object rootObject) + throws EvaluationException { + return TypeDescriptor.valueOf(String.class); + } + + + public boolean isWritable(EvaluationContext context, Object rootObject) throws EvaluationException { + return false; + } + + + public void setValue(EvaluationContext context, Object rootObject, Object value) throws EvaluationException { + throw new EvaluationException(literalValue, "Cannot call setValue() on a LiteralExpression"); + } + + public boolean isWritable(Object rootObject) throws EvaluationException { + return false; + } + + public void setValue(Object rootObject, Object value) throws EvaluationException { + throw new EvaluationException(literalValue, "Cannot call setValue() on a LiteralExpression"); + } + } diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ExpressionState.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ExpressionState.java index 62f365c3575..01de9f2a4d3 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ExpressionState.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ExpressionState.java @@ -46,60 +46,71 @@ public class ExpressionState { private final EvaluationContext relatedContext; - private final Stack variableScopes = new Stack(); + private Stack variableScopes; - private final Stack contextObjects = new Stack(); + private Stack contextObjects; + + private final TypedValue rootObject; private int configuration = 0; public ExpressionState(EvaluationContext context) { this.relatedContext = context; - createVariableScope(); + this.rootObject = context.getRootObject(); } public ExpressionState(EvaluationContext context, int configuration) { this.relatedContext = context; this.configuration = configuration; - createVariableScope(); + this.rootObject = context.getRootObject(); } - // create an empty top level VariableScope - private void createVariableScope() { - this.variableScopes.add(new VariableScope()); + public ExpressionState(EvaluationContext context, TypedValue rootObject) { + this.relatedContext = context; + this.rootObject = rootObject; + } + + public ExpressionState(EvaluationContext context, TypedValue rootObject, int configuration) { + this.relatedContext = context; + this.configuration = configuration; + this.rootObject = rootObject; + } + + private void ensureVariableScopesInitialized() { + if (variableScopes == null) { + this.variableScopes = new Stack(); + // top level empty variable scope + this.variableScopes.add(new VariableScope()); + } } /** * The active context object is what unqualified references to properties/etc are resolved against. */ public TypedValue getActiveContextObject() { - if (this.contextObjects.isEmpty()) { - TypedValue rootObject = this.relatedContext.getRootObject(); - if (rootObject == null) { - return TypedValue.NULL_TYPED_VALUE; - } - else { - return rootObject; - } + if (this.contextObjects==null || this.contextObjects.isEmpty()) { + return this.rootObject; } + return this.contextObjects.peek(); } public void pushActiveContextObject(TypedValue obj) { + if (this.contextObjects==null) { + this.contextObjects = new Stack(); + } this.contextObjects.push(obj); } public void popActiveContextObject() { + if (this.contextObjects==null) { + this.contextObjects = new Stack(); + } this.contextObjects.pop(); } public TypedValue getRootContextObject() { - TypedValue root = this.relatedContext.getRootObject(); - if (root == null) { - return TypedValue.NULL_TYPED_VALUE; - } - else { - return root; - } + return this.rootObject; } public void setVariable(String name, Object value) { @@ -137,22 +148,27 @@ public class ExpressionState { */ public void enterScope(Map argMap) { + ensureVariableScopesInitialized(); this.variableScopes.push(new VariableScope(argMap)); } public void enterScope(String name, Object value) { + ensureVariableScopesInitialized(); this.variableScopes.push(new VariableScope(name, value)); } public void exitScope() { + ensureVariableScopesInitialized(); this.variableScopes.pop(); } public void setLocalVariable(String name, Object value) { + ensureVariableScopesInitialized(); this.variableScopes.peek().setVariable(name, value); } public Object lookupLocalVariable(String name) { + ensureVariableScopesInitialized(); int scopeNumber = this.variableScopes.size() - 1; for (int i = scopeNumber; i >= 0; i--) { if (this.variableScopes.get(i).definesVariable(name)) { diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelExpression.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelExpression.java index b9237660e39..4c64d658abe 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelExpression.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelExpression.java @@ -20,6 +20,7 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; +import org.springframework.expression.TypedValue; import org.springframework.expression.common.ExpressionUtils; import org.springframework.expression.spel.ast.SpelNodeImpl; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -37,6 +38,9 @@ public class SpelExpression implements Expression { private final String expression; + // the default context is used if no override is supplied by the user + private transient EvaluationContext defaultContext; + public final SpelNodeImpl ast; public final int configuration; @@ -53,20 +57,36 @@ public class SpelExpression implements Expression { // implementing Expression public Object getValue() throws EvaluationException { - ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext(), configuration); + ExpressionState expressionState = new ExpressionState(getEvaluationContext(), configuration); return ast.getValue(expressionState); } - + + public Object getValue(Object rootObject) throws EvaluationException { + ExpressionState expressionState = new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), configuration); + return ast.getValue(expressionState); + } + public T getValue(Class expectedResultType) throws EvaluationException { - ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext(), configuration); + ExpressionState expressionState = new ExpressionState(getEvaluationContext(), configuration); Object result = ast.getValue(expressionState); return ExpressionUtils.convert(expressionState.getEvaluationContext(), result, expectedResultType); } - + + public T getValue(Object rootObject, Class expectedResultType) throws EvaluationException { + ExpressionState expressionState = new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), configuration); + Object result = ast.getValue(expressionState); + return ExpressionUtils.convert(expressionState.getEvaluationContext(), result, expectedResultType); + } + public Object getValue(EvaluationContext context) throws EvaluationException { Assert.notNull(context, "The EvaluationContext is required"); return ast.getValue(new ExpressionState(context, configuration)); } + + public Object getValue(EvaluationContext context, Object rootObject) throws EvaluationException { + Assert.notNull(context, "The EvaluationContext is required"); + return ast.getValue(new ExpressionState(context, toTypedValue(rootObject), configuration)); + } @SuppressWarnings("unchecked") public T getValue(EvaluationContext context, Class expectedResultType) throws EvaluationException { @@ -80,9 +100,23 @@ public class SpelExpression implements Expression { } return (T) result; } + + @SuppressWarnings("unchecked") + public T getValue(EvaluationContext context, Object rootObject, Class 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; + } + public Class getValueType() throws EvaluationException { - return ast.getValueInternal(new ExpressionState(new StandardEvaluationContext(), configuration)).getTypeDescriptor().getType(); + return ast.getValueInternal(new ExpressionState(getEvaluationContext(), configuration)).getTypeDescriptor().getType(); } public Class getValueType(EvaluationContext context) throws EvaluationException { @@ -92,8 +126,23 @@ public class SpelExpression implements Expression { return typeDescriptor.getType(); } + public Class getValueType(EvaluationContext context, Object rootObject) throws EvaluationException { + ExpressionState eState = new ExpressionState(context, toTypedValue(rootObject), configuration); + TypeDescriptor typeDescriptor = ast.getValueInternal(eState).getTypeDescriptor(); + return typeDescriptor.getType(); + } + + public Class getValueType(Object rootObject) throws EvaluationException { + return ast.getValueInternal(new ExpressionState(getEvaluationContext(), configuration)).getTypeDescriptor().getType(); + } + public TypeDescriptor getValueTypeDescriptor() throws EvaluationException { - return ast.getValueInternal(new ExpressionState(new StandardEvaluationContext(), configuration)).getTypeDescriptor(); + return ast.getValueInternal(new ExpressionState(getEvaluationContext(), configuration)).getTypeDescriptor(); + } + + public TypeDescriptor getValueTypeDescriptor(Object rootObject) throws EvaluationException { + ExpressionState eState = new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), configuration); + return ast.getValueInternal(eState).getTypeDescriptor(); } public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException { @@ -102,6 +151,12 @@ public class SpelExpression implements Expression { return ast.getValueInternal(eState).getTypeDescriptor(); } + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, Object rootObject) throws EvaluationException { + Assert.notNull(context, "The EvaluationContext is required"); + ExpressionState eState = new ExpressionState(context, toTypedValue(rootObject), configuration); + return ast.getValueInternal(eState).getTypeDescriptor(); + } + public String getExpressionString() { return expression; } @@ -111,10 +166,29 @@ public class SpelExpression implements Expression { return ast.isWritable(new ExpressionState(context, configuration)); } + public boolean isWritable(Object rootObject) throws EvaluationException { + return ast.isWritable(new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), configuration)); + } + + public boolean isWritable(EvaluationContext context, Object rootObject) throws EvaluationException { + Assert.notNull(context, "The EvaluationContext is required"); + return ast.isWritable(new ExpressionState(context, toTypedValue(rootObject), configuration)); + } + public void setValue(EvaluationContext context, Object value) throws EvaluationException { Assert.notNull(context, "The EvaluationContext is required"); ast.setValue(new ExpressionState(context, configuration), value); } + + public void setValue(Object rootObject, Object value) throws EvaluationException { + ast.setValue(new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), configuration), value); + } + + public void setValue(EvaluationContext context, Object rootObject, Object value) throws EvaluationException { + Assert.notNull(context, "The EvaluationContext is required"); + ast.setValue(new ExpressionState(context, toTypedValue(rootObject), configuration), value); + } + // impl only @@ -135,5 +209,33 @@ public class SpelExpression implements Expression { public String toStringAST() { return ast.toStringAST(); } + + /** + * Return the default evaluation context that will be used if none is supplied on an evaluation call + * @return the default evaluation context + */ + public EvaluationContext getEvaluationContext() { + if (defaultContext==null) { + defaultContext = new StandardEvaluationContext(); + } + return defaultContext; + } + + /** + * Set the evaluation context that will be used if none is specified on an evaluation call. + * + * @param context an evaluation context + */ + public void setEvaluationContext(EvaluationContext context) { + this.defaultContext = context; + } + + private TypedValue toTypedValue(Object object) { + if (object==null) { + return TypedValue.NULL_TYPED_VALUE; + } else { + return new TypedValue(object); + } + } } diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 4a2ce061086..5b0bfbfe307 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -206,7 +206,6 @@ public class Indexer extends SpelNodeImpl { return sb.toString(); } - @SuppressWarnings("unchecked") private void setArrayElement(ExpressionState state, Object ctx, int idx, Object newValue, Class clazz) throws EvaluationException { Class arrayComponentType = clazz; if (arrayComponentType == Integer.TYPE) { diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java index 500f22e25d4..3f7d71bc44e 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java @@ -52,7 +52,6 @@ public class MethodReference extends SpelNodeImpl { TypedValue currentContext = state.getActiveContextObject(); Object[] arguments = new Object[getChildCount()]; for (int i = 0; i < arguments.length; i++) { -// System.out.println(i); arguments[i] = children[i].getValueInternal(state).getValue(); } if (currentContext.getValue() == null) { diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Projection.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Projection.java index c4b0a3f54a1..bb8d4a56568 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Projection.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Projection.java @@ -45,7 +45,6 @@ public class Projection extends SpelNodeImpl { this.nullSafe = nullSafe; } - @SuppressWarnings("unchecked") @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { TypedValue op = state.getActiveContextObject(); diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java index fb22e4ddb7a..d37647c03dc 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java @@ -30,6 +30,7 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.ReflectivePropertyResolver; /** * Represents a simple property or field reference. @@ -57,9 +58,10 @@ public class PropertyOrFieldReference extends SpelNodeImpl { @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { TypedValue result = readProperty(state, this.name); - TypeDescriptor resultDescriptor = result.getTypeDescriptor(); + // Dynamically create the objects if the user has requested that optional behaviour if (result.getValue()==null && state.configuredToDynamicallyCreateNullObjects() && nextChildIs(Indexer.class,PropertyOrFieldReference.class)) { + TypeDescriptor resultDescriptor = result.getTypeDescriptor(); // Creating lists and maps if ((resultDescriptor.getType().equals(List.class) || resultDescriptor.getType().equals(Map.class))) { // Create a new collection or map ready for the indexer @@ -130,7 +132,6 @@ public class PropertyOrFieldReference extends SpelNodeImpl { */ private TypedValue readProperty(ExpressionState state, String name) throws EvaluationException { TypedValue contextObject = state.getActiveContextObject(); - EvaluationContext eContext = state.getEvaluationContext(); Object targetObject = contextObject.getValue(); if (targetObject == null && nullSafe) { @@ -151,6 +152,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl { Class contextObjectClass = getObjectClass(contextObject.getValue()); List accessorsToTry = getPropertyAccessorsToTry(contextObjectClass, state); + EvaluationContext eContext = state.getEvaluationContext(); // Go through the accessors that may be able to resolve it. If they are a cacheable accessor then // get the accessor and use it. If they are not cacheable but report they can read the property @@ -159,6 +161,9 @@ public class PropertyOrFieldReference extends SpelNodeImpl { try { for (PropertyAccessor accessor : accessorsToTry) { if (accessor.canRead(eContext, contextObject.getValue(), name)) { + if (accessor instanceof ReflectivePropertyResolver) { + accessor = ((ReflectivePropertyResolver)accessor).createOptimalAccessor(eContext, contextObject.getValue(), name); + } this.cachedReadAccessor = accessor; return accessor.read(eContext, contextObject.getValue(), name); } @@ -224,7 +229,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl { public boolean isWritableProperty(String name, ExpressionState state) throws SpelEvaluationException { Object contextObject = state.getActiveContextObject().getValue(); - TypeDescriptor td = state.getActiveContextObject().getTypeDescriptor(); +// TypeDescriptor td = state.getActiveContextObject().getTypeDescriptor(); EvaluationContext eContext = state.getEvaluationContext(); List resolversToTry = getPropertyAccessorsToTry(getObjectClass(contextObject),state); diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyResolver.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyResolver.java index b318d3fc34f..5ce31135f45 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyResolver.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyResolver.java @@ -44,11 +44,11 @@ import org.springframework.util.StringUtils; */ public class ReflectivePropertyResolver implements PropertyAccessor { - protected final Map readerCache = new ConcurrentHashMap(); + protected Map readerCache; - protected final Map writerCache = new ConcurrentHashMap(); + protected Map writerCache; - protected final Map typeDescriptorCache = new ConcurrentHashMap(); + protected Map typeDescriptorCache; /** * @return null which means this is a general purpose accessor @@ -65,21 +65,29 @@ public class ReflectivePropertyResolver implements PropertyAccessor { if ((type.isArray() && name.equals("length"))) { return true; } + if (this.readerCache==null) { + this.readerCache = new ConcurrentHashMap(); + if (this.typeDescriptorCache == null) { + this.typeDescriptorCache = new ConcurrentHashMap(); + } + } CacheKey cacheKey = new CacheKey(type, name); if (this.readerCache.containsKey(cacheKey)) { return true; } Method method = findGetterForProperty(name, type, target instanceof Class); if (method != null) { - this.readerCache.put(cacheKey, method); - this.typeDescriptorCache.put(cacheKey, new TypeDescriptor(new MethodParameter(method,-1))); + TypeDescriptor typeDescriptor = new TypeDescriptor(new MethodParameter(method,-1)); + this.readerCache.put(cacheKey, new InvokerPair(method,typeDescriptor)); + this.typeDescriptorCache.put(cacheKey, typeDescriptor); return true; } else { Field field = findField(name, type, target instanceof Class); if (field != null) { - this.readerCache.put(cacheKey, field); - this.typeDescriptorCache.put(cacheKey, new TypeDescriptor(field)); + TypeDescriptor typeDescriptor = new TypeDescriptor(field); + this.readerCache.put(cacheKey, new InvokerPair(field,typeDescriptor)); + this.typeDescriptorCache.put(cacheKey, typeDescriptor); return true; } } @@ -99,23 +107,25 @@ public class ReflectivePropertyResolver implements PropertyAccessor { return new TypedValue(Array.getLength(target),TypeDescriptor.valueOf(Integer.TYPE)); } + if (this.readerCache==null) { + this.readerCache = new ConcurrentHashMap(); + } CacheKey cacheKey = new CacheKey(type, name); - Member cachedMember = this.readerCache.get(cacheKey); + InvokerPair invoker = this.readerCache.get(cacheKey); - if (cachedMember == null || cachedMember instanceof Method) { - Method method = (Method) cachedMember; + if (invoker == null || invoker.member instanceof Method) { + Method method = (Method) (invoker==null?null:invoker.member); if (method == null) { method = findGetterForProperty(name, type, target instanceof Class); if (method != null) { - cachedMember = method; - this.readerCache.put(cacheKey, cachedMember); + invoker = new InvokerPair(method,new TypeDescriptor(new MethodParameter(method,-1))); + this.readerCache.put(cacheKey, invoker); } } if (method != null) { try { ReflectionUtils.makeAccessible(method); - TypeDescriptor resultTypeDescriptor = new TypeDescriptor(new MethodParameter(method,-1)); - return new TypedValue(method.invoke(target),resultTypeDescriptor); + return new TypedValue(method.invoke(target),invoker.typeDescriptor); } catch (Exception ex) { throw new AccessException("Unable to access property '" + name + "' through getter", ex); @@ -123,19 +133,19 @@ public class ReflectivePropertyResolver implements PropertyAccessor { } } - if (cachedMember == null || cachedMember instanceof Field) { - Field field = (Field) cachedMember; + if (invoker == null || invoker.member instanceof Field) { + Field field = (Field) (invoker==null?null:invoker.member); if (field == null) { field = findField(name, type, target instanceof Class); if (field != null) { - cachedMember = field; - this.readerCache.put(cacheKey, cachedMember); + invoker = new InvokerPair(field, new TypeDescriptor(field)); + this.readerCache.put(cacheKey, invoker); } } if (field != null) { try { ReflectionUtils.makeAccessible(field); - return new TypedValue(field.get(target),new TypeDescriptor(field)); + return new TypedValue(field.get(target),invoker.typeDescriptor); } catch (Exception ex) { throw new AccessException("Unable to access field: " + name, ex); @@ -151,6 +161,12 @@ public class ReflectivePropertyResolver implements PropertyAccessor { return false; } Class type = (target instanceof Class ? (Class) target : target.getClass()); + if (this.writerCache == null) { + this.writerCache = new ConcurrentHashMap(); + if (this.typeDescriptorCache == null) { + this.typeDescriptorCache = new ConcurrentHashMap(); + } + } CacheKey cacheKey = new CacheKey(type, name); if (this.writerCache.containsKey(cacheKey)) { return true; @@ -187,7 +203,9 @@ public class ReflectivePropertyResolver implements PropertyAccessor { throw new AccessException("Type conversion failure",evaluationException); } } - + if (this.writerCache == null) { + this.writerCache = new ConcurrentHashMap(); + } CacheKey cacheKey = new CacheKey(type, name); Member cachedMember = this.writerCache.get(cacheKey); @@ -218,7 +236,7 @@ public class ReflectivePropertyResolver implements PropertyAccessor { field = findField(name, type, target instanceof Class); if (field != null) { cachedMember = field; - this.readerCache.put(cacheKey, cachedMember); + this.writerCache.put(cacheKey, cachedMember); } } if (field != null) { @@ -315,7 +333,23 @@ public class ReflectivePropertyResolver implements PropertyAccessor { } return null; } + + /** + * Captures the member (method/field) to call reflectively to access a property value and the type descriptor for the + * value returned by the reflective call. + */ + private static class InvokerPair { + + final Member member; + + final TypeDescriptor typeDescriptor; + public InvokerPair(Member member, TypeDescriptor typeDescriptor) { + this.member = member; + this.typeDescriptor = typeDescriptor; + } + + } private static class CacheKey { @@ -346,4 +380,148 @@ public class ReflectivePropertyResolver implements PropertyAccessor { } } + /** + * Attempt to create an optimized property accessor tailored for a property of a particular name on + * a particular class. The general ReflectivePropertyAccessor will always work but is not optimal + * due to the need to lookup which reflective member (method/field) to use each time read() is called. + * This method will just return the ReflectivePropertyAccessor instance if it is unable to build + * something more optimal. + */ + public PropertyAccessor createOptimalAccessor(EvaluationContext eContext, Object target, String name) { + // Don't be clever for arrays or null target + if (target==null) { + return this; + } + Class type = (target instanceof Class ? (Class) target : target.getClass()); + if (type.isArray()) { + return this; + } + + CacheKey cacheKey = new CacheKey(type, name); + if (this.readerCache==null) { + this.readerCache = new ConcurrentHashMap(); + if (this.typeDescriptorCache == null) { + this.typeDescriptorCache = new ConcurrentHashMap(); + } + } + InvokerPair invocationTarget = this.readerCache.get(cacheKey); + + if (invocationTarget == null || invocationTarget.member instanceof Method) { + Method method = (Method) (invocationTarget==null?null:invocationTarget.member); + if (method == null) { + method = findGetterForProperty(name, type, target instanceof Class); + if (method != null) { + invocationTarget = new InvokerPair(method,new TypeDescriptor(new MethodParameter(method,-1))); + ReflectionUtils.makeAccessible(method); + this.readerCache.put(cacheKey, invocationTarget); + } + } + if (method != null) { + return new OptimalPropertyAccessor(invocationTarget); + } + } + + if (invocationTarget == null || invocationTarget.member instanceof Field) { + Field field = (Field) (invocationTarget==null?null:invocationTarget.member); + if (field == null) { + field = findField(name, type, target instanceof Class); + if (field != null) { + invocationTarget = new InvokerPair(field, new TypeDescriptor(field)); + ReflectionUtils.makeAccessible(field); + this.readerCache.put(cacheKey, invocationTarget); + } + } + if (field != null) { + return new OptimalPropertyAccessor(invocationTarget); + } + } + return this; + } + + /** + * An optimized form of a PropertyAccessor that will use reflection but only knows how to access a particular property + * on a particular class. This is unlike the general ReflectivePropertyResolver which manages a cache of methods/fields that + * may be invoked to access different properties on different classes. This optimal accessor exists because looking up + * the appropriate reflective object by class/name on each read is not cheap. + */ + static class OptimalPropertyAccessor implements PropertyAccessor { + private final Member member; + private final TypeDescriptor typeDescriptor; + private final boolean needsToBeMadeAccessible; + + OptimalPropertyAccessor(InvokerPair target) { + this.member = target.member; + this.typeDescriptor = target.typeDescriptor; + if (this.member instanceof Field) { + Field field = (Field)member; + needsToBeMadeAccessible = (!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) + && !field.isAccessible(); + } else { + Method method = (Method)member; + needsToBeMadeAccessible = ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) + && !method.isAccessible()); + } + } + + public Class[] getSpecificTargetClasses() { + throw new UnsupportedOperationException("Should not be called on an OptimalPropertyAccessor"); + } + + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + if (target == null) { + return false; + } + Class type = (target instanceof Class ? (Class) target : target.getClass()); + if (type.isArray()) { + return false; + } + if (member instanceof Method) { + Method method = (Method)member; + String getterName = "get" + StringUtils.capitalize(name); + if (getterName.equals(method.getName())) { + return true; + } + getterName = "is" + StringUtils.capitalize(name); + return getterName.equals(method.getName()); + } else { + Field field = (Field)member; + return field.getName().equals(name); + } + } + + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + if (member instanceof Method) { + try { + if (needsToBeMadeAccessible) { + ReflectionUtils.makeAccessible((Method)member); + } + return new TypedValue(((Method)member).invoke(target),typeDescriptor); + } catch (Exception e) { + throw new AccessException("Unable to access property '" + name + "' through getter", e); + } + } + if (member instanceof Field) { + try { + if (needsToBeMadeAccessible) { + ReflectionUtils.makeAccessible((Field)member); + } + return new TypedValue(((Field)member).get(target),typeDescriptor); + } + catch (Exception ex) { + throw new AccessException("Unable to access field: " + name, ex); + } + } + throw new AccessException("Neither getter nor field found for property '" + name + "'"); + } + + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + throw new UnsupportedOperationException("Should not be called on an OptimalPropertyAccessor"); + } + + public void write(EvaluationContext context, Object target, String name, Object newValue) + throws AccessException { + throw new UnsupportedOperationException("Should not be called on an OptimalPropertyAccessor"); + } + + } } diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java index a4318a07e31..0f9a2df2c4b 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java @@ -47,15 +47,15 @@ public class StandardEvaluationContext implements EvaluationContext { private TypedValue rootObject; - private final List constructorResolvers = new ArrayList(); + private List constructorResolvers; - private final List methodResolvers = new ArrayList(); + private List methodResolvers; - private final List propertyAccessors = new ArrayList(); + private List propertyAccessors; - private TypeLocator typeLocator = new StandardTypeLocator(); + private TypeLocator typeLocator; - private TypeConverter typeConverter = new StandardTypeConverter(); + private TypeConverter typeConverter; private TypeComparator typeComparator = new StandardTypeComparator(); @@ -65,9 +65,7 @@ public class StandardEvaluationContext implements EvaluationContext { public StandardEvaluationContext() { - this.methodResolvers.add(new ReflectiveMethodResolver()); - this.constructorResolvers.add(new ReflectiveConstructorResolver()); - this.propertyAccessors.add(new ReflectivePropertyResolver()); + setRootObject(null); } public StandardEvaluationContext(Object rootObject) { @@ -77,7 +75,11 @@ public class StandardEvaluationContext implements EvaluationContext { public void setRootObject(Object rootObject) { - this.rootObject = new TypedValue(rootObject, TypeDescriptor.forObject(rootObject)); + if (this.rootObject == null) { + this.rootObject = TypedValue.NULL_TYPED_VALUE; + } else { + this.rootObject = new TypedValue(rootObject);//, TypeDescriptor.forObject(rootObject)); + } } public void setRootObject(Object rootObject, TypeDescriptor typeDescriptor) { @@ -89,35 +91,65 @@ public class StandardEvaluationContext implements EvaluationContext { } public void addConstructorResolver(ConstructorResolver resolver) { + ensureConstructorResolversInitialized(); this.constructorResolvers.add(this.constructorResolvers.size() - 1, resolver); } - + public List getConstructorResolvers() { + ensureConstructorResolversInitialized(); return this.constructorResolvers; } + private void ensureConstructorResolversInitialized() { + if (this.constructorResolvers == null) { + this.constructorResolvers = new ArrayList(); + this.constructorResolvers.add(new ReflectiveConstructorResolver()); + } + } + public void addMethodResolver(MethodResolver resolver) { + ensureMethodResolversInitialized(); this.methodResolvers.add(this.methodResolvers.size() - 1, resolver); } public List getMethodResolvers() { + ensureMethodResolversInitialized(); return this.methodResolvers; } + + private void ensureMethodResolversInitialized() { + if (this.methodResolvers == null) { + this.methodResolvers = new ArrayList(); + this.methodResolvers.add(new ReflectiveMethodResolver()); + } + } public void addPropertyAccessor(PropertyAccessor accessor) { + ensurePropertyAccessorsInitialized(); this.propertyAccessors.add(this.propertyAccessors.size() - 1, accessor); } public List getPropertyAccessors() { + ensurePropertyAccessorsInitialized(); return this.propertyAccessors; } + private void ensurePropertyAccessorsInitialized() { + if (this.propertyAccessors == null) { + this.propertyAccessors = new ArrayList(); + this.propertyAccessors.add(new ReflectivePropertyResolver()); + } + } + public void setTypeLocator(TypeLocator typeLocator) { Assert.notNull(typeLocator, "TypeLocator must not be null"); this.typeLocator = typeLocator; } public TypeLocator getTypeLocator() { + if (this.typeLocator == null) { + this.typeLocator = new StandardTypeLocator(); + } return this.typeLocator; } @@ -127,6 +159,9 @@ public class StandardEvaluationContext implements EvaluationContext { } public TypeConverter getTypeConverter() { + if (this.typeConverter == null) { + this.typeConverter = new StandardTypeConverter(); + } return this.typeConverter; } diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java index ca19e048c59..ca7658f3cf5 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java @@ -16,6 +16,7 @@ package org.springframework.expression.spel; +import java.util.EmptyStackException; import java.util.HashMap; import java.util.Map; @@ -117,8 +118,10 @@ public class ExpressionStateTests extends ExpressionTestCase { ExpressionState state = getState(); Assert.assertEquals(Inventor.class,state.getRootContextObject().getValue().getClass()); + // although the root object is being set on the evaluation context, the value in the 'state' remains what it was when constructed ((StandardEvaluationContext) state.getEvaluationContext()).setRootObject(null); - Assert.assertEquals(null, state.getRootContextObject().getValue()); + Assert.assertEquals(Inventor.class,state.getRootContextObject().getValue().getClass()); + // Assert.assertEquals(null, state.getRootContextObject().getValue()); state = new ExpressionState(new StandardEvaluationContext()); Assert.assertEquals(TypedValue.NULL_TYPED_VALUE,state.getRootContextObject()); @@ -133,6 +136,13 @@ public class ExpressionStateTests extends ExpressionTestCase { ExpressionState state = getState(); Assert.assertEquals(state.getRootContextObject().getValue(),state.getActiveContextObject().getValue()); + try { + state.popActiveContextObject(); + Assert.fail("stack should be empty..."); + } catch (EmptyStackException ese) { + // success + } + state.pushActiveContextObject(new TypedValue(34)); Assert.assertEquals(34,state.getActiveContextObject().getValue()); @@ -168,6 +178,17 @@ public class ExpressionStateTests extends ExpressionTestCase { Assert.assertNull(state.lookupLocalVariable("goo")); } + @Test + public void testRootObjectConstructor() { + EvaluationContext ctx = getContext(); + // TypedValue root = ctx.getRootObject(); + // supplied should override root on context + ExpressionState state = new ExpressionState(ctx,new TypedValue("i am a string")); + TypedValue stateRoot = state.getRootContextObject(); + Assert.assertEquals(String.class,stateRoot.getTypeDescriptor().getType()); + Assert.assertEquals("i am a string",stateRoot.getValue()); + } + @Test public void testPopulatedNestedScopesMap() { ExpressionState state = getState(); @@ -257,5 +278,9 @@ public class ExpressionStateTests extends ExpressionTestCase { ExpressionState state = new ExpressionState(context); return state; } + + private EvaluationContext getContext() { + return TestScenarioCreator.getTestEvaluationContext(); + } } diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/HelperTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/HelperTests.java index 9dc22764d65..8f17abc9bfb 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/HelperTests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/HelperTests.java @@ -26,6 +26,7 @@ import org.junit.Test; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.ParseException; +import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ast.FormatHelper; import org.springframework.expression.spel.support.ReflectionHelper; @@ -307,6 +308,82 @@ public class HelperTests extends ExpressionTestCase { // Assert.assertEquals(0,rpr.read(ctx,t,"field3").getValue()); Assert.assertEquals(false,rpr.read(ctx,t,"property4").getValue()); Assert.assertTrue(rpr.canRead(ctx,t,"property4")); + + } + + @Test + public void testOptimalReflectivePropertyResolver() throws Exception { + ReflectivePropertyResolver rpr = new ReflectivePropertyResolver(); + Tester t = new Tester(); + t.setProperty("hello"); + EvaluationContext ctx = new StandardEvaluationContext(t); +// Assert.assertTrue(rpr.canRead(ctx, t, "property")); +// Assert.assertEquals("hello",rpr.read(ctx, t, "property").getValue()); +// Assert.assertEquals("hello",rpr.read(ctx, t, "property").getValue()); // cached accessor used + + PropertyAccessor optA = rpr.createOptimalAccessor(ctx, t, "property"); + Assert.assertTrue(optA.canRead(ctx, t, "property")); + Assert.assertFalse(optA.canRead(ctx, t, "property2")); + try { + optA.canWrite(ctx, t, "property"); + Assert.fail(); + } catch (UnsupportedOperationException uoe) { + // success + } + try { + optA.canWrite(ctx, t, "property2"); + Assert.fail(); + } catch (UnsupportedOperationException uoe) { + // success + } + Assert.assertEquals("hello",optA.read(ctx, t, "property").getValue()); + Assert.assertEquals("hello",optA.read(ctx, t, "property").getValue()); // cached accessor used + + try { + optA.getSpecificTargetClasses(); + Assert.fail(); + } catch (UnsupportedOperationException uoe) { + // success + } + try { + optA.write(ctx,t,"property",null); + Assert.fail(); + } catch (UnsupportedOperationException uoe) { + // success + } + + optA = rpr.createOptimalAccessor(ctx, t, "field"); + Assert.assertTrue(optA.canRead(ctx, t, "field")); + Assert.assertFalse(optA.canRead(ctx, t, "field2")); + try { + optA.canWrite(ctx, t, "field"); + Assert.fail(); + } catch (UnsupportedOperationException uoe) { + // success + } + try { + optA.canWrite(ctx, t, "field2"); + Assert.fail(); + } catch (UnsupportedOperationException uoe) { + // success + } + Assert.assertEquals(3,optA.read(ctx, t, "field").getValue()); + Assert.assertEquals(3,optA.read(ctx, t, "field").getValue()); // cached accessor used + + try { + optA.getSpecificTargetClasses(); + Assert.fail(); + } catch (UnsupportedOperationException uoe) { + // success + } + try { + optA.write(ctx,t,"field",null); + Assert.fail(); + } catch (UnsupportedOperationException uoe) { + // success + } + + } diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/LiteralExpressionTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/LiteralExpressionTests.java index caa9e13ce4f..7aff1e1ba2f 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/LiteralExpressionTests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/LiteralExpressionTests.java @@ -37,9 +37,17 @@ public class LiteralExpressionTests { EvaluationContext ctx = new StandardEvaluationContext(); checkString("somevalue", lEx.getValue(ctx)); checkString("somevalue", lEx.getValue(ctx, String.class)); + checkString("somevalue", lEx.getValue(new Rooty())); + checkString("somevalue", lEx.getValue(new Rooty(), String.class)); + checkString("somevalue", lEx.getValue(ctx, new Rooty())); + checkString("somevalue", lEx.getValue(ctx, new Rooty(),String.class)); Assert.assertEquals("somevalue", lEx.getExpressionString()); Assert.assertFalse(lEx.isWritable(new StandardEvaluationContext())); + Assert.assertFalse(lEx.isWritable(new Rooty())); + Assert.assertFalse(lEx.isWritable(new StandardEvaluationContext(), new Rooty())); } + + static class Rooty {} @Test public void testSetValue() { @@ -52,6 +60,24 @@ public class LiteralExpressionTests { // success, not allowed - whilst here, check the expression value in the exception Assert.assertEquals(ee.getExpressionString(), "somevalue"); } + try { + LiteralExpression lEx = new LiteralExpression("somevalue"); + lEx.setValue(new Rooty(), "flibble"); + Assert.fail("Should have got an exception that the value cannot be set"); + } + catch (EvaluationException ee) { + // success, not allowed - whilst here, check the expression value in the exception + Assert.assertEquals(ee.getExpressionString(), "somevalue"); + } + try { + LiteralExpression lEx = new LiteralExpression("somevalue"); + lEx.setValue(new StandardEvaluationContext(), new Rooty(), "flibble"); + Assert.fail("Should have got an exception that the value cannot be set"); + } + catch (EvaluationException ee) { + // success, not allowed - whilst here, check the expression value in the exception + Assert.assertEquals(ee.getExpressionString(), "somevalue"); + } } @Test @@ -59,6 +85,12 @@ public class LiteralExpressionTests { LiteralExpression lEx = new LiteralExpression("somevalue"); Assert.assertEquals(String.class, lEx.getValueType()); Assert.assertEquals(String.class, lEx.getValueType(new StandardEvaluationContext())); + Assert.assertEquals(String.class, lEx.getValueType(new Rooty())); + Assert.assertEquals(String.class, lEx.getValueType(new StandardEvaluationContext(), new Rooty())); + Assert.assertEquals(String.class, lEx.getValueTypeDescriptor().getType()); + Assert.assertEquals(String.class, lEx.getValueTypeDescriptor(new StandardEvaluationContext()).getType()); + Assert.assertEquals(String.class, lEx.getValueTypeDescriptor(new Rooty()).getType()); + Assert.assertEquals(String.class, lEx.getValueTypeDescriptor(new StandardEvaluationContext(), new Rooty()).getType()); } private void checkString(String expectedString, Object value) { diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java index 318a62882ab..0ce7fc58378 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java @@ -20,6 +20,7 @@ import junit.framework.Assert; import org.junit.Test; import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.ParseException; import org.springframework.expression.ParserContext; @@ -98,6 +99,10 @@ public class TemplateExpressionParsingTests extends ExpressionTestCase { expr = parser.parseExpression("abc", DEFAULT_TEMPLATE_PARSER_CONTEXT); o = expr.getValue(); Assert.assertEquals("abc", o.toString()); + + expr = parser.parseExpression("abc", DEFAULT_TEMPLATE_PARSER_CONTEXT); + o = expr.getValue((Object)null); + Assert.assertEquals("abc", o.toString()); } @Test @@ -106,13 +111,53 @@ public class TemplateExpressionParsingTests extends ExpressionTestCase { Expression ex = parser.parseExpression("hello ${'world'}", DEFAULT_TEMPLATE_PARSER_CONTEXT); checkString("hello world", ex.getValue()); checkString("hello world", ex.getValue(String.class)); + checkString("hello world", ex.getValue((Object)null, String.class)); + checkString("hello world", ex.getValue(new Rooty())); + checkString("hello world", ex.getValue(new Rooty(), String.class)); + EvaluationContext ctx = new StandardEvaluationContext(); checkString("hello world", ex.getValue(ctx)); checkString("hello world", ex.getValue(ctx, String.class)); + checkString("hello world", ex.getValue(ctx, null, String.class)); + checkString("hello world", ex.getValue(ctx, new Rooty())); + checkString("hello world", ex.getValue(ctx, new Rooty(), String.class)); + checkString("hello world", ex.getValue(ctx, new Rooty(), String.class)); Assert.assertEquals("hello ${'world'}", ex.getExpressionString()); Assert.assertFalse(ex.isWritable(new StandardEvaluationContext())); + Assert.assertFalse(ex.isWritable(new Rooty())); + Assert.assertFalse(ex.isWritable(new StandardEvaluationContext(), new Rooty())); + + Assert.assertEquals(String.class,ex.getValueType()); + Assert.assertEquals(String.class,ex.getValueType(ctx)); + Assert.assertEquals(String.class,ex.getValueTypeDescriptor().getType()); + Assert.assertEquals(String.class,ex.getValueTypeDescriptor(ctx).getType()); + Assert.assertEquals(String.class,ex.getValueType(new Rooty())); + Assert.assertEquals(String.class,ex.getValueType(ctx, new Rooty())); + Assert.assertEquals(String.class,ex.getValueTypeDescriptor(new Rooty()).getType()); + Assert.assertEquals(String.class,ex.getValueTypeDescriptor(ctx, new Rooty()).getType()); + + try { + ex.setValue(ctx, null); + Assert.fail(); + } catch (EvaluationException ee) { + // success + } + try { + ex.setValue((Object)null, null); + Assert.fail(); + } catch (EvaluationException ee) { + // success + } + try { + ex.setValue(ctx, null, null); + Assert.fail(); + } catch (EvaluationException ee) { + // success + } } + static class Rooty {} + @Test public void testNestedExpressions() throws Exception { SpelExpressionParser parser = new SpelExpressionParser(); @@ -204,6 +249,16 @@ public class TemplateExpressionParsingTests extends ExpressionTestCase { Assert.assertEquals("abc", tpc.getExpressionPrefix()); Assert.assertEquals("def", tpc.getExpressionSuffix()); Assert.assertTrue(tpc.isTemplate()); + + tpc = new TemplateParserContext(); + Assert.assertEquals("#{", tpc.getExpressionPrefix()); + Assert.assertEquals("}", tpc.getExpressionSuffix()); + Assert.assertTrue(tpc.isTemplate()); + + ParserContext pc = ParserContext.TEMPLATE_EXPRESSION; + Assert.assertEquals("#{", pc.getExpressionPrefix()); + Assert.assertEquals("}", pc.getExpressionSuffix()); + Assert.assertTrue(pc.isTemplate()); } // ---