Browse Source
SimpleEvaluationContext.forReadOnlyDataBinding() documents that it
creates a SimpleEvaluationContext for read-only access to public
properties; however, prior to this commit write access was not disabled
for indexed structures when using the assignment operator, the
increment operator, or the decrement operator.
In order to better align with the documented contract for
forReadOnlyDataBinding(), this commit makes it possible to disable
assignment in general in order to enforce read-only semantics for
SpEL's SimpleEvaluationContext when created via the
forReadOnlyDataBinding() factory method. Specifically:
- This commit introduces a new isAssignmentEnabled() "default" method
in the EvaluationContext API, which returns true by default.
- SimpleEvaluationContext overrides isAssignmentEnabled(), returning
false if the context was created via the forReadOnlyDataBinding()
factory method.
- The Assign, OpDec, and OpInc AST nodes -- representing the assignment
(=), increment (++), and decrement (--) operators, respectively --
now throw a SpelEvaluationException if assignment is disabled for the
current EvaluationContext.
See gh-33319
Closes gh-33321
(cherry picked from commit 0127de5a7a)
6.0.x
9 changed files with 685 additions and 109 deletions
@ -0,0 +1,117 @@
@@ -0,0 +1,117 @@
|
||||
/* |
||||
* 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; |
||||
|
||||
import java.util.Map; |
||||
|
||||
import org.springframework.asm.MethodVisitor; |
||||
import org.springframework.expression.AccessException; |
||||
import org.springframework.expression.EvaluationContext; |
||||
import org.springframework.expression.TypedValue; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* This is a local COPY of {@link org.springframework.context.expression.MapAccessor}. |
||||
* |
||||
* @author Juergen Hoeller |
||||
* @author Andy Clement |
||||
* @since 4.1 |
||||
*/ |
||||
public class CompilableMapAccessor implements CompilablePropertyAccessor { |
||||
|
||||
@Override |
||||
public Class<?>[] getSpecificTargetClasses() { |
||||
return new Class<?>[] {Map.class}; |
||||
} |
||||
|
||||
@Override |
||||
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { |
||||
return (target instanceof Map<?, ?> map && map.containsKey(name)); |
||||
} |
||||
|
||||
@Override |
||||
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { |
||||
Assert.state(target instanceof Map, "Target must be of type Map"); |
||||
Map<?, ?> map = (Map<?, ?>) target; |
||||
Object value = map.get(name); |
||||
if (value == null && !map.containsKey(name)) { |
||||
throw new MapAccessException(name); |
||||
} |
||||
return new TypedValue(value); |
||||
} |
||||
|
||||
@Override |
||||
public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) |
||||
throws AccessException { |
||||
|
||||
Assert.state(target instanceof Map, "Target must be a Map"); |
||||
Map<Object, Object> map = (Map<Object, Object>) target; |
||||
map.put(name, newValue); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isCompilable() { |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public Class<?> getPropertyType() { |
||||
return Object.class; |
||||
} |
||||
|
||||
@Override |
||||
public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { |
||||
String descriptor = cf.lastDescriptor(); |
||||
if (descriptor == null || !descriptor.equals("Ljava/util/Map")) { |
||||
if (descriptor == null) { |
||||
cf.loadTarget(mv); |
||||
} |
||||
CodeFlow.insertCheckCast(mv, "Ljava/util/Map"); |
||||
} |
||||
mv.visitLdcInsn(propertyName); |
||||
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Exception thrown from {@code read} in order to reset a cached |
||||
* PropertyAccessor, allowing other accessors to have a try. |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
private static class MapAccessException extends AccessException { |
||||
|
||||
private final String key; |
||||
|
||||
public MapAccessException(String key) { |
||||
super(""); |
||||
this.key = key; |
||||
} |
||||
|
||||
@Override |
||||
public String getMessage() { |
||||
return "Map does not contain a value for key '" + this.key + "'"; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,477 @@
@@ -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