From c8af02186c0bdaedf22c6d2e37f200fd317aa09f Mon Sep 17 00:00:00 2001 From: Erwin Vervaet Date: Wed, 28 Mar 2007 19:13:19 +0000 Subject: [PATCH] OgnlExpressionParser now supports OGNL collection construction syntax (SWF-274). --- .../support/AbstractExpressionParser.java | 16 ++++- .../binding/method/TextToMethodSignature.java | 66 +++++++++++++++---- .../support/OgnlExpressionParserTests.java | 37 ++++++++++- .../method/TextToMethodSignatureTests.java | 44 ++++++++++++- spring-webflow/changelog.txt | 1 + 5 files changed, 147 insertions(+), 17 deletions(-) diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/AbstractExpressionParser.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/AbstractExpressionParser.java index 62943d4a..3a0564a8 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/support/AbstractExpressionParser.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/AbstractExpressionParser.java @@ -120,7 +120,7 @@ public abstract class AbstractExpressionParser implements ExpressionParser { * 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 concating the results produces the complete evaluated string. + * and concatenating the results produces the complete evaluated string. * @param expressionString the expression string * @return the parsed expressions * @throws ParserException when the expressions cannot be parsed @@ -137,8 +137,18 @@ public abstract class AbstractExpressionParser implements ExpressionParser { expressions.add(new StaticExpression(expressionString.substring(startIdx, prefixIndex))); startIdx = prefixIndex; } - int suffixIndex = expressionString.indexOf(getExpressionSuffix(), prefixIndex); - if (suffixIndex == -1) { + int nextPrefixIndex = expressionString.indexOf(getExpressionPrefix(), + prefixIndex + getExpressionPrefix().length()); + int suffixIndex; + if (nextPrefixIndex == -1) { + // this is the last expression in the expression string + suffixIndex = expressionString.lastIndexOf(getExpressionSuffix()); + } + else { + // another expression exists after this one in the expression string + suffixIndex = expressionString.lastIndexOf(getExpressionSuffix(), nextPrefixIndex); + } + if (suffixIndex < (prefixIndex + getExpressionPrefix().length())) { throw new ParserException(expressionString, "No ending suffix '" + getExpressionSuffix() + "' for expression starting at character " + prefixIndex + ": " + expressionString.substring(prefixIndex), null); diff --git a/spring-binding/src/main/java/org/springframework/binding/method/TextToMethodSignature.java b/spring-binding/src/main/java/org/springframework/binding/method/TextToMethodSignature.java index ab77b827..b015918e 100644 --- a/spring-binding/src/main/java/org/springframework/binding/method/TextToMethodSignature.java +++ b/spring-binding/src/main/java/org/springframework/binding/method/TextToMethodSignature.java @@ -15,12 +15,14 @@ */ package org.springframework.binding.method; +import java.util.LinkedList; +import java.util.List; + import org.springframework.binding.convert.ConversionContext; import org.springframework.binding.convert.ConversionException; import org.springframework.binding.convert.ConversionService; import org.springframework.binding.convert.support.ConversionServiceAwareConverter; import org.springframework.binding.expression.Expression; -import org.springframework.util.StringUtils; /** * Converter that takes an encoded string representation and produces a @@ -69,27 +71,30 @@ public class TextToMethodSignature extends ConversionServiceAwareConverter { } protected Object doConvert(Object source, Class targetClass, ConversionContext context) throws Exception { - String encodedMethodKey = (String)source; - encodedMethodKey = encodedMethodKey.trim(); - int openParan = encodedMethodKey.indexOf('('); + String encodedMethodSignature = (String)source; + encodedMethodSignature = encodedMethodSignature.trim(); + int openParan = encodedMethodSignature.indexOf('('); if (openParan == -1) { - return new MethodSignature(encodedMethodKey); + // form "foo" + return new MethodSignature(encodedMethodSignature); } else { - String methodName = encodedMethodKey.substring(0, openParan); - int closeParan = encodedMethodKey.lastIndexOf(')'); - if (closeParan == -1) { - throw new ConversionException(encodedMethodKey, MethodSignature.class, + // form "foo(...)" + String methodName = encodedMethodSignature.substring(0, openParan); + int closeParan = encodedMethodSignature.lastIndexOf(')'); + if (closeParan != (encodedMethodSignature.length() - 1)) { + throw new ConversionException(encodedMethodSignature, MethodSignature.class, "Syntax error: No close parenthesis specified for method parameter list", null); } - String delimParamList = encodedMethodKey.substring(openParan + 1, closeParan); - String[] paramArray = StringUtils.commaDelimitedListToStringArray(delimParamList); + String delimitedParams = encodedMethodSignature.substring(openParan + 1, closeParan); + String[] paramArray = splitParameters(encodedMethodSignature, delimitedParams); Parameters params = new Parameters(paramArray.length); for (int i = 0; i < paramArray.length; i++) { // param could be of the form "type name", "name", "type ${name}" or "${name}" String param = paramArray[i].trim(); int space = param.indexOf(' '); - if (space == -1 || space > param.indexOf('$')) { + int expr = param.indexOf('{'); + if (space == -1 || (expr != -1 && space > expr)) { // "name" or "${name}" params.add(new Parameter(null, parseExpression(param))); } @@ -103,4 +108,41 @@ public class TextToMethodSignature extends ConversionServiceAwareConverter { return new MethodSignature(methodName, params); } } + + /** + * Split given parameter string into individual parameter definitions. + */ + private String[] splitParameters(String encodedMethodSignature, String parameters) { + List res = new LinkedList(); + + int paramStart = 0; + int blockNestingCount = 0; + for (int i = 0; i < parameters.length(); i++) { + switch (parameters.charAt(i)) { + case '{': + blockNestingCount++; + break; + case '}': + blockNestingCount--; + break; + case ',': + if (blockNestingCount == 0) { + // only take comma delimiter into account when not inside + // a block + res.add(parameters.substring(paramStart, i)); + paramStart = i + 1; + } + break; + } + } + if (blockNestingCount != 0) { + throw new ConversionException(encodedMethodSignature, MethodSignature.class, + "Syntax error: Curly braces do not match", null); + } + if (paramStart < parameters.length()) { + res.add(parameters.substring(paramStart)); + } + + return (String[])res.toArray(new String[res.size()]); + } } \ No newline at end of file diff --git a/spring-binding/src/test/java/org/springframework/binding/expression/support/OgnlExpressionParserTests.java b/spring-binding/src/test/java/org/springframework/binding/expression/support/OgnlExpressionParserTests.java index 24681021..0c64dc18 100644 --- a/spring-binding/src/test/java/org/springframework/binding/expression/support/OgnlExpressionParserTests.java +++ b/spring-binding/src/test/java/org/springframework/binding/expression/support/OgnlExpressionParserTests.java @@ -76,6 +76,13 @@ public class OgnlExpressionParserTests extends TestCase { } public void testSyntaxError1() { + try { + parser.parseExpression("${"); + fail(); + } + catch (ParserException e) { + } + try { String exp = "hello ${flag} ${abcd defg"; parser.parseExpression(exp); @@ -86,6 +93,13 @@ public class OgnlExpressionParserTests extends TestCase { } public void testSyntaxError2() { + try { + parser.parseExpression("${}"); + fail("Should've failed - not intended use"); + } + catch (ParserException e) { + } + try { String exp = "hello ${flag} ${}"; parser.parseExpression(exp); @@ -94,7 +108,7 @@ public class OgnlExpressionParserTests extends TestCase { catch (ParserException e) { } } - + public void testIsDelimitedExpression() { assertTrue(parser.isDelimitedExpression("${foo}")); assertTrue(parser.isDelimitedExpression("${foo ${foo}}")); @@ -107,4 +121,25 @@ public class OgnlExpressionParserTests extends TestCase { assertFalse(parser.isDelimitedExpression("$foo}")); assertFalse(parser.isDelimitedExpression("foo ${}")); } + + public void testCollectionContructionSyntax() { + // lists + parser.parseExpression("name in {null, \"Untitled\"}"); + parser.parseExpression("${name in {null, \"Untitled\"}}"); + + // native arrays + parser.parseExpression("new int[] {1, 2, 3}"); + parser.parseExpression("${new int[] {1, 2, 3}}"); + + // maps + parser.parseExpression("#{ 'foo' : 'foo value', 'bar' : 'bar value' }"); + parser.parseExpression("${#{ 'foo' : 'foo value', 'bar' : 'bar value' }}"); + parser.parseExpression("#@java.util.LinkedHashMap@{ 'foo' : 'foo value', 'bar' : 'bar value' }"); + parser.parseExpression("${#@java.util.LinkedHashMap@{ 'foo' : 'foo value', 'bar' : 'bar value' }}"); + + // complex examples + parser.parseExpression("b,#{1:2}"); + parser.parseExpression("${b,#{1:2}}"); + parser.parseExpression("a${b,#{1:2},e}f${g,#{3:4},j}k"); + } } \ No newline at end of file diff --git a/spring-binding/src/test/java/org/springframework/binding/method/TextToMethodSignatureTests.java b/spring-binding/src/test/java/org/springframework/binding/method/TextToMethodSignatureTests.java index c5a9fdde..339eae92 100644 --- a/spring-binding/src/test/java/org/springframework/binding/method/TextToMethodSignatureTests.java +++ b/spring-binding/src/test/java/org/springframework/binding/method/TextToMethodSignatureTests.java @@ -17,6 +17,7 @@ package org.springframework.binding.method; import junit.framework.TestCase; +import org.springframework.binding.convert.ConversionException; import org.springframework.binding.convert.support.DefaultConversionService; import org.springframework.binding.convert.support.TextToExpression; import org.springframework.binding.expression.support.OgnlExpressionParser; @@ -41,7 +42,7 @@ public class TextToMethodSignatureTests extends TestCase { assertEquals("foo", signature.getMethodName()); assertEquals(0, signature.getParameters().size()); - signature = (MethodSignature)converter.convert("foo"); + signature = (MethodSignature)converter.convert("foo()"); assertEquals("foo", signature.getMethodName()); assertEquals(0, signature.getParameters().size()); } @@ -94,4 +95,45 @@ public class TextToMethodSignatureTests extends TestCase { assertEquals(String.class, signature.getParameters().getParameter(1).getType()); assertEquals("externalContext.requestParameterMap.test", signature.getParameters().getParameter(1).getName().toString()); } + + public void testCollectionConstructionSyntax() { + MethodSignature signature = (MethodSignature)converter.convert("foo({1, 2, 3})"); + assertEquals("foo", signature.getMethodName()); + assertEquals(1, signature.getParameters().size()); + assertNull(signature.getParameters().getParameter(0).getType()); + assertEquals("{1, 2, 3}", signature.getParameters().getParameter(0).getName().toString()); + } + + public void testCollectionConstructionSyntaxWithType() { + MethodSignature signature = (MethodSignature)converter.convert("foo(java.util.List {1, 2, 3})"); + assertEquals("foo", signature.getMethodName()); + assertEquals(1, signature.getParameters().size()); + assertEquals(java.util.List.class, signature.getParameters().getParameter(0).getType()); + assertEquals("{1, 2, 3}", signature.getParameters().getParameter(0).getName().toString()); + + signature = (MethodSignature)converter.convert("foo(a${b,#{1:2},e}f${g,#{3:4},j}k)"); + } + + public void testSyntaxErrors() { + try { + converter.convert("foo("); + fail(); + } + catch (ConversionException e) { + } + + try { + converter.convert("foo(long 1, ${bar()}"); + fail(); + } + catch (ConversionException e) { + } + + try { + converter.convert("foo(long 1, {1, 2)"); + fail(); + } + catch (ConversionException e) { + } + } } diff --git a/spring-webflow/changelog.txt b/spring-webflow/changelog.txt index 158e01ab..852d1933 100644 --- a/spring-webflow/changelog.txt +++ b/spring-webflow/changelog.txt @@ -11,6 +11,7 @@ Package org.springframework.binding * Fixed possbile NullPointerException in MethodKey.parameterTypesString() (SWF-265). * OgnlExpression now unwraps the ognl.OgnlException to make the real exception available to the caller (SWF-255). +* OgnlExpressionParser now supports OGNL collection construction syntax (SWF-274). Package org.springframework.webflow.action * FormAction methods doBind() and createBinder() now declare "throws Exception".