Author: rahul Date: Fri Jul 17 18:40:59 2009 New Revision: 795190 URL: http://svn.apache.org/viewvc?rev=795190&view=rev Log: UnifiedJEXL improvements: - Better names for inner classes - More test coverage, and some fixes - Factor String parsing utilities in new class (StringParser) Slightly modified version of patch thanks to Henri Biestro <hbiestro at gmail dot com>. JEXL-58
Added: commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/util/StringParser.java (with props) Modified: commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/UnifiedJEXL.java commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/parser/Parser.jjt commons/proper/jexl/branches/2.0/src/test/org/apache/commons/jexl/UnifiedJEXLTest.java Modified: commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/UnifiedJEXL.java URL: http://svn.apache.org/viewvc/commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/UnifiedJEXL.java?rev=795190&r1=795189&r2=795190&view=diff ============================================================================== --- commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/UnifiedJEXL.java (original) +++ commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/UnifiedJEXL.java Fri Jul 17 18:40:59 2009 @@ -21,6 +21,7 @@ import java.util.ArrayList; import org.apache.commons.jexl.parser.SimpleNode; import org.apache.commons.jexl.parser.ParseException; +import org.apache.commons.jexl.util.StringParser; /** * An evaluator similar to the unified EL evaluator used in JSP/JSF based on JEXL. @@ -173,7 +174,7 @@ // if only one sub-expr, no need to create a composite return (expressions.size() == 1)? expressions.get(0) : - el.new Composite(counts, expressions, source); + el.new CompositeExpression(counts, expressions, source); } } @@ -199,21 +200,41 @@ * The abstract base class for all expressions, immediate '${...}' and deferred '#{...}'. */ public abstract class Expression { - final Expression source; + protected final Expression source; Expression(Expression source) { this.source = source != null ? source : this; } + @Override + public String toString() { + StringBuilder strb = new StringBuilder(); + if (source != this) { + strb.append(source.toString()); + strb.append(" /*= "); + } + asString(strb); + if (source != this) { + strb.append(" */"); + } + return strb.toString(); + } /** - * Generate the string corresponding to this expression. + * Generates this expression's string representation. * @return the string representation */ public String asString() { - return toString(); + StringBuilder strb = new StringBuilder(); + asString(strb); + return strb.toString(); } /** + * Adds this expression's string representation to a StringBuilder. + */ + abstract void asString(StringBuilder strb); + + /** * When the expression is dependant upon immediate and deferred sub-expressions, * evaluates the immediate sub-expressions with the context passed as parameter * and returns this expression deferred form. @@ -294,49 +315,50 @@ /** A constant expression. */ - private class Constant extends Expression { + private class ConstantExpression extends Expression { private final Object value; - Constant(Object value, Expression source) { + ConstantExpression(Object value, Expression source) { super(source); if (value == null) { throw new NullPointerException("constant can not be null"); } + if (value instanceof String) { + value = StringParser.buildString((String) value, false); + } this.value = value; } + @Override + public String asString() { + StringBuilder strb = new StringBuilder(); + strb.append('"'); + asString(strb); + strb.append('"'); + return strb.toString(); + } + + @Override ExpressionType getType() { return ExpressionType.CONSTANT; } @Override - public String toString() { + void asString(StringBuilder strb) { String str = value.toString(); if (value instanceof String || value instanceof CharSequence) { - StringBuilder strb = new StringBuilder(str.length() + 2); - if (source != this) { - strb.append(source.toString()); - strb.append(" /*= "); - } - strb.append('"'); for (int i = 0, size = str.length(); i < size; ++i) { char c = str.charAt(i); if (c == '"') strb.append('\\'); + else if (c == '\\') + strb.append("\\\\"); strb.append(c); } - strb.append('"'); - if (source != this) { - strb.append(" */"); - } - return strb.toString(); } - return str; - } - - @Override - public String asString() { - return value.toString(); + else { + strb.append(str); + } } @Override @@ -363,11 +385,11 @@ /** The base for Jexl based expressions. */ - abstract private class JexlBased extends Expression { + abstract private class JexlBasedExpression extends Expression { protected final CharSequence expr; protected final SimpleNode node; - JexlBased(CharSequence expr, SimpleNode node, Expression source) { + JexlBasedExpression(CharSequence expr, SimpleNode node, Expression source) { super(source); this.expr = expr; this.node = node; @@ -391,8 +413,11 @@ } @Override - public String asString() { - return expr.toString(); + public void asString(StringBuilder strb) { + strb.append(isImmediate()? '$' : '#'); + strb.append("{"); + strb.append(expr); + strb.append("}"); } @Override @@ -418,8 +443,8 @@ } /** An immediate expression: ${jexl}. */ - private class Immediate extends JexlBased { - Immediate(CharSequence expr, SimpleNode node, Expression source) { + private class ImmediateExpression extends JexlBasedExpression { + ImmediateExpression(CharSequence expr, SimpleNode node, Expression source) { super(expr, node, source); } @@ -435,8 +460,8 @@ } /** An immediate expression: ${jexl}. */ - private class Deferred extends JexlBased { - Deferred(CharSequence expr, SimpleNode node, Expression source) { + private class DeferredExpression extends JexlBasedExpression { + DeferredExpression(CharSequence expr, SimpleNode node, Expression source) { super(expr, node, source); } @@ -456,8 +481,8 @@ * #{...${jexl}...} * Note that the deferred syntax is JEXL's, not UnifiedJEXL. */ - private class Nested extends Deferred { - Nested(CharSequence expr, SimpleNode node, Expression source) { + private class NestedExpression extends DeferredExpression { + NestedExpression(CharSequence expr, SimpleNode node, Expression source) { super(expr, node, source); if (this.source != this) { throw new IllegalArgumentException("Nested expression can not have a source"); @@ -483,7 +508,7 @@ public Expression prepare(Interpreter interpreter) throws ParseException { String value = interpreter.interpret(node).toString(); SimpleNode dnode = toNode(value); - return new Deferred(value, dnode, this); + return new DeferredExpression(value, dnode, this); } @Override @@ -494,13 +519,13 @@ /** A composite expression: "... ${...} ... #{...} ...". */ - private class Composite extends Expression { + private class CompositeExpression extends Expression { // bit encoded (deferred count > 0) bit 1, (immediate count > 0) bit 0 final int meta; // the list of expression resulting from parsing final Expression[] exprs; - Composite(int[] counts, ArrayList<Expression> exprs, Expression source) { + CompositeExpression(int[] counts, ArrayList<Expression> exprs, Expression source) { super(source); this.exprs = exprs.toArray(new Expression[exprs.size()]); this.meta = (counts[ExpressionType.DEFERRED.index] > 0? 2 : 0) | @@ -518,32 +543,14 @@ @Override public boolean isImmediate() { // immediate if no deferred - return (meta & 2) != 0; - } - - @Override - public String toString() { - StringBuilder strb = new StringBuilder(); - if (source != this) { - strb.append(source.toString()); - strb.append(" /*= "); - } - for (Expression e : exprs) { - strb.append(e.toString()); - } - if (source != this) { - strb.append(" */ "); - } - return strb.toString(); + return (meta & 2) == 0; } @Override - public String asString() { - StringBuilder strb = new StringBuilder(); + void asString(StringBuilder strb) { for (Expression e : exprs) { - strb.append(e.asString()); + e.asString(strb); } - return strb.toString(); } @Override @@ -565,10 +572,10 @@ for(int e = 0; e < size; ++e) { Expression expr = exprs[e]; Expression prepared = expr.prepare(interpreter); - if (evalImmediate && prepared instanceof Immediate) { + if (evalImmediate && prepared instanceof ImmediateExpression) { // evaluate immediate as constant Object value = prepared.evaluate(interpreter); - prepared = value == null? null : new Constant(value, prepared); + prepared = value == null? null : new ConstantExpression(value, prepared); } // add it if not null if (prepared != null) { @@ -789,7 +796,7 @@ state = ParseState.IMMEDIATE1; // if chars in buffer, create constant if (strb.length() > 0) { - Expression cexpr = new Constant(strb.toString(), null); + Expression cexpr = new ConstantExpression(strb.toString(), null); builder.add(cexpr); strb.delete(0, Integer.MAX_VALUE); } @@ -806,7 +813,7 @@ state = ParseState.DEFERRED1; // if chars in buffer, create constant if (strb.length() > 0) { - Expression cexpr = new Constant(strb.toString(), null); + Expression cexpr = new ConstantExpression(strb.toString(), null); builder.add(cexpr); strb.delete(0, Integer.MAX_VALUE); } @@ -822,7 +829,7 @@ if (c == '}') { // materialize the immediate expr //Expression iexpr = createExpression(ExpressionType.IMMEDIATE, strb, null); - Expression iexpr = new Immediate(strb.toString(), toNode(strb), null); + Expression iexpr = new ImmediateExpression(strb.toString(), toNode(strb), null); builder.add(iexpr); strb.delete(0, Integer.MAX_VALUE); state = ParseState.CONST; @@ -836,7 +843,7 @@ // skip inner strings (for '}') if (c == '"' || c == '\'') { strb.append(c); - i = readDeferredString(strb, expr, i + 1, c); + i = StringParser.readString(strb, expr, i + 1, c); continue; } // nested immediate in deferred; need to balance count of '{' & '}' @@ -856,8 +863,8 @@ } else { // materialize the nested/deferred expr Expression dexpr = nested? - new Nested(expr.substring(inested, i+1), toNode(strb), null) : - new Deferred(strb.toString(), toNode(strb), null); + new NestedExpression(expr.substring(inested, i+1), toNode(strb), null) : + new DeferredExpression(strb.toString(), toNode(strb), null); builder.add(dexpr); strb.delete(0, Integer.MAX_VALUE); nested = false; @@ -883,42 +890,12 @@ } // we should be in that state if (state != ParseState.CONST) - throw new IllegalStateException("malformed expression: " + expr); + throw new ParseException("malformed expression: " + expr); // if any chars were buffered, add them as a constant if (strb.length() > 0) { - Expression cexpr = new Constant(strb.toString(), null); + Expression cexpr = new ConstantExpression(strb.toString(), null); builder.add(cexpr); } return builder.build(this, null); } - - /** - * Read the remainder of a string till a given separator, - * handles escaping through '\' syntax. - * @param strb the destination buffer to copy characters into - * @param str the origin - * @param index the offset into the origin - * @param sep the separator, single or double quote, marking end of string - * @return the offset in origin - */ - static private int readDeferredString(StringBuilder strb, String str, int index, char sep) { - boolean escape = false; - for(;index < str.length();++index) { - char c = str.charAt(index); - if (escape) { - strb.append(c); - escape = false; - continue; - } - if (c == '\\') { - escape = true; - continue; - } - strb.append(c); - if (c == sep) { - break; - } - } - return index; - } } \ No newline at end of file Modified: commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/parser/Parser.jjt URL: http://svn.apache.org/viewvc/commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/parser/Parser.jjt?rev=795190&r1=795189&r2=795190&view=diff ============================================================================== --- commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/parser/Parser.jjt (original) +++ commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/parser/Parser.jjt Fri Jul 17 18:40:59 2009 @@ -43,7 +43,7 @@ import org.apache.commons.jexl.util.introspection.Uberspect; -public class Parser +public class Parser extends StringParser { public SimpleNode parse(Reader reader) @@ -349,7 +349,7 @@ { ( t=<STRING_LITERAL> - { jjtThis.image = t.image.substring(1, t.image.length() -1); } + { jjtThis.image = Parser.buildString(t.image, true); } ) } @@ -484,8 +484,8 @@ TOKEN : { <STRING_LITERAL : - ("\"" ( ~["\"","\n","\r"] )* "\"" ) + ("\"" ( ~["\"","\n","\r"] | "\\" ["n","t","b","r","f","\\","\""] )* "\"" ) | - ("\'" ( ~["\'","\n","\r"] )* "\'" ) + ("\'" ( ~["\'","\n","\r"] | "\\" ["n","t","b","r","f","\\","\'"])* "\'" ) > } \ No newline at end of file Added: commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/util/StringParser.java URL: http://svn.apache.org/viewvc/commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/util/StringParser.java?rev=795190&view=auto ============================================================================== --- commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/util/StringParser.java (added) +++ commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/util/StringParser.java Fri Jul 17 18:40:59 2009 @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.jexl.util; + +/** + * Common constant strings utilities. + */ +public class StringParser { + /** + * Builds a string, handles escaping through '\' syntax. + * @param str the string to build from + * @param eatsep whether the separator, the first character, should be considered + * @return the built string + */ + public static String buildString(CharSequence str, boolean eatsep) { + StringBuilder strb = new StringBuilder(str.length()); + char sep = eatsep ? str.charAt(0) : 0; + int end = str.length() - (eatsep ? 1 : 0); + int begin = (eatsep ? 1 : 0); + read(strb, str, begin, end, sep); + return strb.toString(); + } + + /** + * Read the remainder of a string till a given separator, + * handles escaping through '\' syntax. + * @param strb the destination buffer to copy characters into + * @param str the origin + * @param index the offset into the origin + * @param sep the separator, single or double quote, marking end of string + * @return the offset in origin + */ + public static int readString(StringBuilder strb, CharSequence str, int index, char sep) { + return read(strb, str, index, str.length(), sep); + } + + /** + * Read the remainder of a string till a given separator, + * handles escaping through '\' syntax. + * @param strb the destination buffer to copy characters into + * @param str the origin + * @param begin the relative offset in str to begin reading + * @param end the relative offset in str to end reading + * @param sep the separator, single or double quote, marking end of string + * @return the last character offset handled in origin + */ + private static int read(StringBuilder strb, CharSequence str, int begin, int end, char sep) { + boolean escape = false; + int index = begin; + for (; index < end; ++index) { + char c = str.charAt(index); + if (escape) { + strb.append(c); + if (c == '\\') + strb.append('\\'); + escape = false; + continue; + } + if (c == '\\') { + escape = true; + continue; + } + strb.append(c); + if (c == sep) { + break; + } + } + return index; + } +} \ No newline at end of file Propchange: commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/util/StringParser.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: commons/proper/jexl/branches/2.0/src/java/org/apache/commons/jexl/util/StringParser.java ------------------------------------------------------------------------------ svn:keywords = Date Author Id Revision HeadURL Modified: commons/proper/jexl/branches/2.0/src/test/org/apache/commons/jexl/UnifiedJEXLTest.java URL: http://svn.apache.org/viewvc/commons/proper/jexl/branches/2.0/src/test/org/apache/commons/jexl/UnifiedJEXLTest.java?rev=795190&r1=795189&r2=795190&view=diff ============================================================================== --- commons/proper/jexl/branches/2.0/src/test/org/apache/commons/jexl/UnifiedJEXLTest.java (original) +++ commons/proper/jexl/branches/2.0/src/test/org/apache/commons/jexl/UnifiedJEXLTest.java Fri Jul 17 18:40:59 2009 @@ -15,7 +15,9 @@ * limitations under the License. */ package org.apache.commons.jexl; - +import java.util.Map; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import junit.framework.TestCase; /** * Test cases for the UnifiedEL. @@ -28,10 +30,16 @@ JEXL.setCache(128); } static UnifiedJEXL EL = new UnifiedJEXL(JEXL); - + static Log LOG = LogFactory.getLog(UnifiedJEXL.class); + JexlContext context = null; + Map<String,Object> vars =null; + + @Override public void setUp() throws Exception { // ensure jul logging is only error java.util.logging.Logger.getLogger(JexlEngine.class.getName()).setLevel(java.util.logging.Level.SEVERE); + context = JexlHelper.createContext(); + vars = context.getVars(); } public static class Froboz { @@ -80,79 +88,154 @@ public void testAssign() throws Exception { UnifiedJEXL.Expression assign = EL.parse("${froboz.value = 10}"); UnifiedJEXL.Expression check = EL.parse("${froboz.value}"); - JexlContext jc = JexlHelper.createContext(); - Object o = assign.evaluate(jc); + Object o = assign.evaluate(context); assertEquals("Result is not 10", new Integer(10), o); - o = check.evaluate(jc); + o = check.evaluate(context); assertEquals("Result is not 10", new Integer(10), o); } - public void testDear() throws Exception { - UnifiedJEXL.Expression assign = EL.parse("Dear ${p} ${name};"); - JexlContext jc = JexlHelper.createContext(); - jc.getVars().put("p", "Mr"); - jc.getVars().put("name", "Doe"); - Object o = assign.evaluate(jc); + public void testComposite() throws Exception { + UnifiedJEXL.Expression expr = EL.parse("Dear ${p} ${name};"); + vars.put("p", "Mr"); + vars.put("name", "Doe"); + assertTrue("expression should be immediate", expr.isImmediate()); + Object o = expr.evaluate(context); assertEquals("Dear Mr Doe;", o); - jc.getVars().put("p", "Ms"); - jc.getVars().put("name", "Jones"); - o = assign.evaluate(jc); + vars.put("p", "Ms"); + vars.put("name", "Jones"); + o = expr.evaluate(context); assertEquals("Dear Ms Jones;", o); } - public void testDear2Phase() throws Exception { - UnifiedJEXL.Expression assign = EL.parse("Dear #{p} ${name};"); - JexlContext jc = JexlHelper.createContext(); - jc.getVars().put("name", "Doe"); - UnifiedJEXL.Expression phase1 = assign.prepare(jc); - jc.getVars().put("p", "Mr"); - jc.getVars().put("name", "Should not be used in 2nd phase"); - Object o = phase1.evaluate(jc); + public void testPrepareEvaluate() throws Exception { + UnifiedJEXL.Expression expr = EL.parse("Dear #{p} ${name};"); + assertTrue("expression should be deferred", expr.isDeferred()); + vars.put("name", "Doe"); + UnifiedJEXL.Expression phase1 = expr.prepare(context); + String as = phase1.asString(); + assertEquals("Dear #{p} Doe;", as); + vars.put("p", "Mr"); + vars.put("name", "Should not be used in 2nd phase"); + Object o = phase1.evaluate(context); assertEquals("Dear Mr Doe;", o); } public void testNested() throws Exception { UnifiedJEXL.Expression expr = EL.parse("#{${hi}+'.world'}"); - JexlContext jc = JexlHelper.createContext(); - jc.getVars().put("hi", "hello"); - jc.getVars().put("hello.world", "Hello World!"); - Object o = expr.evaluate(jc); + vars.put("hi", "hello"); + vars.put("hello.world", "Hello World!"); + Object o = expr.evaluate(context); + assertTrue("source should not be expression", expr.getSource() != expr.prepare(context)); + assertTrue("expression should be deferred", expr.isDeferred()); assertEquals("Hello World!", o); } public void testImmediate() throws Exception { + JexlContext none = null; UnifiedJEXL.Expression expr = EL.parse("${'Hello ' + 'World!'}"); - JexlContext jc = null; - Object o = expr.evaluate(jc); + assertTrue("prepare should return same expression", expr.prepare(none) == expr); + Object o = expr.evaluate(none); + assertTrue("expression should be immediate", expr.isImmediate()); + assertEquals("Hello World!", o); + } + + public void testConstant() throws Exception { + JexlContext none = null; + UnifiedJEXL.Expression expr = EL.parse("Hello World!"); + assertTrue("prepare should return same expression", expr.prepare(none) == expr); + Object o = expr.evaluate(none); + assertTrue("expression should be immediate", expr.isImmediate()); assertEquals("Hello World!", o); } public void testDeferred() throws Exception { + JexlContext none = null; UnifiedJEXL.Expression expr = EL.parse("#{'world'}"); - JexlContext jc = null; - Object o = expr.evaluate(jc); + assertTrue("prepare should return same expression", expr.prepare(none) == expr); + Object o = expr.evaluate(none); + assertTrue("expression should be deferred", expr.isDeferred()); assertEquals("world", o); } + + public void testEscape() throws Exception { + UnifiedJEXL.Expression expr = EL.parse("#\\{'world'\\}"); + JexlContext none = null; + Object o = expr.evaluate(none); + assertEquals("#{'world'}", o); + } + + public void testEscapeString() throws Exception { + UnifiedJEXL.Expression expr = EL.parse("\\\"${'world\\'s finest'}\\\""); + JexlContext none = null; + Object o = expr.evaluate(none); + assertEquals("\"world's finest\"", o); + } + + public void testMalformed() throws Exception { + try { + UnifiedJEXL.Expression expr = EL.parse("${'world'"); + JexlContext none = null; + Object o = expr.evaluate(none); + fail("should be malformed"); + } + catch(UnifiedJEXL.Exception xjexl) { + // expected + String xmsg = xjexl.getMessage(); + LOG.warn(xmsg); + } + } + + public void testMalformedNested() throws Exception { + try { + UnifiedJEXL.Expression expr = EL.parse("#{${hi} world}"); + JexlContext none = null; + Object o = expr.evaluate(none); + fail("should be malformed"); + } + catch(UnifiedJEXL.Exception xjexl) { + // expected + String xmsg = xjexl.getMessage(); + LOG.warn(xmsg); + } + } + + public void testBadContextNested() throws Exception { + try { + UnifiedJEXL.Expression expr = EL.parse("#{${hi}+'.world'}"); + JexlContext none = null; + Object o = expr.evaluate(none); + fail("should be malformed"); + } + catch(UnifiedJEXL.Exception xjexl) { + // expected + String xmsg = xjexl.getMessage(); + LOG.warn(xmsg); + } + } public void testCharAtBug() throws Exception { - JexlContext jc = JexlHelper.createContext(); - - jc.getVars().put("foo", "abcdef"); + vars.put("foo", "abcdef"); UnifiedJEXL.Expression expr = EL.parse("${foo.substring(2,4)/*comment*/}"); - Object o = expr.evaluate(jc); + Object o = expr.evaluate(context); assertEquals("cd", o); - jc.getVars().put("bar", "foo"); - EL.getEngine().setSilent(true); - expr = EL.parse("#{${bar}+'.charAt(-2)'}"); - expr = expr.prepare(jc); - o = expr.evaluate(jc); - assertEquals(null, o); + vars.put("bar", "foo"); + try { + JEXL.setSilent(true); + expr = EL.parse("#{${bar}+'.charAt(-2)'}"); + expr = expr.prepare(context); + o = expr.evaluate(context); + assertEquals(null, o); + } + finally { + JEXL.setSilent(false); + } } - + public static void main(String[] args) throws Exception { - //new UnifiedELTest("debug").testClassHash(); - new UnifiedJEXLTest("debug").testAssign(); + UnifiedJEXLTest test = new UnifiedJEXLTest("debug"); + test.setUp(); + test.testBadContextNested(); } } \ No newline at end of file