diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java index cae9002f40..14afd4d16a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java @@ -1,5 +1,5 @@ /* - * 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 getConstantValue() { + public List getConstantValue() { return (List) this.constant.getValue(); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java new file mode 100644 index 0000000000..7b22ec822e --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java @@ -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 constantMap = new LinkedHashMap(); + 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 returnValue = new LinkedHashMap(); + 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 getConstantValue() { + return (Map) this.constant.getValue(); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index 5a4e841072..90a3155216 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -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 { - List listElements = new ArrayList(); - do { - listElements.add(eatExpression()); - } - while (peekToken(TokenKind.COMMA,true)); - + else if (peekToken(TokenKind.COLON,true)) { closingCurly = eatToken(TokenKind.RCURLY); - expr = new InlineList(toPos(t.startpos,closingCurly.endpos),listElements.toArray(new SpelNodeImpl[listElements.size()])); + // empty map '{:}' + expr = new InlineMap(toPos(t.startpos,closingCurly.endpos)); + } + else { + 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 listElements = new ArrayList(); + 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 listElements = new ArrayList(); + 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 mapElements = new ArrayList(); + 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); + } } 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()]), diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java index f6b2c0f7ca..c64c0ee1ba 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java @@ -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++"); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/MapAccessTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/MapAccessTests.java index 60b3550177..1839a7e705 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/MapAccessTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/MapAccessTests.java @@ -93,7 +93,6 @@ public class MapAccessTests extends AbstractExpressionTests { public void testGetValueFromRootMap() { Map map = new HashMap(); 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 getProperties() { return properties; } - public void setProperties(Map properties) { + public void setProperties(Map 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) target).put(name, newValue); } @Override diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/MapTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/MapTests.java new file mode 100644 index 0000000000..bb735480d0 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/MapTests.java @@ -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()).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); + } +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java index ff92b50476..fa2a7d04e0 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java @@ -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 diff --git a/src/asciidoc/index.adoc b/src/asciidoc/index.adoc index 2f64041b1d..b70a7ba9aa 100644 --- a/src/asciidoc/index.adoc +++ b/src/asciidoc/index.adoc @@ -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