Skip to content

Commit 095bd99

Browse files
committed
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
1 parent c299371 commit 095bd99

File tree

8 files changed

+394
-41
lines changed

8 files changed

+394
-41
lines changed

spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -119,7 +119,7 @@ public boolean isConstant() {
119119
}
120120

121121
@SuppressWarnings("unchecked")
122-
private List<Object> getConstantValue() {
122+
public List<Object> getConstantValue() {
123123
return (List<Object>) this.constant.getValue();
124124
}
125125

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.expression.spel.ast;
18+
19+
import java.util.Collections;
20+
import java.util.LinkedHashMap;
21+
import java.util.Map;
22+
23+
import org.springframework.expression.EvaluationException;
24+
import org.springframework.expression.TypedValue;
25+
import org.springframework.expression.spel.ExpressionState;
26+
import org.springframework.expression.spel.SpelNode;
27+
28+
/**
29+
* Represent a map in an expression, e.g. '{name:'foo',age:12}'
30+
*
31+
* @author Andy Clement
32+
* @since 4.1
33+
*/
34+
public class InlineMap extends SpelNodeImpl {
35+
36+
// if the map is purely literals, it is a constant value and can be computed and cached
37+
TypedValue constant = null;
38+
39+
public InlineMap(int pos, SpelNodeImpl... args) {
40+
super(pos, args);
41+
checkIfConstant();
42+
}
43+
44+
45+
/**
46+
* If all the components of the list are constants, or lists/maps that themselves contain constants, then a constant list
47+
* can be built to represent this node. This will speed up later getValue calls and reduce the amount of garbage
48+
* created.
49+
*/
50+
private void checkIfConstant() {
51+
boolean isConstant = true;
52+
for (int c = 0, max = getChildCount(); c < max; c++) {
53+
SpelNode child = getChild(c);
54+
if (!(child instanceof Literal)) {
55+
if (child instanceof InlineList) {
56+
InlineList inlineList = (InlineList) child;
57+
if (!inlineList.isConstant()) {
58+
isConstant = false;
59+
break;
60+
}
61+
}
62+
else if (child instanceof InlineMap) {
63+
InlineMap inlineMap = (InlineMap) child;
64+
if (!inlineMap.isConstant()) {
65+
isConstant = false;
66+
break;
67+
}
68+
}
69+
else if (!((c%2)==0 && (child instanceof PropertyOrFieldReference))) {
70+
isConstant = false;
71+
break;
72+
}
73+
}
74+
}
75+
if (isConstant) {
76+
Map<Object,Object> constantMap = new LinkedHashMap<Object,Object>();
77+
int childcount = getChildCount();
78+
for (int c = 0; c < childcount; c++) {
79+
SpelNode keyChild = getChild(c++);
80+
SpelNode valueChild = getChild(c);
81+
Object key = null;
82+
Object value = null;
83+
if ((keyChild instanceof Literal)) {
84+
key = ((Literal) keyChild).getLiteralValue().getValue();
85+
}
86+
else if (keyChild instanceof PropertyOrFieldReference) {
87+
key = ((PropertyOrFieldReference) keyChild).getName();
88+
}
89+
else {
90+
return;
91+
}
92+
if (valueChild instanceof Literal) {
93+
value = ((Literal) valueChild).getLiteralValue().getValue();
94+
}
95+
else if (valueChild instanceof InlineList) {
96+
value = ((InlineList) valueChild).getConstantValue();
97+
}
98+
else if (valueChild instanceof InlineMap) {
99+
value = ((InlineMap) valueChild).getConstantValue();
100+
}
101+
constantMap.put(key, value);
102+
}
103+
this.constant = new TypedValue(Collections.unmodifiableMap(constantMap));
104+
}
105+
}
106+
107+
@Override
108+
public TypedValue getValueInternal(ExpressionState expressionState) throws EvaluationException {
109+
if (this.constant != null) {
110+
return this.constant;
111+
}
112+
else {
113+
Map<Object, Object> returnValue = new LinkedHashMap<Object, Object>();
114+
int childcount = getChildCount();
115+
for (int c = 0; c < childcount; c++) {
116+
// TODO allow for key being PropertyOrFieldReference like Indexer on maps
117+
SpelNode keyChild = getChild(c++);
118+
Object key = null;
119+
if (keyChild instanceof PropertyOrFieldReference) {
120+
PropertyOrFieldReference reference = (PropertyOrFieldReference) keyChild;
121+
key = reference.getName();
122+
}
123+
else {
124+
key = keyChild.getValue(expressionState);
125+
}
126+
Object value = getChild(c).getValue(expressionState);
127+
returnValue.put(key, value);
128+
}
129+
return new TypedValue(returnValue);
130+
}
131+
}
132+
133+
@Override
134+
public String toStringAST() {
135+
StringBuilder s = new StringBuilder();
136+
s.append('{');
137+
int count = getChildCount();
138+
for (int c = 0; c < count; c++) {
139+
if (c > 0) {
140+
s.append(',');
141+
}
142+
s.append(getChild(c++).toStringAST());
143+
s.append(':');
144+
s.append(getChild(c).toStringAST());
145+
}
146+
s.append('}');
147+
return s.toString();
148+
}
149+
150+
/**
151+
* @return whether this list is a constant value
152+
*/
153+
public boolean isConstant() {
154+
return this.constant != null;
155+
}
156+
157+
@SuppressWarnings("unchecked")
158+
public Map<Object,Object> getConstantValue() {
159+
return (Map<Object,Object>) this.constant.getValue();
160+
}
161+
162+
}

spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.expression.spel.ast.Identifier;
4040
import org.springframework.expression.spel.ast.Indexer;
4141
import org.springframework.expression.spel.ast.InlineList;
42+
import org.springframework.expression.spel.ast.InlineMap;
4243
import org.springframework.expression.spel.ast.Literal;
4344
import org.springframework.expression.spel.ast.MethodReference;
4445
import org.springframework.expression.spel.ast.NullLiteral;
@@ -515,7 +516,7 @@ else if (maybeEatProjection(false) || maybeEatSelection(false)
515516
|| maybeEatIndexer()) {
516517
return pop();
517518
}
518-
else if (maybeEatInlineList()) {
519+
else if (maybeEatInlineListOrMap()) {
519520
return pop();
520521
}
521522
else {
@@ -600,26 +601,62 @@ private boolean maybeEatProjection(boolean nullSafeNavigation) {
600601
}
601602

602603
// list = LCURLY (element (COMMA element)*) RCURLY
603-
private boolean maybeEatInlineList() {
604+
// map = LCURLY (key ':' value (COMMA key ':' value)*) RCURLY
605+
private boolean maybeEatInlineListOrMap() {
604606
Token t = peekToken();
605607
if (!peekToken(TokenKind.LCURLY, true)) {
606608
return false;
607609
}
608610
SpelNodeImpl expr = null;
609611
Token closingCurly = peekToken();
610612
if (peekToken(TokenKind.RCURLY, true)) {
611-
// empty list '[]'
613+
// empty list '{}'
612614
expr = new InlineList(toPos(t.startpos,closingCurly.endpos));
613615
}
616+
else if (peekToken(TokenKind.COLON,true)) {
617+
closingCurly = eatToken(TokenKind.RCURLY);
618+
// empty map '{:}'
619+
expr = new InlineMap(toPos(t.startpos,closingCurly.endpos));
620+
}
614621
else {
615-
List<SpelNodeImpl> listElements = new ArrayList<SpelNodeImpl>();
616-
do {
617-
listElements.add(eatExpression());
622+
SpelNodeImpl firstExpression = eatExpression();
623+
// Next is either:
624+
// '}' - end of list
625+
// ',' - more expressions in this list
626+
// ':' - this is a map!
627+
628+
if (peekToken(TokenKind.RCURLY)) { // list with one item in it
629+
List<SpelNodeImpl> listElements = new ArrayList<SpelNodeImpl>();
630+
listElements.add(firstExpression);
631+
closingCurly = eatToken(TokenKind.RCURLY);
632+
expr = new InlineList(toPos(t.startpos,closingCurly.endpos),listElements.toArray(new SpelNodeImpl[listElements.size()]));
633+
}
634+
else if (peekToken(TokenKind.COMMA, true)) { // multi item list
635+
List<SpelNodeImpl> listElements = new ArrayList<SpelNodeImpl>();
636+
listElements.add(firstExpression);
637+
do {
638+
listElements.add(eatExpression());
639+
}
640+
while (peekToken(TokenKind.COMMA,true));
641+
closingCurly = eatToken(TokenKind.RCURLY);
642+
expr = new InlineList(toPos(t.startpos,closingCurly.endpos),listElements.toArray(new SpelNodeImpl[listElements.size()]));
643+
644+
}
645+
else if (peekToken(TokenKind.COLON, true)) { // map!
646+
List<SpelNodeImpl> mapElements = new ArrayList<SpelNodeImpl>();
647+
mapElements.add(firstExpression);
648+
mapElements.add(eatExpression());
649+
while (peekToken(TokenKind.COMMA,true)) {
650+
mapElements.add(eatExpression());
651+
eatToken(TokenKind.COLON);
652+
mapElements.add(eatExpression());
653+
}
654+
closingCurly = eatToken(TokenKind.RCURLY);
655+
expr = new InlineMap(toPos(t.startpos,closingCurly.endpos),mapElements.toArray(new SpelNodeImpl[mapElements.size()]));
656+
}
657+
else {
658+
raiseInternalException(t.startpos, SpelMessage.OOD);
618659
}
619-
while (peekToken(TokenKind.COMMA,true));
620-
621-
closingCurly = eatToken(TokenKind.RCURLY);
622-
expr = new InlineList(toPos(t.startpos,closingCurly.endpos),listElements.toArray(new SpelNodeImpl[listElements.size()]));
623660
}
624661
this.constructedNodes.push(expr);
625662
return true;
@@ -734,7 +771,7 @@ private boolean maybeEatConstructorReference() {
734771
}
735772
eatToken(TokenKind.RSQUARE);
736773
}
737-
if (maybeEatInlineList()) {
774+
if (maybeEatInlineListOrMap()) {
738775
nodes.add(pop());
739776
}
740777
push(new ConstructorReference(toPos(newToken), dimensions.toArray(new SpelNodeImpl[dimensions.size()]),

spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,11 @@ public void incrementAllNodeTypes() throws SecurityException, NoSuchMethodExcept
14011401
expectFailNotAssignable(parser, ctx, "--({1,2,3})");
14021402
expectFailSetValueNotSupported(parser, ctx, "({1,2,3})=({1,2,3})");
14031403

1404+
// InlineMap
1405+
expectFailNotAssignable(parser, ctx, "({'a':1,'b':2,'c':3})++");
1406+
expectFailNotAssignable(parser, ctx, "--({'a':1,'b':2,'c':3})");
1407+
expectFailSetValueNotSupported(parser, ctx, "({'a':1,'b':2,'c':3})=({'a':1,'b':2,'c':3})");
1408+
14041409
// BeanReference
14051410
ctx.setBeanResolver(new MyBeanResolver());
14061411
expectFailNotAssignable(parser, ctx, "@foo++");

spring-expression/src/test/java/org/springframework/expression/spel/MapAccessTests.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ public void testGetValue(){
9393
public void testGetValueFromRootMap() {
9494
Map<String, String> map = new HashMap<String, String>();
9595
map.put("key", "value");
96-
EvaluationContext context = new StandardEvaluationContext(map);
9796

9897
ExpressionParser spelExpressionParser = new SpelExpressionParser();
9998
Expression expr = spelExpressionParser.parseExpression("#root['key']");
@@ -168,11 +167,11 @@ public void setPriority(Integer priority) {
168167
this.priority = priority;
169168
}
170169

171-
public Map getProperties() {
170+
public Map<String,String> getProperties() {
172171
return properties;
173172
}
174173

175-
public void setProperties(Map properties) {
174+
public void setProperties(Map<String,String> properties) {
176175
this.properties = properties;
177176
}
178177
}
@@ -198,7 +197,7 @@ public boolean canWrite(EvaluationContext context, Object target, String name) t
198197
@Override
199198
@SuppressWarnings("unchecked")
200199
public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException {
201-
((Map) target).put(name, newValue);
200+
((Map<Object,Object>) target).put(name, newValue);
202201
}
203202

204203
@Override

0 commit comments

Comments
 (0)