|
|
|
@ -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"); |
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
|
|
* you may not use this file except in compliance with the License. |
|
|
|
* you may not use this file except in compliance with the License. |
|
|
|
@ -17,7 +17,6 @@ |
|
|
|
package org.springframework.expression.spel; |
|
|
|
package org.springframework.expression.spel; |
|
|
|
|
|
|
|
|
|
|
|
import java.util.ArrayDeque; |
|
|
|
import java.util.ArrayDeque; |
|
|
|
import java.util.Collections; |
|
|
|
|
|
|
|
import java.util.Deque; |
|
|
|
import java.util.Deque; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.List; |
|
|
|
import java.util.List; |
|
|
|
@ -74,9 +73,9 @@ public class ExpressionState { |
|
|
|
// For example:
|
|
|
|
// For example:
|
|
|
|
// #list1.?[#list2.contains(#this)]
|
|
|
|
// #list1.?[#list2.contains(#this)]
|
|
|
|
// On entering the selection we enter a new scope, and #this is now the
|
|
|
|
// On entering the selection we enter a new scope, and #this is now the
|
|
|
|
// element from list1
|
|
|
|
// element from list1.
|
|
|
|
@Nullable |
|
|
|
@Nullable |
|
|
|
private ArrayDeque<TypedValue> scopeRootObjects; |
|
|
|
private Deque<TypedValue> scopeRootObjects; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public ExpressionState(EvaluationContext context) { |
|
|
|
public ExpressionState(EvaluationContext context) { |
|
|
|
@ -112,18 +111,12 @@ public class ExpressionState { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public void pushActiveContextObject(TypedValue obj) { |
|
|
|
public void pushActiveContextObject(TypedValue obj) { |
|
|
|
if (this.contextObjects == null) { |
|
|
|
initContextObjects().push(obj); |
|
|
|
this.contextObjects = new ArrayDeque<>(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
this.contextObjects.push(obj); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public void popActiveContextObject() { |
|
|
|
public void popActiveContextObject() { |
|
|
|
if (this.contextObjects == null) { |
|
|
|
|
|
|
|
this.contextObjects = new ArrayDeque<>(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
try { |
|
|
|
this.contextObjects.pop(); |
|
|
|
initContextObjects().pop(); |
|
|
|
} |
|
|
|
} |
|
|
|
catch (NoSuchElementException ex) { |
|
|
|
catch (NoSuchElementException ex) { |
|
|
|
throw new IllegalStateException("Cannot pop active context object: stack is empty"); |
|
|
|
throw new IllegalStateException("Cannot pop active context object: stack is empty"); |
|
|
|
@ -168,6 +161,14 @@ public class ExpressionState { |
|
|
|
this.relatedContext.setVariable(name, value); |
|
|
|
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) { |
|
|
|
public TypedValue lookupVariable(String name) { |
|
|
|
Object value = this.relatedContext.lookupVariable(name); |
|
|
|
Object value = this.relatedContext.lookupVariable(name); |
|
|
|
return (value != null ? new TypedValue(value) : TypedValue.NULL); |
|
|
|
return (value != null ? new TypedValue(value) : TypedValue.NULL); |
|
|
|
@ -181,6 +182,10 @@ public class ExpressionState { |
|
|
|
return this.relatedContext.getTypeLocator().findType(type); |
|
|
|
return this.relatedContext.getTypeLocator().findType(type); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public TypeConverter getTypeConverter() { |
|
|
|
|
|
|
|
return this.relatedContext.getTypeConverter(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public Object convertValue(Object value, TypeDescriptor targetTypeDescriptor) throws EvaluationException { |
|
|
|
public Object convertValue(Object value, TypeDescriptor targetTypeDescriptor) throws EvaluationException { |
|
|
|
Object result = this.relatedContext.getTypeConverter().convertValue( |
|
|
|
Object result = this.relatedContext.getTypeConverter().convertValue( |
|
|
|
value, TypeDescriptor.forObject(value), targetTypeDescriptor); |
|
|
|
value, TypeDescriptor.forObject(value), targetTypeDescriptor); |
|
|
|
@ -190,10 +195,6 @@ public class ExpressionState { |
|
|
|
return result; |
|
|
|
return result; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public TypeConverter getTypeConverter() { |
|
|
|
|
|
|
|
return this.relatedContext.getTypeConverter(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Nullable |
|
|
|
@Nullable |
|
|
|
public Object convertValue(TypedValue value, TypeDescriptor targetTypeDescriptor) throws EvaluationException { |
|
|
|
public Object convertValue(TypedValue value, TypeDescriptor targetTypeDescriptor) throws EvaluationException { |
|
|
|
Object val = value.getValue(); |
|
|
|
Object val = value.getValue(); |
|
|
|
@ -201,33 +202,61 @@ public class ExpressionState { |
|
|
|
val, TypeDescriptor.forObject(val), targetTypeDescriptor); |
|
|
|
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<String, Object> argMap) { |
|
|
|
|
|
|
|
initVariableScopes().push(new VariableScope(argMap)); |
|
|
|
|
|
|
|
initScopeRootObjects().push(getActiveContextObject()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void enterScope() { |
|
|
|
public void enterScope() { |
|
|
|
initVariableScopes().push(new VariableScope(Collections.emptyMap())); |
|
|
|
initVariableScopes().push(new VariableScope()); |
|
|
|
initScopeRootObjects().push(getActiveContextObject()); |
|
|
|
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) { |
|
|
|
public void enterScope(String name, Object value) { |
|
|
|
initVariableScopes().push(new VariableScope(name, value)); |
|
|
|
initVariableScopes().push(new VariableScope(name, value)); |
|
|
|
initScopeRootObjects().push(getActiveContextObject()); |
|
|
|
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<String, Object> variables) { |
|
|
|
|
|
|
|
initVariableScopes().push(new VariableScope(variables)); |
|
|
|
|
|
|
|
initScopeRootObjects().push(getActiveContextObject()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public void exitScope() { |
|
|
|
public void exitScope() { |
|
|
|
initVariableScopes().pop(); |
|
|
|
initVariableScopes().pop(); |
|
|
|
initScopeRootObjects().pop(); |
|
|
|
initScopeRootObjects().pop(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Set a local variable with the given name to the supplied value within the |
|
|
|
|
|
|
|
* current scope. |
|
|
|
|
|
|
|
* <p>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) { |
|
|
|
public void setLocalVariable(String name, Object value) { |
|
|
|
initVariableScopes().element().setVariable(name, 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 |
|
|
|
@Nullable |
|
|
|
public Object lookupLocalVariable(String name) { |
|
|
|
public Object lookupLocalVariable(String name) { |
|
|
|
for (VariableScope scope : initVariableScopes()) { |
|
|
|
for (VariableScope scope : initVariableScopes()) { |
|
|
|
@ -238,13 +267,11 @@ public class ExpressionState { |
|
|
|
return null; |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private Deque<VariableScope> initVariableScopes() { |
|
|
|
private Deque<TypedValue> initContextObjects() { |
|
|
|
if (this.variableScopes == null) { |
|
|
|
if (this.contextObjects == null) { |
|
|
|
this.variableScopes = new ArrayDeque<>(); |
|
|
|
this.contextObjects = new ArrayDeque<>(); |
|
|
|
// top-level empty variable scope
|
|
|
|
|
|
|
|
this.variableScopes.add(new VariableScope()); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
return this.variableScopes; |
|
|
|
return this.contextObjects; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private Deque<TypedValue> initScopeRootObjects() { |
|
|
|
private Deque<TypedValue> initScopeRootObjects() { |
|
|
|
@ -254,6 +281,15 @@ public class ExpressionState { |
|
|
|
return this.scopeRootObjects; |
|
|
|
return this.scopeRootObjects; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private Deque<VariableScope> 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 { |
|
|
|
public TypedValue operate(Operation op, @Nullable Object left, @Nullable Object right) throws EvaluationException { |
|
|
|
OperatorOverloader overloader = this.relatedContext.getOperatorOverloader(); |
|
|
|
OperatorOverloader overloader = this.relatedContext.getOperatorOverloader(); |
|
|
|
if (overloader.overridesOperation(op, left, right)) { |
|
|
|
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 |
|
|
|
* A new local variable scope is entered when a new expression scope is |
|
|
|
* parameters to the function call. If the names of the parameters clash with |
|
|
|
* entered and exited when the corresponding expression scope is exited. |
|
|
|
* 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, |
|
|
|
* <p>If variable names clash with those in a higher level scope, those in |
|
|
|
* the scope is exited. |
|
|
|
* the higher level scope will not be accessible within the current scope. |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
private static class VariableScope { |
|
|
|
private static class VariableScope { |
|
|
|
|
|
|
|
|
|
|
|
private final Map<String, Object> vars = new HashMap<>(); |
|
|
|
private final Map<String, Object> variables = new HashMap<>(); |
|
|
|
|
|
|
|
|
|
|
|
public VariableScope() { |
|
|
|
VariableScope() { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public VariableScope(@Nullable Map<String, Object> arguments) { |
|
|
|
VariableScope(String name, Object value) { |
|
|
|
if (arguments != null) { |
|
|
|
this.variables.put(name, value); |
|
|
|
this.vars.putAll(arguments); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public VariableScope(String name, Object value) { |
|
|
|
VariableScope(@Nullable Map<String, Object> variables) { |
|
|
|
this.vars.put(name, value); |
|
|
|
if (variables != null) { |
|
|
|
|
|
|
|
this.variables.putAll(variables); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Nullable |
|
|
|
@Nullable |
|
|
|
public Object lookupVariable(String name) { |
|
|
|
Object lookupVariable(String name) { |
|
|
|
return this.vars.get(name); |
|
|
|
return this.variables.get(name); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public void setVariable(String name, Object value) { |
|
|
|
void setVariable(String name, Object value) { |
|
|
|
this.vars.put(name,value); |
|
|
|
this.variables.put(name,value); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public boolean definesVariable(String name) { |
|
|
|
boolean definesVariable(String name) { |
|
|
|
return this.vars.containsKey(name); |
|
|
|
return this.variables.containsKey(name); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|