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 bf4866660a..5a24e72801 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-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 scopeRootObjects; + private final SpelParserConfiguration configuration; private Stack 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(); + } } /** @@ -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 argMap) { ensureVariableScopesInitialized(); this.variableScopes.push(new VariableScope(argMap)); + this.scopeRootObjects.push(getActiveContextObject()); + } + + public void enterScope() { + ensureVariableScopesInitialized(); + this.variableScopes.push(new VariableScope(Collections.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) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java index 98b9e6f85c..0db8050f27 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java @@ -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 { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java index 2e91f8b32f..2b23219ad7 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java @@ -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 result = new ArrayList(); 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 diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java index 944b9486b8..81523e505a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java @@ -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()) { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java index f025594421..ea3213afa3 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java @@ -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> myPayload = new ArrayList>(); + + Map v1 = new HashMap(); + Map v2 = new HashMap(); + + 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 list; + + public BeanClass(ListOf... list) { + this.list = Arrays.asList(list); + } + + public List getList() { + return list; + } + } private static enum ABC { A, B, C }