From be17c8d85f2b6b436a612e12ba05e8e1ebc0891b Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 9 Apr 2023 18:19:33 +0200 Subject: [PATCH] Disable variable assignment in SimpleEvaluationContext This commit introduces infrastructure to differentiate between programmatic setting of a variable in an EvaluationContext versus the assignment of a variable within a SpEL expression using the assignment operator (=). In addition, this commit disables variable assignment within expressions when using the SimpleEvaluationContext. Closes gh-30326 --- .../expression/EvaluationContext.java | 45 ++++++++++++++++--- .../expression/spel/ExpressionState.java | 33 ++++++++++++-- .../expression/spel/SpelMessage.java | 6 ++- .../expression/spel/ast/Assign.java | 7 ++- .../spel/ast/CompoundExpression.java | 13 ++++-- .../expression/spel/ast/Indexer.java | 13 ++++-- .../spel/ast/PropertyOrFieldReference.java | 9 +++- .../expression/spel/ast/SpelNodeImpl.java | 32 +++++++++++-- .../spel/ast/VariableReference.java | 21 +++++---- .../spel/support/SimpleEvaluationContext.java | 15 ++++++- .../expression/spel/EvaluationTests.java | 23 +++++++++- 11 files changed, 177 insertions(+), 40 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java index 0b4d2eab971b..0c393a86dbe6 100644 --- a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -17,6 +17,7 @@ package org.springframework.expression; import java.util.List; +import java.util.function.Supplier; import org.springframework.lang.Nullable; @@ -24,12 +25,21 @@ * Expressions are executed in an evaluation context. It is in this context that * references are resolved when encountered during expression evaluation. * - *

There is a default implementation of this EvaluationContext interface: - * {@link org.springframework.expression.spel.support.StandardEvaluationContext} - * which can be extended, rather than having to implement everything manually. + *

There are two default implementations of this interface. + *

* * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public interface EvaluationContext { @@ -85,7 +95,30 @@ public interface EvaluationContext { OperatorOverloader getOperatorOverloader(); /** - * Set a named variable within this evaluation context to a specified value. + * Assign the value created by the specified {@link Supplier} to a named variable + * within this evaluation context. + *

In contrast to {@link #setVariable(String, Object)}, this method should only + * be invoked to support the assignment operator ({@code =}) within an expression. + *

By default, this method delegates to {@code setVariable(String, Object)}, + * providing the value created by the {@code valueSupplier}. Concrete implementations + * may override this default method to provide different semantics. + * @param name the name of the variable to assign + * @param valueSupplier the supplier of the value to be assigned to the variable + * @return a {@link TypedValue} wrapping the assigned value + * @since 5.2.24 + */ + default TypedValue assignVariable(String name, Supplier valueSupplier) { + TypedValue typedValue = valueSupplier.get(); + setVariable(name, typedValue.getValue()); + return typedValue; + } + + /** + * Set a named variable in this evaluation context to a specified value. + *

In contrast to {@link #assignVariable(String, Supplier)}, this method + * should only be invoked programmatically when interacting directly with the + * {@code EvaluationContext} — for example, to provide initial + * configuration for the context. * @param name the name of the variable to set * @param value the value to be placed in the variable */ @@ -93,7 +126,7 @@ public interface EvaluationContext { /** * Look up a named variable within this evaluation context. - * @param name variable to lookup + * @param name the name of the variable to look up * @return the value of the variable, or {@code null} if not found */ @Nullable diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java index 252af447af31..8dadae596732 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.function.Supplier; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; @@ -38,18 +39,19 @@ import org.springframework.util.CollectionUtils; /** - * An ExpressionState is for maintaining per-expression-evaluation state, any changes to - * it are not seen by other expressions but it gives a place to hold local variables and + * ExpressionState is for maintaining per-expression-evaluation state: any changes to + * it are not seen by other expressions, but it gives a place to hold local variables and * for component expressions in a compound expression to communicate state. This is in * contrast to the EvaluationContext, which is shared amongst expression evaluations, and * any changes to it will be seen by other expressions or any code that chooses to ask * questions of the context. * - *

It also acts as a place for to define common utility routines that the various AST + *

It also acts as a place to define common utility routines that the various AST * nodes might need. * * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public class ExpressionState { @@ -138,6 +140,29 @@ public TypedValue getScopeRootContextObject() { return this.scopeRootObjects.element(); } + /** + * Assign the value created by the specified {@link Supplier} to a named variable + * within the evaluation context. + *

In contrast to {@link #setVariable(String, Object)}, this method should + * only be invoked to support assignment within an expression. + * @param name the name of the variable to assign + * @param valueSupplier the supplier of the value to be assigned to the variable + * @return a {@link TypedValue} wrapping the assigned value + * @since 5.2.24 + * @see EvaluationContext#assignVariable(String, Supplier) + */ + public TypedValue assignVariable(String name, Supplier valueSupplier) { + return this.relatedContext.assignVariable(name, valueSupplier); + } + + /** + * Set a named variable in the evaluation context to a specified value. + *

In contrast to {@link #assignVariable(String, Supplier)}, this method + * should only be invoked programmatically. + * @param name the name of the variable to set + * @param value the value to be placed in the variable + * @see EvaluationContext#setVariable(String, Object) + */ public void setVariable(String name, @Nullable Object value) { this.relatedContext.setVariable(name, value); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java index 11f68d94e5d9..b181cda36d8e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java @@ -280,7 +280,11 @@ public enum SpelMessage { /** @since 5.2.24 */ MAX_EXPRESSION_LENGTH_EXCEEDED(Kind.ERROR, 1079, - "SpEL expression is too long, exceeding the threshold of ''{0}'' characters"); + "SpEL expression is too long, exceeding the threshold of ''{0}'' characters"), + + /** @since 5.2.24 */ + VARIABLE_ASSIGNMENT_NOT_SUPPORTED(Kind.ERROR, 1080, + "Assignment to variable ''{0}'' is not supported"); private final Kind kind; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java index a009a07db512..55e5d2e4ff08 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -27,6 +27,7 @@ *

Example: 'someNumberProperty=42' * * @author Andy Clement + * @author Sam Brannen * @since 3.0 */ public class Assign extends SpelNodeImpl { @@ -38,9 +39,7 @@ public Assign(int startPos, int endPos, SpelNodeImpl... operands) { @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { - TypedValue newValue = this.children[1].getValueInternal(state); - getChild(0).setValue(state, newValue.getValue()); - return newValue; + return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state)); } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java index 0e47facfa7bf..616a503a4ec1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -17,6 +17,7 @@ package org.springframework.expression.spel.ast; import java.util.StringJoiner; +import java.util.function.Supplier; import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; @@ -24,13 +25,13 @@ import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; -import org.springframework.lang.Nullable; /** * Represents a DOT separated expression sequence, such as * {@code 'property1.property2.methodOne()'}. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 */ public class CompoundExpression extends SpelNodeImpl { @@ -95,8 +96,12 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep } @Override - public void setValue(ExpressionState state, @Nullable Object value) throws EvaluationException { - getValueRef(state).setValue(value); + public TypedValue setValueInternal(ExpressionState state, Supplier valueSupplier) + throws EvaluationException { + + TypedValue typedValue = valueSupplier.get(); + getValueRef(state).setValue(typedValue.getValue()); + return typedValue; } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index a12c11df85e6..7e80a576c13b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.StringJoiner; +import java.util.function.Supplier; import org.springframework.asm.MethodVisitor; import org.springframework.core.convert.TypeDescriptor; @@ -45,7 +46,7 @@ /** * An Indexer can index into some proceeding structure to access a particular piece of it. - * Supported structures are: strings / collections (lists/sets) / arrays. + *

Supported structures are: strings / collections (lists/sets) / arrays. * * @author Andy Clement * @author Phillip Webb @@ -103,8 +104,12 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep } @Override - public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException { - getValueRef(state).setValue(newValue); + public TypedValue setValueInternal(ExpressionState state, Supplier valueSupplier) + throws EvaluationException { + + TypedValue typedValue = valueSupplier.get(); + getValueRef(state).setValue(typedValue.getValue()); + return typedValue; } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java index 04e4336a7e20..28762153950b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import org.springframework.asm.Label; import org.springframework.asm.MethodVisitor; @@ -147,8 +148,12 @@ else if (Map.class == resultDescriptor.getType()) { } @Override - public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException { - writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, newValue); + public TypedValue setValueInternal(ExpressionState state, Supplier valueSupplier) + throws EvaluationException { + + TypedValue typedValue = valueSupplier.get(); + writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, typedValue.getValue()); + return typedValue; } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java index c3514ab1cdd9..cd528937cc6d 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Member; import java.lang.reflect.Method; +import java.util.function.Supplier; import org.springframework.asm.MethodVisitor; import org.springframework.asm.Opcodes; @@ -40,6 +41,7 @@ * * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public abstract class SpelNodeImpl implements SpelNode, Opcodes { @@ -64,7 +66,7 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes { *

The descriptor is like the bytecode form but is slightly easier to work with. * It does not include the trailing semicolon (for non array reference types). * Some examples: Ljava/lang/String, I, [I - */ + */ @Nullable protected volatile String exitTypeDescriptor; @@ -83,8 +85,8 @@ public SpelNodeImpl(int startPos, int endPos, SpelNodeImpl... operands) { /** - * Return {@code true} if the next child is one of the specified classes. - */ + * Return {@code true} if the next child is one of the specified classes. + */ protected boolean nextChildIs(Class... classes) { if (this.parent != null) { SpelNodeImpl[] peers = this.parent.children; @@ -125,6 +127,28 @@ public boolean isWritable(ExpressionState expressionState) throws EvaluationExce @Override public void setValue(ExpressionState expressionState, @Nullable Object newValue) throws EvaluationException { + setValueInternal(expressionState, () -> new TypedValue(newValue)); + } + + /** + * Evaluate the expression to a node and then set the new value created by the + * specified {@link Supplier} on that node. + *

For example, if the expression evaluates to a property reference, then the + * property will be set to the new value. + *

Favor this method over {@link #setValue(ExpressionState, Object)} when + * the value should be lazily computed. + *

By default, this method throws a {@link SpelEvaluationException}, + * effectively disabling this feature. Subclasses may override this method to + * provide an actual implementation. + * @param expressionState the current expression state (includes the context) + * @param valueSupplier a supplier of the new value + * @throws EvaluationException if any problem occurs evaluating the expression or + * setting the new value + * @since 5.2.24 + */ + public TypedValue setValueInternal(ExpressionState expressionState, Supplier valueSupplier) + throws EvaluationException { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.SETVALUE_NOT_SUPPORTED, getClass()); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java index 769e4efedbea..97dae78e902e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -17,9 +17,11 @@ package org.springframework.expression.spel.ast; import java.lang.reflect.Modifier; +import java.util.function.Supplier; import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.ExpressionState; @@ -27,10 +29,11 @@ import org.springframework.lang.Nullable; /** - * Represents a variable reference, eg. #someVar. Note this is different to a *local* - * variable like $someVar + * Represents a variable reference — for example, {@code #someVar}. Note + * that this is different than a local variable like {@code $someVar}. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 */ public class VariableReference extends SpelNodeImpl { @@ -53,14 +56,14 @@ public VariableReference(String variableName, int startPos, int endPos) { @Override public ValueRef getValueRef(ExpressionState state) throws SpelEvaluationException { if (this.name.equals(THIS)) { - return new ValueRef.TypedValueHolderValueRef(state.getActiveContextObject(),this); + return new ValueRef.TypedValueHolderValueRef(state.getActiveContextObject(), this); } if (this.name.equals(ROOT)) { - return new ValueRef.TypedValueHolderValueRef(state.getRootContextObject(),this); + return new ValueRef.TypedValueHolderValueRef(state.getRootContextObject(), this); } TypedValue result = state.lookupVariable(this.name); // a null value will mean either the value was null or the variable was not found - return new VariableRef(this.name,result,state.getEvaluationContext()); + return new VariableRef(this.name, result, state.getEvaluationContext()); } @Override @@ -90,8 +93,10 @@ public TypedValue getValueInternal(ExpressionState state) throws SpelEvaluationE } @Override - public void setValue(ExpressionState state, @Nullable Object value) throws SpelEvaluationException { - state.setVariable(this.name, value); + public TypedValue setValueInternal(ExpressionState state, Supplier valueSupplier) + throws EvaluationException { + + return state.assignVariable(this.name, valueSupplier); } @Override 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 d8826e344720..1168c9c91a26 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; @@ -78,6 +79,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 4.3.15 * @see #forPropertyAccessors * @see #forReadOnlyDataBinding() @@ -200,6 +202,17 @@ public OperatorOverloader getOperatorOverloader() { return this.operatorOverloader; } + /** + * {@code SimpleEvaluationContext} does not support variable assignment within + * expressions. + * @throws SpelEvaluationException with {@link SpelMessage#VARIABLE_ASSIGNMENT_NOT_SUPPORTED} + * @since 5.2.24 + */ + @Override + public TypedValue assignVariable(String name, Supplier valueSupplier) { + throw new SpelEvaluationException(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED, "#" + name); + } + @Override public void setVariable(String name, @Nullable Object value) { this.variables.put(name, value); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java index e81e2a43f713..fb3dda85507d 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java @@ -25,6 +25,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.expression.AccessException; import org.springframework.expression.BeanResolver; @@ -36,6 +38,7 @@ import org.springframework.expression.ParseException; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardTypeLocator; import org.springframework.expression.spel.testresources.TestPerson; @@ -149,8 +152,24 @@ void mixingOperators() { // assignment @Test - void assignmentToVariables() { - evaluate("#var1='value1'", "value1", String.class); + void assignmentToVariableWithStandardEvaluationContext() { + evaluate("#var1 = 'value1'", "value1", String.class); + } + + @ParameterizedTest + @CsvSource(quoteCharacter = '"', delimiterString = "->", textBlock = """ + "#var1 = 'value1'" -> #var1 + "true ? #myVar = 4 : 0" -> #myVar + """) + void assignmentToVariableWithSimpleEvaluationContext(String expression, String varName) { + EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); + Expression expr = parser.parseExpression(expression); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> expr.getValue(context)) + .satisfies(ex -> { + assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED); + assertThat(ex.getInserts()).as("inserts").containsExactly(varName); + }); } @Test