Merge branch '6.1.x'
# Conflicts: # spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java
This commit is contained in:
commit
a698f66c3a
|
@ -153,4 +153,18 @@ public interface EvaluationContext {
|
|||
@Nullable
|
||||
Object lookupVariable(String name);
|
||||
|
||||
/**
|
||||
* Determine if assignment is enabled within expressions evaluated by this evaluation
|
||||
* context.
|
||||
* <p>If this method returns {@code false}, the assignment ({@code =}), increment
|
||||
* ({@code ++}), and decrement ({@code --}) operators are disabled.
|
||||
* <p>By default, this method returns {@code true}. Concrete implementations may override
|
||||
* this <em>default</em> method to disable assignment.
|
||||
* @return {@code true} if assignment is enabled; {@code false} otherwise
|
||||
* @since 5.3.38
|
||||
*/
|
||||
default boolean isAssignmentEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
|
@ -19,6 +19,8 @@ package org.springframework.expression.spel.ast;
|
|||
import org.springframework.expression.EvaluationException;
|
||||
import org.springframework.expression.TypedValue;
|
||||
import org.springframework.expression.spel.ExpressionState;
|
||||
import org.springframework.expression.spel.SpelEvaluationException;
|
||||
import org.springframework.expression.spel.SpelMessage;
|
||||
|
||||
/**
|
||||
* Represents assignment. An alternative to calling {@code setValue}
|
||||
|
@ -39,6 +41,9 @@ public class Assign extends SpelNodeImpl {
|
|||
|
||||
@Override
|
||||
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
|
||||
if (!state.getEvaluationContext().isAssignmentEnabled()) {
|
||||
throw new SpelEvaluationException(getStartPosition(), SpelMessage.NOT_ASSIGNABLE, toStringAST());
|
||||
}
|
||||
return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state));
|
||||
}
|
||||
|
||||
|
|
|
@ -53,6 +53,10 @@ public class OpDec extends Operator {
|
|||
|
||||
@Override
|
||||
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
|
||||
if (!state.getEvaluationContext().isAssignmentEnabled()) {
|
||||
throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_DECREMENTABLE, toStringAST());
|
||||
}
|
||||
|
||||
SpelNodeImpl operand = getLeftOperand();
|
||||
|
||||
// The operand is going to be read and then assigned to, we don't want to evaluate it twice.
|
||||
|
|
|
@ -53,6 +53,10 @@ public class OpInc extends Operator {
|
|||
|
||||
@Override
|
||||
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
|
||||
if (!state.getEvaluationContext().isAssignmentEnabled()) {
|
||||
throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_INCREMENTABLE, toStringAST());
|
||||
}
|
||||
|
||||
SpelNodeImpl operand = getLeftOperand();
|
||||
ValueRef valueRef = operand.getValueRef(state);
|
||||
|
||||
|
@ -106,7 +110,7 @@ public class OpInc extends Operator {
|
|||
}
|
||||
}
|
||||
|
||||
// set the name value
|
||||
// set the new value
|
||||
try {
|
||||
valueRef.setValue(newValue.getValue());
|
||||
}
|
||||
|
|
|
@ -51,25 +51,25 @@ import org.springframework.lang.Nullable;
|
|||
* SpEL language syntax, e.g. excluding references to Java types, constructors,
|
||||
* and bean references.
|
||||
*
|
||||
* <p>When creating a {@code SimpleEvaluationContext} you need to choose the
|
||||
* level of support that you need for property access in SpEL expressions:
|
||||
* <p>When creating a {@code SimpleEvaluationContext} you need to choose the level of
|
||||
* support that you need for data binding in SpEL expressions:
|
||||
* <ul>
|
||||
* <li>A custom {@code PropertyAccessor} (typically not reflection-based),
|
||||
* potentially combined with a {@link DataBindingPropertyAccessor}</li>
|
||||
* <li>Data binding properties for read-only access</li>
|
||||
* <li>Data binding properties for read and write</li>
|
||||
* <li>Data binding for read-only access</li>
|
||||
* <li>Data binding for read and write access</li>
|
||||
* <li>A custom {@code PropertyAccessor} (typically not reflection-based), potentially
|
||||
* combined with a {@link DataBindingPropertyAccessor}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()}
|
||||
* enables read access to properties via {@link DataBindingPropertyAccessor};
|
||||
* same for {@link SimpleEvaluationContext#forReadWriteDataBinding()} when
|
||||
* write access is needed as well. Alternatively, configure custom accessors
|
||||
* via {@link SimpleEvaluationContext#forPropertyAccessors}, and potentially
|
||||
* activate method resolution and/or a type converter through the builder.
|
||||
* <p>Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()} enables
|
||||
* read-only access to properties via {@link DataBindingPropertyAccessor}. Similarly,
|
||||
* {@link SimpleEvaluationContext#forReadWriteDataBinding()} enables read and write access
|
||||
* to properties. Alternatively, configure custom accessors via
|
||||
* {@link SimpleEvaluationContext#forPropertyAccessors} and potentially activate method
|
||||
* resolution and/or a type converter through the builder.
|
||||
*
|
||||
* <p>Note that {@code SimpleEvaluationContext} is typically not configured
|
||||
* with a default root object. Instead it is meant to be created once and
|
||||
* used repeatedly through {@code getValue} calls on a pre-compiled
|
||||
* used repeatedly through {@code getValue} calls on a predefined
|
||||
* {@link org.springframework.expression.Expression} with both an
|
||||
* {@code EvaluationContext} and a root object as arguments:
|
||||
* {@link org.springframework.expression.Expression#getValue(EvaluationContext, Object)}.
|
||||
|
@ -89,9 +89,9 @@ import org.springframework.lang.Nullable;
|
|||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 4.3.15
|
||||
* @see #forPropertyAccessors
|
||||
* @see #forReadOnlyDataBinding()
|
||||
* @see #forReadWriteDataBinding()
|
||||
* @see #forPropertyAccessors
|
||||
* @see StandardEvaluationContext
|
||||
* @see StandardTypeConverter
|
||||
* @see DataBindingPropertyAccessor
|
||||
|
@ -120,15 +120,19 @@ public final class SimpleEvaluationContext implements EvaluationContext {
|
|||
|
||||
private final Map<String, Object> variables = new HashMap<>();
|
||||
|
||||
private final boolean assignmentEnabled;
|
||||
|
||||
|
||||
private SimpleEvaluationContext(List<PropertyAccessor> propertyAccessors, List<IndexAccessor> indexAccessors,
|
||||
List<MethodResolver> resolvers, @Nullable TypeConverter converter, @Nullable TypedValue rootObject) {
|
||||
List<MethodResolver> resolvers, @Nullable TypeConverter converter, @Nullable TypedValue rootObject,
|
||||
boolean assignmentEnabled) {
|
||||
|
||||
this.propertyAccessors = propertyAccessors;
|
||||
this.indexAccessors = indexAccessors;
|
||||
this.methodResolvers = resolvers;
|
||||
this.typeConverter = (converter != null ? converter : new StandardTypeConverter());
|
||||
this.rootObject = (rootObject != null ? rootObject : TypedValue.NULL);
|
||||
this.assignmentEnabled = assignmentEnabled;
|
||||
}
|
||||
|
||||
|
||||
|
@ -257,15 +261,33 @@ public final class SimpleEvaluationContext implements EvaluationContext {
|
|||
return this.variables.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if assignment is enabled within expressions evaluated by this evaluation
|
||||
* context.
|
||||
* <p>If this method returns {@code false}, the assignment ({@code =}), increment
|
||||
* ({@code ++}), and decrement ({@code --}) operators are disabled.
|
||||
* @return {@code true} if assignment is enabled; {@code false} otherwise
|
||||
* @since 5.3.38
|
||||
* @see #forPropertyAccessors(PropertyAccessor...)
|
||||
* @see #forReadOnlyDataBinding()
|
||||
* @see #forReadWriteDataBinding()
|
||||
*/
|
||||
@Override
|
||||
public boolean isAssignmentEnabled() {
|
||||
return this.assignmentEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor}
|
||||
* delegates: typically a custom {@code PropertyAccessor} specific to a use case
|
||||
* (e.g. attribute resolution in a custom data structure), potentially combined with
|
||||
* a {@link DataBindingPropertyAccessor} if property dereferences are needed as well.
|
||||
* <p>Assignment is enabled within expressions evaluated by the context created via
|
||||
* this factory method.
|
||||
* @param accessors the accessor delegates to use
|
||||
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
|
||||
* @see DataBindingPropertyAccessor#forReadWriteAccess()
|
||||
* @see #isAssignmentEnabled()
|
||||
*/
|
||||
public static Builder forPropertyAccessors(PropertyAccessor... accessors) {
|
||||
for (PropertyAccessor accessor : accessors) {
|
||||
|
@ -274,27 +296,33 @@ public final class SimpleEvaluationContext implements EvaluationContext {
|
|||
"ReflectivePropertyAccessor. Consider using DataBindingPropertyAccessor or a custom subclass.");
|
||||
}
|
||||
}
|
||||
return new Builder(accessors);
|
||||
return new Builder(true, accessors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code SimpleEvaluationContext} for read-only access to
|
||||
* public properties via {@link DataBindingPropertyAccessor}.
|
||||
* <p>Assignment is disabled within expressions evaluated by the context created via
|
||||
* this factory method.
|
||||
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
|
||||
* @see #forPropertyAccessors
|
||||
* @see #isAssignmentEnabled()
|
||||
*/
|
||||
public static Builder forReadOnlyDataBinding() {
|
||||
return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess());
|
||||
return new Builder(false, DataBindingPropertyAccessor.forReadOnlyAccess());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code SimpleEvaluationContext} for read-write access to
|
||||
* public properties via {@link DataBindingPropertyAccessor}.
|
||||
* <p>Assignment is enabled within expressions evaluated by the context created via
|
||||
* this factory method.
|
||||
* @see DataBindingPropertyAccessor#forReadWriteAccess()
|
||||
* @see #forPropertyAccessors
|
||||
* @see #isAssignmentEnabled()
|
||||
*/
|
||||
public static Builder forReadWriteDataBinding() {
|
||||
return new Builder(DataBindingPropertyAccessor.forReadWriteAccess());
|
||||
return new Builder(true, DataBindingPropertyAccessor.forReadWriteAccess());
|
||||
}
|
||||
|
||||
|
||||
|
@ -315,8 +343,11 @@ public final class SimpleEvaluationContext implements EvaluationContext {
|
|||
@Nullable
|
||||
private TypedValue rootObject;
|
||||
|
||||
private final boolean assignmentEnabled;
|
||||
|
||||
private Builder(PropertyAccessor... accessors) {
|
||||
|
||||
private Builder(boolean assignmentEnabled, PropertyAccessor... accessors) {
|
||||
this.assignmentEnabled = assignmentEnabled;
|
||||
this.propertyAccessors = Arrays.asList(accessors);
|
||||
}
|
||||
|
||||
|
@ -410,8 +441,9 @@ public final class SimpleEvaluationContext implements EvaluationContext {
|
|||
|
||||
public SimpleEvaluationContext build() {
|
||||
return new SimpleEvaluationContext(this.propertyAccessors, this.indexAccessors,
|
||||
this.resolvers, this.typeConverter, this.rootObject);
|
||||
this.resolvers, this.typeConverter, this.rootObject, this.assignmentEnabled);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ import org.springframework.util.Assert;
|
|||
* @author Andy Clement
|
||||
* @since 4.1
|
||||
*/
|
||||
class CompilableMapAccessor implements CompilablePropertyAccessor {
|
||||
public class CompilableMapAccessor implements CompilablePropertyAccessor {
|
||||
|
||||
private final boolean allowWrite;
|
||||
|
||||
|
|
|
@ -188,11 +188,11 @@ class PropertyAccessTests extends AbstractExpressionTests {
|
|||
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target))
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE);
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE);
|
||||
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("['name']='p4'").getValue(context, target))
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE);
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -207,7 +207,7 @@ class PropertyAccessTests extends AbstractExpressionTests {
|
|||
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target2))
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE);
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,477 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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
|
||||
*
|
||||
* https://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.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.api.ThrowableTypeAssert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.CompilableMapAccessor;
|
||||
import org.springframework.expression.spel.SpelEvaluationException;
|
||||
import org.springframework.expression.spel.SpelMessage;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
|
||||
/**
|
||||
* Tests for {@link SimpleEvaluationContext}.
|
||||
*
|
||||
* <p>Some of the use cases in this test class are duplicated elsewhere within the test
|
||||
* suite; however, we include them here to consistently focus on related features in this
|
||||
* test class.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
class SimpleEvaluationContextTests {
|
||||
|
||||
private final SpelExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
private final Model model = new Model();
|
||||
|
||||
|
||||
@Test
|
||||
void forReadWriteDataBinding() {
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
|
||||
|
||||
assertReadWriteMode(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
void forReadOnlyDataBinding() {
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
|
||||
assertCommonReadOnlyModeBehavior(context);
|
||||
|
||||
// WRITE -- via assignment operator
|
||||
|
||||
// Variable
|
||||
assertAssignmentDisabled(context, "#myVar = 'rejected'");
|
||||
|
||||
// Property
|
||||
assertAssignmentDisabled(context, "name = 'rejected'");
|
||||
assertIncrementDisabled(context, "count++");
|
||||
assertIncrementDisabled(context, "++count");
|
||||
assertDecrementDisabled(context, "count--");
|
||||
assertDecrementDisabled(context, "--count");
|
||||
|
||||
// Array Index
|
||||
assertAssignmentDisabled(context, "array[0] = 'rejected'");
|
||||
assertIncrementDisabled(context, "numbers[0]++");
|
||||
assertIncrementDisabled(context, "++numbers[0]");
|
||||
assertDecrementDisabled(context, "numbers[0]--");
|
||||
assertDecrementDisabled(context, "--numbers[0]");
|
||||
|
||||
// List Index
|
||||
assertAssignmentDisabled(context, "list[0] = 'rejected'");
|
||||
|
||||
// Map Index -- key as String
|
||||
assertAssignmentDisabled(context, "map['red'] = 'rejected'");
|
||||
|
||||
// Map Index -- key as pseudo property name
|
||||
assertAssignmentDisabled(context, "map[yellow] = 'rejected'");
|
||||
|
||||
// String Index
|
||||
assertAssignmentDisabled(context, "name[0] = 'rejected'");
|
||||
|
||||
// Object Index
|
||||
assertAssignmentDisabled(context, "['name'] = 'rejected'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forPropertyAccessorsInReadWriteMode() {
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext
|
||||
.forPropertyAccessors(new CompilableMapAccessor(), DataBindingPropertyAccessor.forReadWriteAccess())
|
||||
.build();
|
||||
|
||||
assertReadWriteMode(context);
|
||||
|
||||
// Map -- with key as property name supported by CompilableMapAccessor
|
||||
|
||||
Expression expression;
|
||||
expression = parser.parseExpression("map.yellow");
|
||||
expression.setValue(context, model, "pineapple");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("pineapple");
|
||||
|
||||
expression = parser.parseExpression("map.yellow = 'banana'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana");
|
||||
expression = parser.parseExpression("map.yellow");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana");
|
||||
}
|
||||
|
||||
/**
|
||||
* We call this "mixed" read-only mode, because write access via PropertyAccessors is
|
||||
* disabled, but write access via the Indexer is not disabled.
|
||||
*/
|
||||
@Test
|
||||
void forPropertyAccessorsInMixedReadOnlyMode() {
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext
|
||||
.forPropertyAccessors(new CompilableMapAccessor(), DataBindingPropertyAccessor.forReadOnlyAccess())
|
||||
.build();
|
||||
|
||||
assertCommonReadOnlyModeBehavior(context);
|
||||
|
||||
// Map -- with key as property name supported by CompilableMapAccessor
|
||||
|
||||
Expression expression;
|
||||
expression = parser.parseExpression("map.yellow");
|
||||
expression.setValue(context, model, "pineapple");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("pineapple");
|
||||
|
||||
expression = parser.parseExpression("map.yellow = 'banana'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana");
|
||||
expression = parser.parseExpression("map.yellow");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana");
|
||||
|
||||
// WRITE -- via assignment operator
|
||||
|
||||
// Variable
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("#myVar = 'rejected'").getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED));
|
||||
|
||||
// Property
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("name = 'rejected'").getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE));
|
||||
|
||||
// Array Index
|
||||
parser.parseExpression("array[0]").setValue(context, model, "foo");
|
||||
assertThat(model.array).containsExactly("foo");
|
||||
|
||||
// List Index
|
||||
parser.parseExpression("list[0]").setValue(context, model, "cat");
|
||||
assertThat(model.list).containsExactly("cat");
|
||||
|
||||
// Map Index -- key as String
|
||||
parser.parseExpression("map['red']").setValue(context, model, "cherry");
|
||||
assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "banana"));
|
||||
|
||||
// Map Index -- key as pseudo property name
|
||||
parser.parseExpression("map[yellow]").setValue(context, model, "lemon");
|
||||
assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "lemon"));
|
||||
|
||||
// String Index
|
||||
// The Indexer does not support writes when indexing into a String.
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("name[0] = 'rejected'").getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE));
|
||||
|
||||
// Object Index
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("['name'] = 'rejected'").getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE));
|
||||
|
||||
// WRITE -- via increment and decrement operators
|
||||
|
||||
assertIncrementAndDecrementWritesForIndexedStructures(context);
|
||||
}
|
||||
|
||||
|
||||
private void assertReadWriteMode(SimpleEvaluationContext context) {
|
||||
// Variables can always be set programmatically within an EvaluationContext.
|
||||
context.setVariable("myVar", "enigma");
|
||||
|
||||
// WRITE -- via setValue()
|
||||
|
||||
// Property
|
||||
parser.parseExpression("name").setValue(context, model, "test");
|
||||
assertThat(model.name).isEqualTo("test");
|
||||
parser.parseExpression("count").setValue(context, model, 42);
|
||||
assertThat(model.count).isEqualTo(42);
|
||||
|
||||
// Array Index
|
||||
parser.parseExpression("array[0]").setValue(context, model, "foo");
|
||||
assertThat(model.array).containsExactly("foo");
|
||||
|
||||
// List Index
|
||||
parser.parseExpression("list[0]").setValue(context, model, "cat");
|
||||
assertThat(model.list).containsExactly("cat");
|
||||
|
||||
// Map Index -- key as String
|
||||
parser.parseExpression("map['red']").setValue(context, model, "cherry");
|
||||
assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "replace me"));
|
||||
|
||||
// Map Index -- key as pseudo property name
|
||||
parser.parseExpression("map[yellow]").setValue(context, model, "lemon");
|
||||
assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "lemon"));
|
||||
|
||||
// READ
|
||||
assertReadAccess(context);
|
||||
|
||||
// WRITE -- via assignment operator
|
||||
|
||||
// Variable assignment is always disabled in a SimpleEvaluationContext.
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("#myVar = 'rejected'").getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED));
|
||||
|
||||
Expression expression;
|
||||
|
||||
// Property
|
||||
expression = parser.parseExpression("name = 'changed'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("changed");
|
||||
expression = parser.parseExpression("name");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("changed");
|
||||
|
||||
// Array Index
|
||||
expression = parser.parseExpression("array[0] = 'bar'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("bar");
|
||||
expression = parser.parseExpression("array[0]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("bar");
|
||||
|
||||
// List Index
|
||||
expression = parser.parseExpression("list[0] = 'dog'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("dog");
|
||||
expression = parser.parseExpression("list[0]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("dog");
|
||||
|
||||
// Map Index -- key as String
|
||||
expression = parser.parseExpression("map['red'] = 'strawberry'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("strawberry");
|
||||
expression = parser.parseExpression("map['red']");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("strawberry");
|
||||
|
||||
// Map Index -- key as pseudo property name
|
||||
expression = parser.parseExpression("map[yellow] = 'banana'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana");
|
||||
expression = parser.parseExpression("map[yellow]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana");
|
||||
|
||||
// String Index
|
||||
// The Indexer does not support writes when indexing into a String.
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("name[0] = 'rejected'").getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE));
|
||||
|
||||
// Object Index
|
||||
expression = parser.parseExpression("['name'] = 'new name'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("new name");
|
||||
expression = parser.parseExpression("['name']");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("new name");
|
||||
|
||||
// WRITE -- via increment and decrement operators
|
||||
|
||||
assertIncrementAndDecrementWritesForProperties(context);
|
||||
assertIncrementAndDecrementWritesForIndexedStructures(context);
|
||||
}
|
||||
|
||||
private void assertCommonReadOnlyModeBehavior(SimpleEvaluationContext context) {
|
||||
// Variables can always be set programmatically within an EvaluationContext.
|
||||
context.setVariable("myVar", "enigma");
|
||||
|
||||
// WRITE -- via setValue()
|
||||
|
||||
// Note: forReadOnlyDataBinding() disables programmatic writes via setValue() for
|
||||
// properties but allows programmatic writes via setValue() for indexed structures.
|
||||
|
||||
// Property
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("name").setValue(context, model, "test"))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE));
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("count").setValue(context, model, 42))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE));
|
||||
|
||||
// Array Index
|
||||
parser.parseExpression("array[0]").setValue(context, model, "foo");
|
||||
assertThat(model.array).containsExactly("foo");
|
||||
|
||||
// List Index
|
||||
parser.parseExpression("list[0]").setValue(context, model, "cat");
|
||||
assertThat(model.list).containsExactly("cat");
|
||||
|
||||
// Map Index -- key as String
|
||||
parser.parseExpression("map['red']").setValue(context, model, "cherry");
|
||||
assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "replace me"));
|
||||
|
||||
// Map Index -- key as pseudo property name
|
||||
parser.parseExpression("map[yellow]").setValue(context, model, "lemon");
|
||||
assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "lemon"));
|
||||
|
||||
// Since the setValue() attempts for "name" and "count" failed above, we have to set
|
||||
// them directly for assertReadAccess().
|
||||
model.name = "test";
|
||||
model.count = 42;
|
||||
|
||||
// READ
|
||||
assertReadAccess(context);
|
||||
}
|
||||
|
||||
private void assertReadAccess(SimpleEvaluationContext context) {
|
||||
Expression expression;
|
||||
|
||||
// Variable
|
||||
expression = parser.parseExpression("#myVar");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("enigma");
|
||||
|
||||
// Property
|
||||
expression = parser.parseExpression("name");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("test");
|
||||
expression = parser.parseExpression("count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42);
|
||||
|
||||
// Array Index
|
||||
expression = parser.parseExpression("array[0]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("foo");
|
||||
|
||||
// List Index
|
||||
expression = parser.parseExpression("list[0]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("cat");
|
||||
|
||||
// Map Index -- key as String
|
||||
expression = parser.parseExpression("map['red']");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("cherry");
|
||||
|
||||
// Map Index -- key as pseudo property name
|
||||
expression = parser.parseExpression("map[yellow]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("lemon");
|
||||
|
||||
// String Index
|
||||
expression = parser.parseExpression("name[0]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("t");
|
||||
|
||||
// Object Index
|
||||
expression = parser.parseExpression("['name']");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("test");
|
||||
}
|
||||
|
||||
private void assertIncrementAndDecrementWritesForProperties(SimpleEvaluationContext context) {
|
||||
Expression expression;
|
||||
expression = parser.parseExpression("count++");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42);
|
||||
expression = parser.parseExpression("count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(43);
|
||||
|
||||
expression = parser.parseExpression("++count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(44);
|
||||
expression = parser.parseExpression("count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(44);
|
||||
|
||||
expression = parser.parseExpression("count--");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(44);
|
||||
expression = parser.parseExpression("count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(43);
|
||||
|
||||
expression = parser.parseExpression("--count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42);
|
||||
expression = parser.parseExpression("count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42);
|
||||
}
|
||||
|
||||
private void assertIncrementAndDecrementWritesForIndexedStructures(SimpleEvaluationContext context) {
|
||||
Expression expression;
|
||||
expression = parser.parseExpression("numbers[0]++");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(99);
|
||||
expression = parser.parseExpression("numbers[0]");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(100);
|
||||
|
||||
expression = parser.parseExpression("++numbers[0]");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(101);
|
||||
expression = parser.parseExpression("numbers[0]");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(101);
|
||||
|
||||
expression = parser.parseExpression("numbers[0]--");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(101);
|
||||
expression = parser.parseExpression("numbers[0]");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(100);
|
||||
|
||||
expression = parser.parseExpression("--numbers[0]");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(99);
|
||||
expression = parser.parseExpression("numbers[0]");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(99);
|
||||
}
|
||||
|
||||
private ThrowableTypeAssert<SpelEvaluationException> assertThatSpelEvaluationException() {
|
||||
return assertThatExceptionOfType(SpelEvaluationException.class);
|
||||
}
|
||||
|
||||
private void assertAssignmentDisabled(SimpleEvaluationContext context, String expression) {
|
||||
assertEvaluationException(context, expression, SpelMessage.NOT_ASSIGNABLE);
|
||||
}
|
||||
|
||||
private void assertIncrementDisabled(SimpleEvaluationContext context, String expression) {
|
||||
assertEvaluationException(context, expression, SpelMessage.OPERAND_NOT_INCREMENTABLE);
|
||||
}
|
||||
|
||||
private void assertDecrementDisabled(SimpleEvaluationContext context, String expression) {
|
||||
assertEvaluationException(context, expression, SpelMessage.OPERAND_NOT_DECREMENTABLE);
|
||||
}
|
||||
|
||||
private void assertEvaluationException(SimpleEvaluationContext context, String expression, SpelMessage spelMessage) {
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression(expression).getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(spelMessage));
|
||||
}
|
||||
|
||||
|
||||
static class Model {
|
||||
|
||||
private String name = "replace me";
|
||||
private int count = 0;
|
||||
private final String[] array = {"replace me"};
|
||||
private final int[] numbers = {99};
|
||||
private final List<String> list = new ArrayList<>();
|
||||
private final Map<String, String> map = new HashMap<>();
|
||||
|
||||
Model() {
|
||||
this.list.add("replace me");
|
||||
this.map.put("red", "replace me");
|
||||
this.map.put("yellow", "replace me");
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
public void setCount(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public String[] getArray() {
|
||||
return this.array;
|
||||
}
|
||||
|
||||
public int[] getNumbers() {
|
||||
return this.numbers;
|
||||
}
|
||||
|
||||
public List<String> getList() {
|
||||
return this.list;
|
||||
}
|
||||
|
||||
public Map<String, String> getMap() {
|
||||
return this.map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue