Skip to content

Commit

Permalink
Disable variable assignment in SimpleEvaluationContext
Browse files Browse the repository at this point in the history
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-30328
  • Loading branch information
sbrannen authored and bclozel committed Apr 13, 2023
1 parent 965a639 commit e246b47
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -17,19 +17,29 @@
package org.springframework.expression;

import java.util.List;
import java.util.function.Supplier;

import org.springframework.lang.Nullable;

/**
* Expressions are executed in an evaluation context. It is in this context that
* references are resolved when encountered during expression evaluation.
*
* <p>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.
* <p>There are two default implementations of this interface.
* <ul>
* <li>{@link org.springframework.expression.spel.support.SimpleEvaluationContext
* SimpleEvaluationContext}: a simpler builder-style {@code EvaluationContext}
* variant for data-binding purposes, which allows for opting into several SpEL
* features as needed.</li>
* <li>{@link org.springframework.expression.spel.support.StandardEvaluationContext
* StandardEvaluationContext}: a powerful and highly configurable {@code EvaluationContext}
* implementation, which can be extended, rather than having to implement everything
* manually.</li>
* </ul>
*
* @author Andy Clement
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
*/
public interface EvaluationContext {
Expand Down Expand Up @@ -85,15 +95,38 @@ 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.
* <p>In contrast to {@link #setVariable(String, Object)}, this method should only
* be invoked to support the assignment operator ({@code =}) within an expression.
* <p>By default, this method delegates to {@code setVariable(String, Object)},
* providing the value created by the {@code valueSupplier}. Concrete implementations
* may override this <em>default</em> 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<TypedValue> valueSupplier) {
TypedValue typedValue = valueSupplier.get();
setVariable(name, typedValue.getValue());
return typedValue;
}

/**
* Set a named variable in this evaluation context to a specified value.
* <p>In contrast to {@link #assignVariable(String, Supplier)}, this method
* should only be invoked programmatically when interacting directly with the
* {@code EvaluationContext} &mdash; 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
*/
void setVariable(String name, @Nullable Object value);

/**
* 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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.
*
* <p>It also acts as a place for to define common utility routines that the various AST
* <p>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 {
Expand Down Expand Up @@ -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.
* <p>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<TypedValue> valueSupplier) {
return this.relatedContext.assignVariable(name, valueSupplier);
}

/**
* Set a named variable in the evaluation context to a specified value.
* <p>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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -27,6 +27,7 @@
* <p>Example: 'someNumberProperty=42'
*
* @author Andy Clement
* @author Sam Brannen
* @since 3.0
*/
public class Assign extends SpelNodeImpl {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -17,20 +17,21 @@
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;
import org.springframework.expression.TypedValue;
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 {
Expand Down Expand Up @@ -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<TypedValue> valueSupplier)
throws EvaluationException {

TypedValue typedValue = valueSupplier.get();
getValueRef(state).setValue(typedValue.getValue());
return typedValue;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -45,11 +46,12 @@

/**
* An Indexer can index into some proceeding structure to access a particular piece of it.
* Supported structures are: strings / collections (lists/sets) / arrays.
* <p>Supported structures are: strings / collections (lists/sets) / arrays.
*
* @author Andy Clement
* @author Phillip Webb
* @author Stephane Nicoll
* @author Sam Brannen
* @since 3.0
*/
// TODO support multidimensional arrays
Expand Down Expand Up @@ -102,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<TypedValue> valueSupplier)
throws EvaluationException {

TypedValue typedValue = valueSupplier.get();
getValueRef(state).setValue(typedValue.getValue());
return typedValue;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -46,6 +47,7 @@
* @author Andy Clement
* @author Juergen Hoeller
* @author Clark Duplichien
* @author Sam Brannen
* @since 3.0
*/
public class PropertyOrFieldReference extends SpelNodeImpl {
Expand Down Expand Up @@ -147,8 +149,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<TypedValue> valueSupplier)
throws EvaluationException {

TypedValue typedValue = valueSupplier.get();
writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, typedValue.getValue());
return typedValue;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -40,6 +41,7 @@
*
* @author Andy Clement
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
*/
public abstract class SpelNodeImpl implements SpelNode, Opcodes {
Expand All @@ -64,7 +66,7 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes {
* <p>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;

Expand All @@ -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;
Expand Down Expand Up @@ -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.
* <p>For example, if the expression evaluates to a property reference, then the
* property will be set to the new value.
* <p>Favor this method over {@link #setValue(ExpressionState, Object)} when
* the value should be lazily computed.
* <p>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<TypedValue> valueSupplier)
throws EvaluationException {

throw new SpelEvaluationException(getStartPosition(), SpelMessage.SETVALUE_NOT_SUPPORTED, getClass());
}

Expand Down
Loading

0 comments on commit e246b47

Please sign in to comment.