提交 095bd999 编写于 作者: A Andy Clement

Add support for inline maps in SpEL expressions

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-9472
上级 c2993719
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-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.
......@@ -119,7 +119,7 @@ public class InlineList extends SpelNodeImpl {
}
@SuppressWarnings("unchecked")
private List<Object> getConstantValue() {
public List<Object> getConstantValue() {
return (List<Object>) this.constant.getValue();
}
......
/*
* 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();
}
}
......@@ -39,6 +39,7 @@ import org.springframework.expression.spel.ast.FunctionReference;
import org.springframework.expression.spel.ast.Identifier;
import org.springframework.expression.spel.ast.Indexer;
import org.springframework.expression.spel.ast.InlineList;
import org.springframework.expression.spel.ast.InlineMap;
import org.springframework.expression.spel.ast.Literal;
import org.springframework.expression.spel.ast.MethodReference;
import org.springframework.expression.spel.ast.NullLiteral;
......@@ -515,7 +516,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
|| maybeEatIndexer()) {
return pop();
}
else if (maybeEatInlineList()) {
else if (maybeEatInlineListOrMap()) {
return pop();
}
else {
......@@ -600,7 +601,8 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
}
// list = LCURLY (element (COMMA element)*) RCURLY
private boolean maybeEatInlineList() {
// map = LCURLY (key ':' value (COMMA key ':' value)*) RCURLY
private boolean maybeEatInlineListOrMap() {
Token t = peekToken();
if (!peekToken(TokenKind.LCURLY, true)) {
return false;
......@@ -608,18 +610,53 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
SpelNodeImpl expr = null;
Token closingCurly = peekToken();
if (peekToken(TokenKind.RCURLY, true)) {
// empty list '[]'
// empty list '{}'
expr = new InlineList(toPos(t.startpos,closingCurly.endpos));
}
else if (peekToken(TokenKind.COLON,true)) {
closingCurly = eatToken(TokenKind.RCURLY);
// empty map '{:}'
expr = new InlineMap(toPos(t.startpos,closingCurly.endpos));
}
else {
List<SpelNodeImpl> listElements = new ArrayList<SpelNodeImpl>();
do {
listElements.add(eatExpression());
SpelNodeImpl firstExpression = eatExpression();
// Next is either:
// '}' - end of list
// ',' - more expressions in this list
// ':' - this is a map!
if (peekToken(TokenKind.RCURLY)) { // list with one item in it
List<SpelNodeImpl> listElements = new ArrayList<SpelNodeImpl>();
listElements.add(firstExpression);
closingCurly = eatToken(TokenKind.RCURLY);
expr = new InlineList(toPos(t.startpos,closingCurly.endpos),listElements.toArray(new SpelNodeImpl[listElements.size()]));
}
else if (peekToken(TokenKind.COMMA, true)) { // multi item list
List<SpelNodeImpl> listElements = new ArrayList<SpelNodeImpl>();
listElements.add(firstExpression);
do {
listElements.add(eatExpression());
}
while (peekToken(TokenKind.COMMA,true));
closingCurly = eatToken(TokenKind.RCURLY);
expr = new InlineList(toPos(t.startpos,closingCurly.endpos),listElements.toArray(new SpelNodeImpl[listElements.size()]));
}
else if (peekToken(TokenKind.COLON, true)) { // map!
List<SpelNodeImpl> mapElements = new ArrayList<SpelNodeImpl>();
mapElements.add(firstExpression);
mapElements.add(eatExpression());
while (peekToken(TokenKind.COMMA,true)) {
mapElements.add(eatExpression());
eatToken(TokenKind.COLON);
mapElements.add(eatExpression());
}
closingCurly = eatToken(TokenKind.RCURLY);
expr = new InlineMap(toPos(t.startpos,closingCurly.endpos),mapElements.toArray(new SpelNodeImpl[mapElements.size()]));
}
else {
raiseInternalException(t.startpos, SpelMessage.OOD);
}
while (peekToken(TokenKind.COMMA,true));
closingCurly = eatToken(TokenKind.RCURLY);
expr = new InlineList(toPos(t.startpos,closingCurly.endpos),listElements.toArray(new SpelNodeImpl[listElements.size()]));
}
this.constructedNodes.push(expr);
return true;
......@@ -734,7 +771,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
}
eatToken(TokenKind.RSQUARE);
}
if (maybeEatInlineList()) {
if (maybeEatInlineListOrMap()) {
nodes.add(pop());
}
push(new ConstructorReference(toPos(newToken), dimensions.toArray(new SpelNodeImpl[dimensions.size()]),
......
......@@ -1401,6 +1401,11 @@ public class EvaluationTests extends AbstractExpressionTests {
expectFailNotAssignable(parser, ctx, "--({1,2,3})");
expectFailSetValueNotSupported(parser, ctx, "({1,2,3})=({1,2,3})");
// InlineMap
expectFailNotAssignable(parser, ctx, "({'a':1,'b':2,'c':3})++");
expectFailNotAssignable(parser, ctx, "--({'a':1,'b':2,'c':3})");
expectFailSetValueNotSupported(parser, ctx, "({'a':1,'b':2,'c':3})=({'a':1,'b':2,'c':3})");
// BeanReference
ctx.setBeanResolver(new MyBeanResolver());
expectFailNotAssignable(parser, ctx, "@foo++");
......
......@@ -93,7 +93,6 @@ public class MapAccessTests extends AbstractExpressionTests {
public void testGetValueFromRootMap() {
Map<String, String> map = new HashMap<String, String>();
map.put("key", "value");
EvaluationContext context = new StandardEvaluationContext(map);
ExpressionParser spelExpressionParser = new SpelExpressionParser();
Expression expr = spelExpressionParser.parseExpression("#root['key']");
......@@ -168,11 +167,11 @@ public class MapAccessTests extends AbstractExpressionTests {
this.priority = priority;
}
public Map getProperties() {
public Map<String,String> getProperties() {
return properties;
}
public void setProperties(Map properties) {
public void setProperties(Map<String,String> properties) {
this.properties = properties;
}
}
......@@ -198,7 +197,7 @@ public class MapAccessTests extends AbstractExpressionTests {
@Override
@SuppressWarnings("unchecked")
public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException {
((Map) target).put(name, newValue);
((Map<Object,Object>) target).put(name, newValue);
}
@Override
......
/*
* 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);
}
}
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-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.
......@@ -298,30 +298,26 @@ public class ParsingTests {
}
// inline list creation
// public void testInlineListCreation01() {
// parseCheck("{1, 2, 3, 4, 5}", "{1,2,3,4,5}");
// }
//
// public void testInlineListCreation02() {
// parseCheck("{'abc','xyz'}", "{'abc','xyz'}");
// }
@Test
public void testInlineListCreation01() {
parseCheck("{1, 2, 3, 4, 5}", "{1,2,3,4,5}");
}
// // inline map creation
// public void testInlineMapCreation01() {
// parseCheck("#{'key1':'Value 1', 'today':DateTime.Today}");
// }
//
// public void testInlineMapCreation02() {
// parseCheck("#{1:'January', 2:'February', 3:'March'}");
// }
//
// public void testInlineMapCreation03() {
// parseCheck("#{'key1':'Value 1', 'today':'Monday'}['key1']");
// }
//
// public void testInlineMapCreation04() {
// parseCheck("#{1:'January', 2:'February', 3:'March'}[3]");
// }
@Test
public void testInlineListCreation02() {
parseCheck("{'abc','xyz'}", "{'abc','xyz'}");
}
// inline map creation
@Test
public void testInlineMapCreation01() {
parseCheck("{'key1':'Value 1','today':DateTime.Today}");
}
@Test
public void testInlineMapCreation02() {
parseCheck("{1:'January',2:'February',3:'March'}");
}
// methods
@Test
......
......@@ -11058,6 +11058,7 @@ The expression language supports the following functionality
* Bean references
* Array construction
* Inline lists
* Inline maps
* Ternary operator
* Variables
* User defined functions
......@@ -11562,7 +11563,22 @@ Lists can be expressed directly in an expression using `{}` notation.
entirely composed of fixed literals then a constant list is created to represent the
expression, rather than building a new list on each evaluation.
[[expressions-inline-maps]]
==== Inline Maps
Maps can also be expressed directly in an expression using `{key:value}` notation.
[source,java,indent=0]
[subs="verbatim,quotes"]
----
// evaluates to a Java map containing the two entries
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);
Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);
----
`{:}` by itself means an empty map. For performance reasons, if the map is itself composed
of fixed literals or other nested constant structures (lists or maps) then a constant map is created
to represent the expression, rather than building a new map on each evaluation. Quoting of the map keys
is optional, the examples above are not using quoted keys.
[[expressions-array-construction]]
==== Array construction
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册