Browse Source
This commit introduces the ability to specify an inline map in an expression. The syntax is similar to inline lists and of the form: "{key:value,key2:value}". The keys can optionally be quoted. The documentation is also updated with information on the syntax. Issue: SPR-9472pull/619/head
8 changed files with 394 additions and 41 deletions
@ -0,0 +1,162 @@
@@ -0,0 +1,162 @@
|
||||
/* |
||||
* Copyright 2014 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.expression.spel.ast; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
|
||||
import org.springframework.expression.EvaluationException; |
||||
import org.springframework.expression.TypedValue; |
||||
import org.springframework.expression.spel.ExpressionState; |
||||
import org.springframework.expression.spel.SpelNode; |
||||
|
||||
/** |
||||
* Represent a map in an expression, e.g. '{name:'foo',age:12}' |
||||
* |
||||
* @author Andy Clement |
||||
* @since 4.1 |
||||
*/ |
||||
public class InlineMap extends SpelNodeImpl { |
||||
|
||||
// if the map is purely literals, it is a constant value and can be computed and cached
|
||||
TypedValue constant = null; |
||||
|
||||
public InlineMap(int pos, SpelNodeImpl... args) { |
||||
super(pos, args); |
||||
checkIfConstant(); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* If all the components of the list are constants, or lists/maps that themselves contain constants, then a constant list |
||||
* can be built to represent this node. This will speed up later getValue calls and reduce the amount of garbage |
||||
* created. |
||||
*/ |
||||
private void checkIfConstant() { |
||||
boolean isConstant = true; |
||||
for (int c = 0, max = getChildCount(); c < max; c++) { |
||||
SpelNode child = getChild(c); |
||||
if (!(child instanceof Literal)) { |
||||
if (child instanceof InlineList) { |
||||
InlineList inlineList = (InlineList) child; |
||||
if (!inlineList.isConstant()) { |
||||
isConstant = false; |
||||
break; |
||||
} |
||||
} |
||||
else if (child instanceof InlineMap) { |
||||
InlineMap inlineMap = (InlineMap) child; |
||||
if (!inlineMap.isConstant()) { |
||||
isConstant = false; |
||||
break; |
||||
} |
||||
} |
||||
else if (!((c%2)==0 && (child instanceof PropertyOrFieldReference))) { |
||||
isConstant = false; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
if (isConstant) { |
||||
Map<Object,Object> constantMap = new LinkedHashMap<Object,Object>(); |
||||
int childcount = getChildCount(); |
||||
for (int c = 0; c < childcount; c++) { |
||||
SpelNode keyChild = getChild(c++); |
||||
SpelNode valueChild = getChild(c); |
||||
Object key = null; |
||||
Object value = null; |
||||
if ((keyChild instanceof Literal)) { |
||||
key = ((Literal) keyChild).getLiteralValue().getValue(); |
||||
} |
||||
else if (keyChild instanceof PropertyOrFieldReference) { |
||||
key = ((PropertyOrFieldReference) keyChild).getName(); |
||||
} |
||||
else { |
||||
return; |
||||
} |
||||
if (valueChild instanceof Literal) { |
||||
value = ((Literal) valueChild).getLiteralValue().getValue(); |
||||
} |
||||
else if (valueChild instanceof InlineList) { |
||||
value = ((InlineList) valueChild).getConstantValue(); |
||||
} |
||||
else if (valueChild instanceof InlineMap) { |
||||
value = ((InlineMap) valueChild).getConstantValue(); |
||||
} |
||||
constantMap.put(key, value); |
||||
} |
||||
this.constant = new TypedValue(Collections.unmodifiableMap(constantMap)); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public TypedValue getValueInternal(ExpressionState expressionState) throws EvaluationException { |
||||
if (this.constant != null) { |
||||
return this.constant; |
||||
} |
||||
else { |
||||
Map<Object, Object> returnValue = new LinkedHashMap<Object, Object>(); |
||||
int childcount = getChildCount(); |
||||
for (int c = 0; c < childcount; c++) { |
||||
// TODO allow for key being PropertyOrFieldReference like Indexer on maps
|
||||
SpelNode keyChild = getChild(c++); |
||||
Object key = null; |
||||
if (keyChild instanceof PropertyOrFieldReference) { |
||||
PropertyOrFieldReference reference = (PropertyOrFieldReference) keyChild; |
||||
key = reference.getName(); |
||||
} |
||||
else { |
||||
key = keyChild.getValue(expressionState); |
||||
} |
||||
Object value = getChild(c).getValue(expressionState); |
||||
returnValue.put(key, value); |
||||
} |
||||
return new TypedValue(returnValue); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public String toStringAST() { |
||||
StringBuilder s = new StringBuilder(); |
||||
s.append('{'); |
||||
int count = getChildCount(); |
||||
for (int c = 0; c < count; c++) { |
||||
if (c > 0) { |
||||
s.append(','); |
||||
} |
||||
s.append(getChild(c++).toStringAST()); |
||||
s.append(':'); |
||||
s.append(getChild(c).toStringAST()); |
||||
} |
||||
s.append('}'); |
||||
return s.toString(); |
||||
} |
||||
|
||||
/** |
||||
* @return whether this list is a constant value |
||||
*/ |
||||
public boolean isConstant() { |
||||
return this.constant != null; |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
public Map<Object,Object> getConstantValue() { |
||||
return (Map<Object,Object>) this.constant.getValue(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,138 @@
@@ -0,0 +1,138 @@
|
||||
/* |
||||
* Copyright 2014 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.expression.spel; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.LinkedHashMap; |
||||
|
||||
import org.junit.Test; |
||||
import org.springframework.expression.spel.ast.InlineMap; |
||||
import org.springframework.expression.spel.standard.SpelExpression; |
||||
import org.springframework.expression.spel.standard.SpelExpressionParser; |
||||
|
||||
import static org.junit.Assert.*; |
||||
|
||||
/** |
||||
* Test usage of inline maps. |
||||
* |
||||
* @author Andy Clement |
||||
* @since 4.1 |
||||
*/ |
||||
public class MapTests extends AbstractExpressionTests { |
||||
|
||||
// if the list is full of literals then it will be of the type unmodifiableClass
|
||||
// rather than HashMap (or similar)
|
||||
Class<?> unmodifiableClass = Collections.unmodifiableMap(new LinkedHashMap<Object,Object>()).getClass(); |
||||
|
||||
|
||||
@Test |
||||
public void testInlineMapCreation01() { |
||||
evaluate("{'a':1, 'b':2, 'c':3, 'd':4, 'e':5}", "{a=1, b=2, c=3, d=4, e=5}", unmodifiableClass); |
||||
evaluate("{'a':1}", "{a=1}", unmodifiableClass); |
||||
} |
||||
|
||||
@Test |
||||
public void testInlineMapCreation02() { |
||||
evaluate("{'abc':'def', 'uvw':'xyz'}", "{abc=def, uvw=xyz}", unmodifiableClass); |
||||
} |
||||
|
||||
@Test |
||||
public void testInlineMapCreation03() { |
||||
evaluate("{:}", "{}", unmodifiableClass); |
||||
} |
||||
|
||||
@Test |
||||
public void testInlineMapCreation04() { |
||||
evaluate("{'key':'abc'=='xyz'}", "{key=false}", LinkedHashMap.class); |
||||
evaluate("{key:'abc'=='xyz'}", "{key=false}", LinkedHashMap.class); |
||||
evaluate("{key:'abc'=='xyz',key2:true}[key]", "false", Boolean.class); |
||||
evaluate("{key:'abc'=='xyz',key2:true}.get('key2')", "true", Boolean.class); |
||||
evaluate("{key:'abc'=='xyz',key2:true}['key2']", "true", Boolean.class); |
||||
} |
||||
|
||||
@Test |
||||
public void testInlineMapAndNesting() { |
||||
evaluate("{a:{a:1,b:2,c:3},b:{d:4,e:5,f:6}}", "{a={a=1, b=2, c=3}, b={d=4, e=5, f=6}}", unmodifiableClass); |
||||
evaluate("{a:{x:1,y:'2',z:3},b:{u:4,v:{'a','b'},w:5,x:6}}", "{a={x=1, y=2, z=3}, b={u=4, v=[a, b], w=5, x=6}}", unmodifiableClass); |
||||
evaluate("{a:{1,2,3},b:{4,5,6}}", "{a=[1, 2, 3], b=[4, 5, 6]}", unmodifiableClass); |
||||
} |
||||
|
||||
@Test |
||||
public void testInlineMapWithFunkyKeys() { |
||||
evaluate("{#root.name:true}","{Nikola Tesla=true}",LinkedHashMap.class); |
||||
} |
||||
|
||||
@Test |
||||
public void testInlineMapError() { |
||||
parseAndCheckError("{key:'abc'", SpelMessage.OOD); |
||||
} |
||||
|
||||
@Test |
||||
public void testRelOperatorsIs02() { |
||||
evaluate("{a:1, b:2, c:3, d:4, e:5} instanceof T(java.util.Map)", "true", Boolean.class); |
||||
} |
||||
|
||||
@Test |
||||
public void testInlineMapAndProjectionSelection() { |
||||
evaluate("{a:1,b:2,c:3,d:4,e:5,f:6}.![value>3]", "[false, false, false, true, true, true]", ArrayList.class); |
||||
evaluate("{a:1,b:2,c:3,d:4,e:5,f:6}.?[value>3]", "{d=4, e=5, f=6}", HashMap.class); |
||||
evaluate("{a:1,b:2,c:3,d:4,e:5,f:6,g:7,h:8,i:9,j:10}.?[value%2==0]", "{b=2, d=4, f=6, h=8, j=10}", HashMap.class); |
||||
// TODO this looks like a serious issue (but not a new one): the context object against which arguments are evaluated seems wrong:
|
||||
// evaluate("{a:1,b:2,c:3,d:4,e:5,f:6,g:7,h:8,i:9,j:10}.?[isEven(value) == 'y']", "[2, 4, 6, 8, 10]", ArrayList.class);
|
||||
} |
||||
|
||||
@Test |
||||
public void testSetConstruction01() { |
||||
evaluate("new java.util.HashMap().putAll({a:'a',b:'b',c:'c'})", null, Object.class); |
||||
} |
||||
|
||||
@Test |
||||
public void testConstantRepresentation1() { |
||||
checkConstantMap("{f:{'a','b','c'}}", true); |
||||
checkConstantMap("{'a':1,'b':2,'c':3,'d':4,'e':5}", true); |
||||
checkConstantMap("{aaa:'abc'}", true); |
||||
checkConstantMap("{:}", true); |
||||
checkConstantMap("{a:#a,b:2,c:3}", false); |
||||
checkConstantMap("{a:1,b:2,c:Integer.valueOf(4)}", false); |
||||
checkConstantMap("{a:1,b:2,c:{#a}}", false); |
||||
checkConstantMap("{#root.name:true}",false); |
||||
checkConstantMap("{a:1,b:2,c:{d:true,e:false}}", true); |
||||
checkConstantMap("{a:1,b:2,c:{d:{1,2,3},e:{4,5,6},f:{'a','b','c'}}}", true); |
||||
} |
||||
|
||||
private void checkConstantMap(String expressionText, boolean expectedToBeConstant) { |
||||
SpelExpressionParser parser = new SpelExpressionParser(); |
||||
SpelExpression expression = (SpelExpression) parser.parseExpression(expressionText); |
||||
SpelNode node = expression.getAST(); |
||||
assertTrue(node instanceof InlineMap); |
||||
InlineMap inlineMap = (InlineMap) node; |
||||
if (expectedToBeConstant) { |
||||
assertTrue(inlineMap.isConstant()); |
||||
} |
||||
else { |
||||
assertFalse(inlineMap.isConstant()); |
||||
} |
||||
} |
||||
|
||||
@Test(expected = UnsupportedOperationException.class) |
||||
public void testInlineMapWriting() { |
||||
// list should be unmodifiable
|
||||
evaluate("{a:1, b:2, c:3, d:4, e:5}[a]=6", "[a:1,b: 2,c: 3,d: 4,e: 5]", unmodifiableClass); |
||||
} |
||||
} |
Loading…
Reference in new issue