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
This commit is contained in:
Andy Clement
2014-08-06 11:12:32 -07:00
parent c29937196f
commit 095bd99951
8 changed files with 395 additions and 42 deletions

View File

@@ -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++");

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -1,5 +1,5 @@
/*
* 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