SpEL: ensure correct object used for nested #this references

Before this commit the object that #this would refer to in
nested expressions within projection/selection clauses was always
the root context object. This was incorrect as it should be the
element being projected/selected over. This commit introduces
a scope root context object which is set upon entering a new
scope (like when entering a projection or selection). Any
object. With this change this kind of expression now behaves:

where #this is the element of list1. Unqualified references
are also resolved against this scope root context object.

Issues: SPR-10417, SPR-12035, SPR-13055
This commit is contained in:
Andy Clement
2015-05-22 08:07:09 -07:00
parent 5a3eea8adb
commit 91ed5b6b8c
5 changed files with 197 additions and 7 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2015 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.
@@ -16,6 +16,7 @@
package org.springframework.expression.spel;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -52,6 +53,15 @@ public class ExpressionState {
private final TypedValue rootObject;
// When entering a new scope there is a new base object which should be used
// for '#this' references (or to act as a target for unqualified references).
// This stack captures those objects at each nested scope level.
// For example:
// #list1.?[#list2.contains(#this)]
// On entering the selection we enter a new scope, and #this is now the
// element from list1
private Stack<TypedValue> scopeRootObjects;
private final SpelParserConfiguration configuration;
private Stack<VariableScope> variableScopes;
@@ -86,6 +96,9 @@ public class ExpressionState {
// top level empty variable scope
this.variableScopes.add(new VariableScope());
}
if (this.scopeRootObjects == null) {
this.scopeRootObjects = new Stack<TypedValue>();
}
}
/**
@@ -116,6 +129,13 @@ public class ExpressionState {
return this.rootObject;
}
public TypedValue getScopeRootContextObject() {
if (this.scopeRootObjects == null || this.scopeRootObjects.isEmpty()) {
return this.rootObject;
}
return this.scopeRootObjects.peek();
}
public void setVariable(String name, Object value) {
this.relatedContext.setVariable(name, value);
}
@@ -158,16 +178,25 @@ public class ExpressionState {
public void enterScope(Map<String, Object> argMap) {
ensureVariableScopesInitialized();
this.variableScopes.push(new VariableScope(argMap));
this.scopeRootObjects.push(getActiveContextObject());
}
public void enterScope() {
ensureVariableScopesInitialized();
this.variableScopes.push(new VariableScope(Collections.<String,Object>emptyMap()));
this.scopeRootObjects.push(getActiveContextObject());
}
public void enterScope(String name, Object value) {
ensureVariableScopesInitialized();
this.variableScopes.push(new VariableScope(name, value));
this.scopeRootObjects.push(getActiveContextObject());
}
public void exitScope() {
ensureVariableScopesInitialized();
this.variableScopes.pop();
this.scopeRootObjects.pop();
}
public void setLocalVariable(String name, Object value) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -150,7 +150,7 @@ public class MethodReference extends SpelNodeImpl {
for (int i = 0; i < arguments.length; i++) {
// Make the root object the active context again for evaluating the parameter expressions
try {
state.pushActiveContextObject(state.getRootContextObject());
state.pushActiveContextObject(state.getScopeRootContextObject());
arguments[i] = this.children[i].getValueInternal(state).getValue();
}
finally {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -68,17 +68,19 @@ public class Projection extends SpelNodeImpl {
// before calling the specified operation. This special context object
// has two fields 'key' and 'value' that refer to the map entries key
// and value, and they can be referenced in the operation
// eg. {'a':'y','b':'n'}.!{value=='y'?key:null}" == ['a', null]
// eg. {'a':'y','b':'n'}.![value=='y'?key:null]" == ['a', null]
if (operand instanceof Map) {
Map<?, ?> mapData = (Map<?, ?>) operand;
List<Object> result = new ArrayList<Object>();
for (Map.Entry<?, ?> entry : mapData.entrySet()) {
try {
state.pushActiveContextObject(new TypedValue(entry));
state.enterScope();
result.add(this.children[0].getValueInternal(state).getValue());
}
finally {
state.popActiveContextObject();
state.exitScope();
}
}
return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this); // TODO unable to build correct type descriptor

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -86,6 +86,7 @@ public class Selection extends SpelNodeImpl {
try {
TypedValue kvPair = new TypedValue(entry);
state.pushActiveContextObject(kvPair);
state.enterScope();
Object val = selectionCriteria.getValueInternal(state).getValue();
if (val instanceof Boolean) {
if ((Boolean) val) {
@@ -104,6 +105,7 @@ public class Selection extends SpelNodeImpl {
}
finally {
state.popActiveContextObject();
state.exitScope();
}
}
if ((this.variant == FIRST || this.variant == LAST) && result.isEmpty()) {