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 338b18dad3e..15797db806d 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-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. @@ -17,7 +17,6 @@ package org.springframework.expression.spel; import java.util.ArrayDeque; -import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.List; @@ -74,9 +73,9 @@ public class ExpressionState { // For example: // #list1.?[#list2.contains(#this)] // On entering the selection we enter a new scope, and #this is now the - // element from list1 + // element from list1. @Nullable - private ArrayDeque scopeRootObjects; + private Deque scopeRootObjects; public ExpressionState(EvaluationContext context) { @@ -112,18 +111,12 @@ public class ExpressionState { } public void pushActiveContextObject(TypedValue obj) { - if (this.contextObjects == null) { - this.contextObjects = new ArrayDeque<>(); - } - this.contextObjects.push(obj); + initContextObjects().push(obj); } public void popActiveContextObject() { - if (this.contextObjects == null) { - this.contextObjects = new ArrayDeque<>(); - } try { - this.contextObjects.pop(); + initContextObjects().pop(); } catch (NoSuchElementException ex) { throw new IllegalStateException("Cannot pop active context object: stack is empty"); @@ -168,6 +161,14 @@ public class ExpressionState { this.relatedContext.setVariable(name, value); } + /** + * Look up a named global variable in the evaluation context. + * @param name the name of the variable to look up + * @return a {@link TypedValue} containing the value of the variable, or + * {@link TypedValue#NULL} if the variable does not exist + * @see #assignVariable(String, Supplier) + * @see #setVariable(String, Object) + */ public TypedValue lookupVariable(String name) { Object value = this.relatedContext.lookupVariable(name); return (value != null ? new TypedValue(value) : TypedValue.NULL); @@ -181,6 +182,10 @@ public class ExpressionState { return this.relatedContext.getTypeLocator().findType(type); } + public TypeConverter getTypeConverter() { + return this.relatedContext.getTypeConverter(); + } + public Object convertValue(Object value, TypeDescriptor targetTypeDescriptor) throws EvaluationException { Object result = this.relatedContext.getTypeConverter().convertValue( value, TypeDescriptor.forObject(value), targetTypeDescriptor); @@ -190,10 +195,6 @@ public class ExpressionState { return result; } - public TypeConverter getTypeConverter() { - return this.relatedContext.getTypeConverter(); - } - @Nullable public Object convertValue(TypedValue value, TypeDescriptor targetTypeDescriptor) throws EvaluationException { Object val = value.getValue(); @@ -201,33 +202,61 @@ public class ExpressionState { val, TypeDescriptor.forObject(val), targetTypeDescriptor); } - /* - * A new scope is entered when a function is invoked. + /** + * Enter a new scope with a new {@linkplain #getActiveContextObject() root + * context object} and a new local variable scope. */ - public void enterScope(@Nullable Map argMap) { - initVariableScopes().push(new VariableScope(argMap)); - initScopeRootObjects().push(getActiveContextObject()); - } - public void enterScope() { - initVariableScopes().push(new VariableScope(Collections.emptyMap())); + initVariableScopes().push(new VariableScope()); initScopeRootObjects().push(getActiveContextObject()); } + /** + * Enter a new scope with a new {@linkplain #getActiveContextObject() root + * context object} and a new local variable scope containing the supplied + * name/value pair. + * @param name the name of the local variable + * @param value the value of the local variable + */ public void enterScope(String name, Object value) { initVariableScopes().push(new VariableScope(name, value)); initScopeRootObjects().push(getActiveContextObject()); } + /** + * Enter a new scope with a new {@linkplain #getActiveContextObject() root + * context object} and a new local variable scope containing the supplied + * name/value pairs. + * @param variables a map containing name/value pairs for local variables + */ + public void enterScope(@Nullable Map variables) { + initVariableScopes().push(new VariableScope(variables)); + initScopeRootObjects().push(getActiveContextObject()); + } + public void exitScope() { initVariableScopes().pop(); initScopeRootObjects().pop(); } + /** + * Set a local variable with the given name to the supplied value within the + * current scope. + *

If a local variable with the given name already exists, it will be + * overwritten. + * @param name the name of the local variable + * @param value the value of the local variable + */ public void setLocalVariable(String name, Object value) { initVariableScopes().element().setVariable(name, value); } + /** + * Look up the value of the local variable with the given name. + * @param name the name of the local variable + * @return the value of the local variable, or {@code null} if the variable + * does not exist in the current scope + */ @Nullable public Object lookupLocalVariable(String name) { for (VariableScope scope : initVariableScopes()) { @@ -238,13 +267,11 @@ public class ExpressionState { return null; } - private Deque initVariableScopes() { - if (this.variableScopes == null) { - this.variableScopes = new ArrayDeque<>(); - // top-level empty variable scope - this.variableScopes.add(new VariableScope()); + private Deque initContextObjects() { + if (this.contextObjects == null) { + this.contextObjects = new ArrayDeque<>(); } - return this.variableScopes; + return this.contextObjects; } private Deque initScopeRootObjects() { @@ -254,6 +281,15 @@ public class ExpressionState { return this.scopeRootObjects; } + private Deque initVariableScopes() { + if (this.variableScopes == null) { + this.variableScopes = new ArrayDeque<>(); + // top-level empty variable scope + this.variableScopes.add(new VariableScope()); + } + return this.variableScopes; + } + public TypedValue operate(Operation op, @Nullable Object left, @Nullable Object right) throws EvaluationException { OperatorOverloader overloader = this.relatedContext.getOperatorOverloader(); if (overloader.overridesOperation(op, left, right)) { @@ -281,40 +317,40 @@ public class ExpressionState { /** - * A new scope is entered when a function is called and it is used to hold the - * parameters to the function call. If the names of the parameters clash with - * those in a higher level scope, those in the higher level scope will not be - * accessible whilst the function is executing. When the function returns, - * the scope is exited. + * A new local variable scope is entered when a new expression scope is + * entered and exited when the corresponding expression scope is exited. + * + *

If variable names clash with those in a higher level scope, those in + * the higher level scope will not be accessible within the current scope. */ private static class VariableScope { - private final Map vars = new HashMap<>(); + private final Map variables = new HashMap<>(); - public VariableScope() { + VariableScope() { } - public VariableScope(@Nullable Map arguments) { - if (arguments != null) { - this.vars.putAll(arguments); - } + VariableScope(String name, Object value) { + this.variables.put(name, value); } - public VariableScope(String name, Object value) { - this.vars.put(name, value); + VariableScope(@Nullable Map variables) { + if (variables != null) { + this.variables.putAll(variables); + } } @Nullable - public Object lookupVariable(String name) { - return this.vars.get(name); + Object lookupVariable(String name) { + return this.variables.get(name); } - public void setVariable(String name, Object value) { - this.vars.put(name,value); + void setVariable(String name, Object value) { + this.variables.put(name,value); } - public boolean definesVariable(String name) { - return this.vars.containsKey(name); + boolean definesVariable(String name) { + return this.variables.containsKey(name); } }