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:
parent
5a3eea8adb
commit
91ed5b6b8c
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -36,7 +36,6 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.expression.AccessException;
|
||||
|
@ -1918,6 +1917,164 @@ public class SpelReproTests extends AbstractExpressionTests {
|
|||
sec.setVariable("no", "1.0");
|
||||
assertTrue(expression.getValue(sec).toString().startsWith("Object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
public void SPR13055() throws Exception {
|
||||
List<Map<String, Object>> myPayload = new ArrayList<Map<String, Object>>();
|
||||
|
||||
Map<String, Object> v1 = new HashMap<String, Object>();
|
||||
Map<String, Object> v2 = new HashMap<String, Object>();
|
||||
|
||||
v1.put("test11", "test11");
|
||||
v1.put("test12", "test12");
|
||||
v2.put("test21", "test21");
|
||||
v2.put("test22", "test22");
|
||||
|
||||
myPayload.add(v1);
|
||||
myPayload.add(v2);
|
||||
|
||||
EvaluationContext context = new StandardEvaluationContext(myPayload);
|
||||
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
String ex = "#root.![T(org.springframework.util.StringUtils).collectionToCommaDelimitedString(#this.values())]";
|
||||
List res = parser.parseExpression(ex).getValue(context, List.class);
|
||||
assertEquals("[test12,test11, test22,test21]", res.toString());
|
||||
|
||||
res = parser.parseExpression("#root.![#this.values()]").getValue(context,
|
||||
List.class);
|
||||
assertEquals("[[test12, test11], [test22, test21]]", res.toString());
|
||||
|
||||
res = parser.parseExpression("#root.![values()]").getValue(context, List.class);
|
||||
assertEquals("[[test12, test11], [test22, test21]]", res.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void SPR12035() {
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
Expression expression1 = parser.parseExpression("list.?[ value>2 ].size()!=0");
|
||||
assertTrue(expression1.getValue(new BeanClass(new ListOf(1.1), new ListOf(2.2)),
|
||||
Boolean.class));
|
||||
|
||||
Expression expression2 = parser.parseExpression("list.?[ T(java.lang.Math).abs(value) > 2 ].size()!=0");
|
||||
assertTrue(expression2.getValue(new BeanClass(new ListOf(1.1), new ListOf(-2.2)),
|
||||
Boolean.class));
|
||||
}
|
||||
|
||||
static class CCC {
|
||||
public boolean method(Object o) {
|
||||
System.out.println(o);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void SPR13055_maps() {
|
||||
EvaluationContext context = new StandardEvaluationContext();
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
Expression ex = parser.parseExpression("{'a':'y','b':'n'}.![value=='y'?key:null]");
|
||||
assertEquals("[a, null]", ex.getValue(context).toString());
|
||||
|
||||
ex = parser.parseExpression("{2:4,3:6}.![T(java.lang.Math).abs(#this.key) + 5]");
|
||||
assertEquals("[7, 8]", ex.getValue(context).toString());
|
||||
|
||||
ex = parser.parseExpression("{2:4,3:6}.![T(java.lang.Math).abs(#this.value) + 5]");
|
||||
assertEquals("[9, 11]", ex.getValue(context).toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public void SPR10417() {
|
||||
List list1 = new ArrayList();
|
||||
list1.add("a");
|
||||
list1.add("b");
|
||||
list1.add("x");
|
||||
List list2 = new ArrayList();
|
||||
list2.add("c");
|
||||
list2.add("x");
|
||||
EvaluationContext context = new StandardEvaluationContext();
|
||||
context.setVariable("list1", list1);
|
||||
context.setVariable("list2", list2);
|
||||
|
||||
// #this should be the element from list1
|
||||
Expression ex = parser.parseExpression("#list1.?[#list2.contains(#this)]");
|
||||
Object result = ex.getValue(context);
|
||||
assertEquals("[x]", result.toString());
|
||||
|
||||
// toString() should be called on the element from list1
|
||||
ex = parser.parseExpression("#list1.?[#list2.contains(toString())]");
|
||||
result = ex.getValue(context);
|
||||
assertEquals("[x]", result.toString());
|
||||
|
||||
List list3 = new ArrayList();
|
||||
list3.add(1);
|
||||
list3.add(2);
|
||||
list3.add(3);
|
||||
list3.add(4);
|
||||
|
||||
context = new StandardEvaluationContext();
|
||||
context.setVariable("list3", list3);
|
||||
ex = parser.parseExpression("#list3.?[#this > 2]");
|
||||
result = ex.getValue(context);
|
||||
assertEquals("[3, 4]", result.toString());
|
||||
|
||||
ex = parser.parseExpression("#list3.?[#this >= T(java.lang.Math).abs(T(java.lang.Math).abs(#this))]");
|
||||
result = ex.getValue(context);
|
||||
assertEquals("[1, 2, 3, 4]", result.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public void SPR10417_maps() {
|
||||
Map map1 = new HashMap();
|
||||
map1.put("A", 65);
|
||||
map1.put("B", 66);
|
||||
map1.put("X", 66);
|
||||
Map map2 = new HashMap();
|
||||
map2.put("X", 66);
|
||||
|
||||
EvaluationContext context = new StandardEvaluationContext();
|
||||
context.setVariable("map1", map1);
|
||||
context.setVariable("map2", map2);
|
||||
|
||||
// #this should be the element from list1
|
||||
Expression ex = parser.parseExpression("#map1.?[#map2.containsKey(#this.getKey())]");
|
||||
Object result = ex.getValue(context);
|
||||
assertEquals("{X=66}", result.toString());
|
||||
|
||||
ex = parser.parseExpression("#map1.?[#map2.containsKey(key)]");
|
||||
result = ex.getValue(context);
|
||||
assertEquals("{X=66}", result.toString());
|
||||
}
|
||||
|
||||
public static class ListOf {
|
||||
|
||||
private final double value;
|
||||
|
||||
public ListOf(double v) {
|
||||
this.value = v;
|
||||
}
|
||||
|
||||
public double getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public static class BeanClass {
|
||||
|
||||
private final List<ListOf> list;
|
||||
|
||||
public BeanClass(ListOf... list) {
|
||||
this.list = Arrays.asList(list);
|
||||
}
|
||||
|
||||
public List<ListOf> getList() {
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static enum ABC { A, B, C }
|
||||
|
|
Loading…
Reference in New Issue