diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java b/org.springframework.expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java index bd09dc56e12..112c8ffc2eb 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java @@ -1 +1 @@ -/* * Copyright 2002-2009 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.common; import java.util.LinkedList; import java.util.List; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParseException; import org.springframework.expression.ParserContext; /** * An expression parser that understands templates. It can be subclassed * by expression parsers that do not offer first class support for templating. * * @author Keith Donald * @author Juergen Hoeller * @author Andy Clement * @since 3.0 */ public abstract class TemplateAwareExpressionParser implements ExpressionParser { /** * Default ParserContext instance for non-template expressions. */ private static final ParserContext NON_TEMPLATE_PARSER_CONTEXT = new ParserContext() { public String getExpressionPrefix() { return null; } public String getExpressionSuffix() { return null; } public boolean isTemplate() { return false; } }; public Expression parseExpression(String expressionString) throws ParseException { return parseExpression(expressionString, NON_TEMPLATE_PARSER_CONTEXT); } public Expression parseExpression(String expressionString, ParserContext context) throws ParseException { if (context == null) { context = NON_TEMPLATE_PARSER_CONTEXT; } if (context.isTemplate()) { return parseTemplate(expressionString, context); } else { return doParseExpression(expressionString, context); } } private Expression parseTemplate(String expressionString, ParserContext context) throws ParseException { if (expressionString.length() == 0) { return new LiteralExpression(""); } Expression[] expressions = parseExpressions(expressionString, context); if (expressions.length == 1) { return expressions[0]; } else { return new CompositeStringExpression(expressionString, expressions); } } /** * Helper that parses given expression string using the configured parser. The expression string can contain any * number of expressions all contained in "${...}" markers. For instance: "foo${expr0}bar${expr1}". The static * pieces of text will also be returned as Expressions that just return that static piece of text. As a result, * evaluating all returned expressions and concatenating the results produces the complete evaluated string. * Unwrapping is only done of the outermost delimiters found, so the string 'hello ${foo${abc}}' would break into * the pieces 'hello ' and 'foo${abc}'. This means that expression languages that used ${..} as part of their * functionality are supported without any problem * @param expressionString the expression string * @return the parsed expressions * @throws ParseException when the expressions cannot be parsed */ private Expression[] parseExpressions(String expressionString, ParserContext context) throws ParseException { List expressions = new LinkedList(); int startIdx = 0; String prefix = context.getExpressionPrefix(); String suffix = context.getExpressionSuffix(); while (startIdx < expressionString.length()) { int prefixIndex = expressionString.indexOf(prefix, startIdx); if (prefixIndex >= startIdx) { // a inner expression was found - this is a composite if (prefixIndex > startIdx) { expressions.add(new LiteralExpression(expressionString.substring(startIdx, prefixIndex))); } int afterPrefixIndex = prefixIndex + prefix.length(); int suffixIndex = expressionString.indexOf(suffix, afterPrefixIndex); int nextPrefixIndex = expressionString.indexOf(prefix, afterPrefixIndex); if (suffixIndex == -1) { throw new ParseException(expressionString, "No ending suffix '" + suffix + "' for expression starting at character " + prefixIndex + ": " + expressionString.substring(prefixIndex)); } if (nextPrefixIndex != -1 && suffixIndex > nextPrefixIndex) { throw new ParseException(expressionString, "No ending suffix '" + suffix + "' for expression starting at character " + prefixIndex + ": " + expressionString.substring(prefixIndex)); } else if (suffixIndex == afterPrefixIndex) { throw new ParseException(expressionString, "No expression defined within delimiter '" + prefix + suffix + "' at character " + prefixIndex); } else { String expr = expressionString.substring(prefixIndex + prefix.length(), suffixIndex); expressions.add(doParseExpression(expr, context)); startIdx = suffixIndex + suffix.length(); } } else { // no more ${expressions} found in string, add rest as static text expressions.add(new LiteralExpression(expressionString.substring(startIdx))); startIdx = expressionString.length(); } } return expressions.toArray(new Expression[expressions.size()]); } /** * Actually parse the expression string and return an Expression object. * @param expressionString the raw expression string to parse * @param context a context for influencing this expression parsing routine (optional) * @return an evaluator for the parsed expression * @throws ParseException an exception occurred during parsing */ protected abstract Expression doParseExpression(String expressionString, ParserContext context) throws ParseException; } \ No newline at end of file +/* * Copyright 2002-2009 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.common; import java.util.LinkedList; import java.util.List; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParseException; import org.springframework.expression.ParserContext; /** * An expression parser that understands templates. It can be subclassed * by expression parsers that do not offer first class support for templating. * * @author Keith Donald * @author Juergen Hoeller * @author Andy Clement * @since 3.0 */ public abstract class TemplateAwareExpressionParser implements ExpressionParser { /** * Default ParserContext instance for non-template expressions. */ private static final ParserContext NON_TEMPLATE_PARSER_CONTEXT = new ParserContext() { public String getExpressionPrefix() { return null; } public String getExpressionSuffix() { return null; } public boolean isTemplate() { return false; } }; public Expression parseExpression(String expressionString) throws ParseException { return parseExpression(expressionString, NON_TEMPLATE_PARSER_CONTEXT); } public Expression parseExpression(String expressionString, ParserContext context) throws ParseException { if (context == null) { context = NON_TEMPLATE_PARSER_CONTEXT; } if (context.isTemplate()) { return parseTemplate(expressionString, context); } else { return doParseExpression(expressionString, context); } } private Expression parseTemplate(String expressionString, ParserContext context) throws ParseException { if (expressionString.length() == 0) { return new LiteralExpression(""); } Expression[] expressions = parseExpressions(expressionString, context); if (expressions.length == 1) { return expressions[0]; } else { return new CompositeStringExpression(expressionString, expressions); } } /** * Helper that parses given expression string using the configured parser. The expression string can contain any * number of expressions all contained in "${...}" markers. For instance: "foo${expr0}bar${expr1}". The static * pieces of text will also be returned as Expressions that just return that static piece of text. As a result, * evaluating all returned expressions and concatenating the results produces the complete evaluated string. * Unwrapping is only done of the outermost delimiters found, so the string 'hello ${foo${abc}}' would break into * the pieces 'hello ' and 'foo${abc}'. This means that expression languages that used ${..} as part of their * functionality are supported without any problem * @param expressionString the expression string * @return the parsed expressions * @throws ParseException when the expressions cannot be parsed */ private Expression[] parseExpressions(String expressionString, ParserContext context) throws ParseException { List expressions = new LinkedList(); int startIdx = 0; String prefix = context.getExpressionPrefix(); String suffix = context.getExpressionSuffix(); while (startIdx < expressionString.length()) { int prefixIndex = findUnescapedOccurenceOf(prefix,expressionString,startIdx); if (prefixIndex >= startIdx) { // an inner expression was found - this is a composite if (prefixIndex > startIdx) { expressions.add(createLiteralExpression(context,expressionString.substring(startIdx, prefixIndex))); } int afterPrefixIndex = prefixIndex + prefix.length(); int suffixIndex = skipToCorrectEndSuffix(prefix,suffix,expressionString,afterPrefixIndex); if (suffixIndex == -1) { throw new ParseException(expressionString, "No ending suffix '" + suffix + "' for expression starting at character " + prefixIndex + ": " + expressionString.substring(prefixIndex)); } if (suffixIndex == afterPrefixIndex) { throw new ParseException(expressionString, "No expression defined within delimiter '" + prefix + suffix + "' at character " + prefixIndex); } else { String expr = expressionString.substring(prefixIndex + prefix.length(), suffixIndex); expressions.add(doParseExpression(unescape(context,expr), context)); startIdx = suffixIndex + suffix.length(); } } else { // no more ${expressions} found in string, add rest as static text expressions.add(createLiteralExpression(context,expressionString.substring(startIdx))); startIdx = expressionString.length(); } } return expressions.toArray(new Expression[expressions.size()]); } private Expression createLiteralExpression(ParserContext context, String text) { return new LiteralExpression(unescape(context,text)); } /** * Replace any escaped versions of the template prefix/suffix. This will reduce, for example, the string * 'hello\${ world' to 'hello${ world' - the escaping was only in the original to ensure the ${ was not * treated as a real prefix. */ private String unescape(ParserContext context, String text) { String prefix = context.getExpressionPrefix(); String suffix = context.getExpressionSuffix(); String escapedPrefixesFixedUp = text.replace("\\"+prefix, prefix); String escapedSuffixesFixedUp = escapedPrefixesFixedUp.replace("\\"+suffix, suffix); return escapedSuffixesFixedUp; } /** * Find the next unescaped occurence of a particular string within an expression string. An escaped version of the string * is preceeded by a backslash. */ private int findUnescapedOccurenceOf(String toSearchFor, String expressionString, int startIdx) { int nextPossibility = -1; boolean foundOne = false; while (!foundOne) { nextPossibility = expressionString.indexOf(toSearchFor, startIdx); if (nextPossibility==-1) { return -1; } else { if (nextPossibility>0) { // check for the escape character if (expressionString.charAt(nextPossibility-1)!='\\') { // it was escaped, do not treat this as a real prefix foundOne = true; } else { startIdx = nextPossibility+toSearchFor.length(); } } else { foundOne = true; } } } return nextPossibility; } /** * Copes with nesting, for example '${...${...}}' where the correct end for the first ${ is the final }. * * @param prefix the prefix * @param suffix the suffix * @param expressionString the expression string * @param afterPrefixIndex the most recently found prefix location for which the matching end suffix is being sought * @return the position of the correct matching nextSuffix or -1 if none can be found */ private int skipToCorrectEndSuffix(String prefix, String suffix, String expressionString, int afterPrefixIndex) { int nextSuffix = findUnescapedOccurenceOf(suffix,expressionString,afterPrefixIndex); int nextPrefix = findUnescapedOccurenceOf(prefix,expressionString,afterPrefixIndex); if (nextPrefix==-1 || nextPrefix>nextSuffix) { return nextSuffix; } else { int depth = 0; while (nextPrefix!=-1 && nextPrefix0) { depth--; nextSuffix = findUnescapedOccurenceOf(suffix, expressionString, nextSuffix+suffix.length()); if (nextSuffix==-1) { return -1; // error case, not enough matching suffixes for prefixes } } return nextSuffix; } } /** * Actually parse the expression string and return an Expression object. * @param expressionString the raw expression string to parse * @param context a context for influencing this expression parsing routine (optional) * @return an evaluator for the parsed expression * @throws ParseException an exception occurred during parsing */ protected abstract Expression doParseExpression(String expressionString, ParserContext context) throws ParseException; } \ No newline at end of file diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Selection.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Selection.java index 59931f49f34..b5be715cacc 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Selection.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Selection.java @@ -123,7 +123,7 @@ public class Selection extends SpelNodeImpl { } } if ((variant == FIRST || variant == LAST) && result.size() == 0) { - return null; + return TypedValue.NULL_TYPED_VALUE; } if (variant == LAST) { return new TypedValue(result.get(result.size() - 1),TypeDescriptor.valueOf(op.getTypeDescriptor().getElementType())); diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/InProgressTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/InProgressTests.java index 2ff063f89f6..1719eaf9608 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/InProgressTests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/InProgressTests.java @@ -352,6 +352,7 @@ public class InProgressTests extends ExpressionTestCase { public void testSelection03() { evaluate("mapOfNumbersUpToTen.?{key>5}.size()", "5", Integer.class); +// evaluate("listOfNumbersUpToTen.?{#this>5}", "5", ArrayList.class); } public void testSelection04() { @@ -386,10 +387,4 @@ public class InProgressTests extends ExpressionTestCase { assertFalse(expr.isWritable(new StandardEvaluationContext())); } - - - - - - } diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/SpringEL300Tests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/SpringEL300Tests.java index e7275e22995..c2687f37d9c 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/SpringEL300Tests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/SpringEL300Tests.java @@ -16,6 +16,9 @@ package org.springframework.expression.spel; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.antlr.SpelAntlrExpressionParser; + /** * Tests based on Jiras up to the release of Spring 3.0.0 * @@ -26,5 +29,14 @@ public class SpringEL300Tests extends ExpressionTestCase { public void testNPE_5661() { evaluate("joinThreeStrings('a',null,'c')", "anullc", String.class); } + + public void testNPE_5673() throws Exception { + SpelAntlrExpressionParser parser = new SpelAntlrExpressionParser(); + Expression ex = parser.parseExpression("#{'Unable to render embedded object: File ({#this == 2\\}'}", TemplateExpressionParsingTests.HASH_DELIMITED_PARSER_CONTEXT); + assertEquals("Unable to render embedded object: File ({#this == 2}",ex.getValue()); +// ex = parser.parseExpression("Unable to render embedded object: File (#{#this}) not found", TemplateExpressionParsingTests.HASH_DELIMITED_PARSER_CONTEXT); +// assertEquals() +// System.out.println(ex.getValue(new StandardEvaluationContext(new File("C:/temp")))); + } } diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java index 2b3fe300902..7e2a6bd02b2 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java @@ -41,6 +41,17 @@ public class TemplateExpressionParsingTests extends ExpressionTestCase { } }; + public static final ParserContext HASH_DELIMITED_PARSER_CONTEXT = new ParserContext() { + public String getExpressionPrefix() { + return "#{"; + } + public String getExpressionSuffix() { + return "}"; + } + public boolean isTemplate() { + return true; + } + }; public void testParsingSimpleTemplateExpression01() throws Exception { SpelAntlrExpressionParser parser = new SpelAntlrExpressionParser(); @@ -91,6 +102,52 @@ public class TemplateExpressionParsingTests extends ExpressionTestCase { assertFalse(ex.isWritable(new StandardEvaluationContext())); } + public void testNestedExpressions() throws Exception { + SpelAntlrExpressionParser parser = new SpelAntlrExpressionParser(); + // treat the nested ${..} as a part of the expression + Expression ex = parser.parseExpression("hello ${listOfNumbersUpToTen.${#this<5}} world",DEFAULT_TEMPLATE_PARSER_CONTEXT); + String s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); + assertEquals("hello 4 world",s); + + // not a useful expression but tests nested expression syntax that clashes with template prefix/suffix + ex = parser.parseExpression("hello ${listOfNumbersUpToTen.${#root.listOfNumbersUpToTen.${#this%2==1}==3}} world",DEFAULT_TEMPLATE_PARSER_CONTEXT); + s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); + assertEquals("hello world",s); + + ex = parser.parseExpression("hello ${listOfNumbersUpToTen.${#this<5}} ${listOfNumbersUpToTen.${#this>5}} world",DEFAULT_TEMPLATE_PARSER_CONTEXT); + s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); + assertEquals("hello 4 10 world",s); + + try { + ex = parser.parseExpression("hello ${listOfNumbersUpToTen.${#this<5}} ${listOfNumbersUpToTen.${#this>5} world",DEFAULT_TEMPLATE_PARSER_CONTEXT); + fail("Should have failed"); + } catch (ParseException pe) { + assertEquals("No ending suffix '}' for expression starting at character 41: ${listOfNumbersUpToTen.${#this>5} world",pe.getMessage()); + } + + try { + ex = parser.parseExpression("hello ${listOfNumbersUpToTen.${#root.listOfNumbersUpToTen.${#this%2==1==3}} world",DEFAULT_TEMPLATE_PARSER_CONTEXT); + fail("Should have failed"); + } catch (ParseException pe) { + assertEquals("No ending suffix '}' for expression starting at character 6: ${listOfNumbersUpToTen.${#root.listOfNumbersUpToTen.${#this%2==1==3}} world",pe.getMessage()); + } + } + + public void testClashingWithSuffixes() throws Exception { + // Just wanting to use the prefix or suffix within the template: + Expression ex = parser.parseExpression("hello ${3+4} world",DEFAULT_TEMPLATE_PARSER_CONTEXT); + String s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); + assertEquals("hello 7 world",s); + + ex = parser.parseExpression("hello ${3+4} wo\\${rld",DEFAULT_TEMPLATE_PARSER_CONTEXT); + s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); + assertEquals("hello 7 wo${rld",s); + + ex = parser.parseExpression("hello ${3+4} wo\\}rld",DEFAULT_TEMPLATE_PARSER_CONTEXT); + s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); + assertEquals("hello 7 wo}rld",s); + } + public void testParsingNormalExpressionThroughTemplateParser() throws Exception { Expression expr = parser.parseExpression("1+2+3"); assertEquals(6,expr.getValue()); @@ -117,10 +174,12 @@ public class TemplateExpressionParsingTests extends ExpressionTestCase { fail("Should have failed"); } catch (ParseException pe) { assertEquals("No expression defined within delimiter '${}' at character 6",pe.getMessage()); - } - + } } + + // --- + private void checkString(String expectedString, Object value) { if (!(value instanceof String)) { fail("Result was not a string, it was of type " + value.getClass() + " (value=" + value + ")"); @@ -130,15 +189,4 @@ public class TemplateExpressionParsingTests extends ExpressionTestCase { } } - // TODO need to support this case but what is the neatest way? Escape the clashing delimiters in the expression - // string? - // public void testParsingTemplateExpressionThatEmbedsTheDelimiters() throws Exception { - // SpelExpressionParser parser = new SpelExpressionParser(); - // Expression expr = parser.parseExpression("The quick ${{'green','brown'}.${true}} fox jumped over the ${'lazy'} - // dog",DefaultTemplateParserContext.INSTANCE); - // Object o = expr.getValue(); - // System.out.println(o); - // assertEquals("The quick brown fox jumped over the lazy dog",o.toString()); - // } - }