diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java index fa379a47d5..265588a61a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java @@ -23,9 +23,11 @@ import java.util.List; import java.util.Map; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.BeanResolver; import org.springframework.expression.ConstructorResolver; import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; import org.springframework.expression.MethodResolver; import org.springframework.expression.OperatorOverloader; import org.springframework.expression.PropertyAccessor; @@ -90,6 +92,8 @@ public class SimpleEvaluationContext implements EvaluationContext { }; + private final TypedValue rootObject; + private final List propertyAccessors; private final List methodResolvers; @@ -104,24 +108,21 @@ public class SimpleEvaluationContext implements EvaluationContext { private SimpleEvaluationContext(List accessors, List resolvers, - @Nullable TypeConverter converter) { + @Nullable TypeConverter converter, @Nullable TypedValue rootObject) { this.propertyAccessors = accessors; this.methodResolvers = resolvers; this.typeConverter = (converter != null ? converter : new StandardTypeConverter()); + this.rootObject = (rootObject != null ? rootObject : TypedValue.NULL); } /** - * {@code SimpleEvaluationContext} cannot be configured with a root object. - * It is meant for repeated use with - * {@link org.springframework.expression.Expression Expression} method - * variants that accept both an {@code EvaluationContext} and a root object. - * @return Always returns {@link TypedValue#NULL}. + * Return the specified root object, if any. */ @Override public TypedValue getRootObject() { - return TypedValue.NULL; + return this.rootObject; } /** @@ -262,6 +263,9 @@ public class SimpleEvaluationContext implements EvaluationContext { @Nullable private TypeConverter typeConverter; + @Nullable + private TypedValue rootObject; + public Builder(PropertyAccessor... accessors) { this.accessors = Arrays.asList(accessors); } @@ -297,6 +301,18 @@ public class SimpleEvaluationContext implements EvaluationContext { return this; } + + /** + * Register a custom {@link ConversionService}. + *

By default a {@link StandardTypeConverter} backed by a + * {@link org.springframework.core.convert.support.DefaultConversionService} is used. + * @see #withTypeConverter + * @see StandardTypeConverter#StandardTypeConverter(ConversionService) + */ + public Builder withConversionService(ConversionService conversionService) { + this.typeConverter = new StandardTypeConverter(conversionService); + return this; + } /** * Register a custom {@link TypeConverter}. *

By default a {@link StandardTypeConverter} backed by a @@ -310,19 +326,29 @@ public class SimpleEvaluationContext implements EvaluationContext { } /** - * Register a custom {@link ConversionService}. - *

By default a {@link StandardTypeConverter} backed by a - * {@link org.springframework.core.convert.support.DefaultConversionService} is used. - * @see #withTypeConverter - * @see StandardTypeConverter#StandardTypeConverter(ConversionService) + * Specify a default root object to resolve against. + *

Default is none, expecting an object argument at evaluation time. + * @see Expression#getValue(EvaluationContext) + * @see Expression#getValue(EvaluationContext, Object) */ - public Builder withConversionService(ConversionService conversionService) { - this.typeConverter = new StandardTypeConverter(conversionService); + public Builder withRootObject(Object rootObject) { + this.rootObject = new TypedValue(rootObject); + return this; + } + + /** + * Specify a typed root object to resolve against. + *

Default is none, expecting an object argument at evaluation time. + * @see Expression#getValue(EvaluationContext) + * @see Expression#getValue(EvaluationContext, Object) + */ + public Builder withTypedRootObject(Object rootObject, TypeDescriptor typeDescriptor) { + this.rootObject = new TypedValue(rootObject, typeDescriptor); return this; } public SimpleEvaluationContext build() { - return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter); + return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject); } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java index 26bd762861..21141276d1 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java @@ -234,10 +234,29 @@ public class PropertyAccessTests extends AbstractExpressionTests { assertEquals("p4", expr.getValue(context, target)); } + @Test + public void propertyReadWriteWithRootObject() { + Person target = new Person("p1"); + EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().withRootObject(target).build(); + assertSame(target, context.getRootObject().getValue()); + + Expression expr = parser.parseExpression("name"); + assertEquals("p1", expr.getValue(context, target)); + target.setName("p2"); + assertEquals("p2", expr.getValue(context, target)); + + parser.parseExpression("name='p3'").getValue(context, target); + assertEquals("p3", target.getName()); + assertEquals("p3", expr.getValue(context, target)); + + expr.setValue(context, target, "p4"); + assertEquals("p4", target.getName()); + assertEquals("p4", expr.getValue(context, target)); + } + @Test public void propertyAccessWithoutMethodResolver() { EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); - Person target = new Person("p1"); try { parser.parseExpression("name.substring(1)").getValue(context, target); @@ -251,11 +270,21 @@ public class PropertyAccessTests extends AbstractExpressionTests { @Test public void propertyAccessWithInstanceMethodResolver() { EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().withInstanceMethods().build(); - Person target = new Person("p1"); assertEquals("1", parser.parseExpression("name.substring(1)").getValue(context, target)); } + @Test + public void propertyAccessWithInstanceMethodResolverAndTypedRootObject() { + Person target = new Person("p1"); + EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding(). + withInstanceMethods().withTypedRootObject(target, TypeDescriptor.valueOf(Object.class)).build(); + + assertEquals("1", parser.parseExpression("name.substring(1)").getValue(context, target)); + assertSame(target, context.getRootObject().getValue()); + assertSame(Object.class, context.getRootObject().getTypeDescriptor().getType()); + } + // This can resolve the property 'flibbles' on any String (very useful...) private static class StringyPropertyAccessor implements PropertyAccessor {