View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl2;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.Reader;
22  import java.io.StringReader;
23  import java.io.Writer;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.LinkedHashSet;
27  import java.util.List;
28  import java.util.Set;
29  import org.apache.commons.jexl2.introspection.JexlMethod;
30  import org.apache.commons.jexl2.introspection.Uberspect;
31  import org.apache.commons.jexl2.parser.ASTJexlScript;
32  import org.apache.commons.jexl2.parser.JexlNode;
33  import org.apache.commons.jexl2.parser.StringParser;
34  
35  /**
36   * An evaluator similar to the Unified EL evaluator used in JSP/JSF based on JEXL.
37   * It is intended to be used in configuration modules, XML based frameworks or JSP taglibs
38   * and facilitate the implementation of expression evaluation.
39   * <p>
40   * An expression can mix immediate, deferred and nested sub-expressions as well as string constants;
41   * <ul>
42   * <li>The "immediate" syntax is of the form <code>"...${jexl-expr}..."</code></li>
43   * <li>The "deferred" syntax is of the form <code>"...#{jexl-expr}..."</code></li>
44   * <li>The "nested" syntax is of the form <code>"...#{...${jexl-expr0}...}..."</code></li>
45   * <li>The "composite" syntax is of the form <code>"...${jexl-expr0}... #{jexl-expr1}..."</code></li>
46   * </ul>
47   * </p>
48   * <p>
49   * Deferred & immediate expression carry different intentions:
50   * <ul>
51   * <li>An immediate expression indicate that evaluation is intended to be performed close to
52   * the definition/parsing point.</li>
53   * <li>A deferred expression indicate that evaluation is intended to occur at a later stage.</li>
54   * </ul>
55   * </p>
56   * <p>
57   * For instance: <code>"Hello ${name}, now is #{time}"</code> is a composite "deferred" expression since one
58   * of its subexpressions is deferred. Furthermore, this (composite) expression intent is
59   * to perform two evaluations; one close to its definition and another one in a later
60   * phase.
61   * </p>
62   * <p>
63   * The API reflects this feature in 2 methods, prepare and evaluate. The prepare method
64   * will evaluate the immediate subexpression and return an expression that contains only
65   * the deferred subexpressions (& constants), a prepared expression. Such a prepared expression
66   * is suitable for a later phase evaluation that may occur with a different JexlContext.
67   * Note that it is valid to call evaluate without prepare in which case the same JexlContext
68   * is used for the 2 evaluation phases.
69   * </p>
70   * <p>
71   * In the most common use-case where deferred expressions are to be kept around as properties of objects,
72   * one should parse & prepare an expression before storing it and evaluate it each time
73   * the property storing it is accessed.
74   * </p>
75   * <p>
76   * Note that nested expression use the JEXL syntax as in:
77   * <code>"#{${bar}+'.charAt(2)'}"</code>
78   * The most common mistake leading to an invalid expression being the following:
79   * <code>"#{${bar}charAt(2)}"</code>
80   * </p>
81   * <p>Also note that methods that parse evaluate expressions may throw <em>unchecked</em> exceptions;
82   * The {@link UnifiedJEXL.Exception} are thrown when the engine instance is in "non-silent" mode
83   * but since these are RuntimeException, user-code <em>should</em> catch them where appropriate.
84   * </p>
85   * @since 2.0
86   */
87  public final class UnifiedJEXL {
88      /** The JEXL engine instance. */
89      private final JexlEngine jexl;
90      /** The expression cache. */
91      private final JexlEngine.SoftCache<String, Expression> cache;
92      /** The default cache size. */
93      private static final int CACHE_SIZE = 256;
94      /** The first character for immediate expressions. */
95      private static final char IMM_CHAR = '$';
96      /** The first character for deferred expressions. */
97      private static final char DEF_CHAR = '#';
98  
99      /**
100      * Creates a new instance of UnifiedJEXL with a default size cache.
101      * @param aJexl the JexlEngine to use.
102      */
103     public UnifiedJEXL(JexlEngine aJexl) {
104         this(aJexl, CACHE_SIZE);
105     }
106 
107     /**
108      * Creates a new instance of UnifiedJEXL creating a local cache.
109      * @param aJexl the JexlEngine to use.
110      * @param cacheSize the number of expressions in this cache
111      */
112     public UnifiedJEXL(JexlEngine aJexl, int cacheSize) {
113         this.jexl = aJexl;
114         this.cache = aJexl.new SoftCache<String, Expression>(cacheSize);
115     }
116 
117     /**
118      * Types of expressions.
119      * Each instance carries a counter index per (composite sub-) expression type.
120      * @see ExpressionBuilder
121      */
122     private static enum ExpressionType {
123         /** Constant expression, count index 0. */
124         CONSTANT(0),
125         /** Immediate expression, count index 1. */
126         IMMEDIATE(1),
127         /** Deferred expression, count index 2. */
128         DEFERRED(2),
129         /** Nested (which are deferred) expressions, count index 2. */
130         NESTED(2),
131         /** Composite expressions are not counted, index -1. */
132         COMPOSITE(-1);
133         /** The index in arrays of expression counters for composite expressions. */
134         private final int index;
135 
136         /**
137          * Creates an ExpressionType.
138          * @param idx the index for this type in counters arrays.
139          */
140         ExpressionType(int idx) {
141             this.index = idx;
142         }
143     }
144 
145     /**
146      * A helper class to build expressions.
147      * Keeps count of sub-expressions by type.
148      */
149     private static class ExpressionBuilder {
150         /** Per expression type counters. */
151         private final int[] counts;
152         /** The list of expressions. */
153         private final ArrayList<Expression> expressions;
154 
155         /**
156          * Creates a builder.
157          * @param size the initial expression array size
158          */
159         ExpressionBuilder(int size) {
160             counts = new int[]{0, 0, 0};
161             expressions = new ArrayList<Expression>(size <= 0 ? 3 : size);
162         }
163 
164         /**
165          * Adds an expression to the list of expressions, maintain per-type counts.
166          * @param expr the expression to add
167          */
168         void add(Expression expr) {
169             counts[expr.getType().index] += 1;
170             expressions.add(expr);
171         }
172 
173         /**
174          * Builds an expression from a source, performs checks.
175          * @param el the unified el instance
176          * @param source the source expression
177          * @return an expression
178          */
179         Expression build(UnifiedJEXL el, Expression source) {
180             int sum = 0;
181             for (int count : counts) {
182                 sum += count;
183             }
184             if (expressions.size() != sum) {
185                 StringBuilder error = new StringBuilder("parsing algorithm error, exprs: ");
186                 error.append(expressions.size());
187                 error.append(", constant:");
188                 error.append(counts[ExpressionType.CONSTANT.index]);
189                 error.append(", immediate:");
190                 error.append(counts[ExpressionType.IMMEDIATE.index]);
191                 error.append(", deferred:");
192                 error.append(counts[ExpressionType.DEFERRED.index]);
193                 throw new IllegalStateException(error.toString());
194             }
195             // if only one sub-expr, no need to create a composite
196             if (expressions.size() == 1) {
197                 return expressions.get(0);
198             } else {
199                 return el.new CompositeExpression(counts, expressions, source);
200             }
201         }
202     }
203 
204     /**
205      * Gets the JexlEngine underlying the UnifiedJEXL.
206      * @return the JexlEngine
207      */
208     public JexlEngine getEngine() {
209         return jexl;
210     }
211 
212     /**
213      * Clears the cache.
214      * @since 2.1
215      */
216     public void clearCache() {
217         synchronized (cache) {
218             cache.clear();
219         }
220     }
221 
222     /**
223      * The sole type of (runtime) exception the UnifiedJEXL can throw.
224      */
225     public static class Exception extends RuntimeException {
226         /** Serial version UID. */
227         private static final long serialVersionUID = -8201402995815975726L;
228 
229         /**
230          * Creates a UnifiedJEXL.Exception.
231          * @param msg the exception message
232          * @param cause the exception cause
233          */
234         public Exception(String msg, Throwable cause) {
235             super(msg, cause);
236         }
237     }
238 
239     /**
240      * The abstract base class for all expressions, immediate '${...}' and deferred '#{...}'.
241      */
242     public abstract class Expression {
243         /** The source of this expression (see {@link UnifiedJEXL.Expression#prepare}). */
244         protected final Expression source;
245 
246         /**
247          * Creates an expression.
248          * @param src the source expression if any
249          */
250         Expression(Expression src) {
251             this.source = src != null ? src : this;
252         }
253 
254         /**
255          * Checks whether this expression is immediate.
256          * @return true if immediate, false otherwise
257          */
258         public boolean isImmediate() {
259             return true;
260         }
261 
262         /**
263          * Checks whether this expression is deferred.
264          * @return true if deferred, false otherwise
265          */
266         public final boolean isDeferred() {
267             return !isImmediate();
268         }
269 
270         /**
271          * Gets this expression type.
272          * @return its type
273          */
274         abstract ExpressionType getType();
275 
276         /**
277          * Formats this expression, adding its source string representation in
278          * comments if available: 'expression /*= source *\/'' .
279          * <b>Note:</b> do not override; will be made final in a future release.
280          * @return the formatted expression string
281          */
282         @Override
283         public String toString() {
284             StringBuilder strb = new StringBuilder();
285             asString(strb);
286             if (source != this) {
287                 strb.append(" /*= ");
288                 strb.append(source.toString());
289                 strb.append(" */");
290             }
291             return strb.toString();
292         }
293 
294         /**
295          * Generates this expression's string representation.
296          * @return the string representation
297          */
298         public String asString() {
299             StringBuilder strb = new StringBuilder();
300             asString(strb);
301             return strb.toString();
302         }
303 
304         /**
305          * Adds this expression's string representation to a StringBuilder.
306          * @param strb the builder to fill
307          * @return the builder argument
308          */
309         public abstract StringBuilder asString(StringBuilder strb);
310 
311         /**
312          * Gets the list of variables accessed by this expression.
313          * <p>This method will visit all nodes of the sub-expressions and extract all variables whether they
314          * are written in 'dot' or 'bracketed' notation. (a.b is equivalent to a['b']).</p>
315          * @return the set of variables, each as a list of strings (ant-ish variables use more than 1 string)
316          *         or the empty set if no variables are used
317          * @since 2.1
318          */
319         public Set<List<String>> getVariables() {
320             return Collections.emptySet();
321         }
322 
323         /**
324          * Fills up the list of variables accessed by this expression.
325          * @param refs the set of variable being filled
326          * @since 2.1
327          */
328         protected void getVariables(Set<List<String>> refs) {
329             // nothing to do
330         }
331 
332         /**
333          * Evaluates the immediate sub-expressions.
334          * <p>
335          * When the expression is dependant upon immediate and deferred sub-expressions,
336          * evaluates the immediate sub-expressions with the context passed as parameter
337          * and returns this expression deferred form.
338          * </p>
339          * <p>
340          * In effect, this binds the result of the immediate sub-expressions evaluation in the
341          * context, allowing to differ evaluation of the remaining (deferred) expression within another context.
342          * This only has an effect to nested & composite expressions that contain differed & immediate sub-expressions.
343          * </p>
344          * <p>
345          * If the underlying JEXL engine is silent, errors will be logged through its logger as warning.
346          * </p>
347          * <b>Note:</b> do not override; will be made final in a future release.
348          * @param context the context to use for immediate expression evaluations
349          * @return an expression or null if an error occurs and the {@link JexlEngine} is running in silent mode
350          * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not in silent mode
351          */
352         public Expression prepare(JexlContext context) {
353             try {
354                 Interpreter interpreter = new Interpreter(jexl, context, !jexl.isLenient(), jexl.isSilent());
355                 if (context instanceof TemplateContext) {
356                     interpreter.setFrame(((TemplateContext) context).getFrame());
357                 }
358                 return prepare(interpreter);
359             } catch (JexlException xjexl) {
360                 Exception xuel = createException("prepare", this, xjexl);
361                 if (jexl.isSilent()) {
362                     jexl.logger.warn(xuel.getMessage(), xuel.getCause());
363                     return null;
364                 }
365                 throw xuel;
366             }
367         }
368 
369         /**
370          * Evaluates this expression.
371          * <p>
372          * If the underlying JEXL engine is silent, errors will be logged through its logger as warning.
373          * </p>
374          * <b>Note:</b> do not override; will be made final in a future release.
375          * @param context the variable context
376          * @return the result of this expression evaluation or null if an error occurs and the {@link JexlEngine} is
377          * running in silent mode
378          * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent
379          */
380         public Object evaluate(JexlContext context) {
381             try {
382                 Interpreter interpreter = new Interpreter(jexl, context, !jexl.isLenient(), jexl.isSilent());
383                 if (context instanceof TemplateContext) {
384                     interpreter.setFrame(((TemplateContext) context).getFrame());
385                 }
386                 return evaluate(interpreter);
387             } catch (JexlException xjexl) {
388                 Exception xuel = createException("prepare", this, xjexl);
389                 if (jexl.isSilent()) {
390                     jexl.logger.warn(xuel.getMessage(), xuel.getCause());
391                     return null;
392                 }
393                 throw xuel;
394             }
395         }
396 
397         /**
398          * Retrieves this expression's source expression.
399          * If this expression was prepared, this allows to retrieve the
400          * original expression that lead to it.
401          * Other expressions return themselves.
402          * @return the source expression
403          */
404         public final Expression getSource() {
405             return source;
406         }
407 
408         /**
409          * Prepares a sub-expression for interpretation.
410          * @param interpreter a JEXL interpreter
411          * @return a prepared expression
412          * @throws JexlException (only for nested & composite)
413          */
414         protected Expression prepare(Interpreter interpreter) {
415             return this;
416         }
417 
418         /**
419          * Intreprets a sub-expression.
420          * @param interpreter a JEXL interpreter
421          * @return the result of interpretation
422          * @throws JexlException (only for nested & composite)
423          */
424         protected abstract Object evaluate(Interpreter interpreter);
425     }
426 
427     /** A constant expression. */
428     private class ConstantExpression extends Expression {
429         /** The constant held by this expression. */
430         private final Object value;
431 
432         /**
433          * Creates a constant expression.
434          * <p>
435          * If the wrapped constant is a string, it is treated
436          * as a JEXL strings with respect to escaping.
437          * </p>
438          * @param val the constant value
439          * @param source the source expression if any
440          */
441         ConstantExpression(Object val, Expression source) {
442             super(source);
443             if (val == null) {
444                 throw new NullPointerException("constant can not be null");
445             }
446             if (val instanceof String) {
447                 val = StringParser.buildString((String) val, false);
448             }
449             this.value = val;
450         }
451 
452         /** {@inheritDoc} */
453         @Override
454         ExpressionType getType() {
455             return ExpressionType.CONSTANT;
456         }
457 
458         /** {@inheritDoc} */
459         @Override
460         public StringBuilder asString(StringBuilder strb) {
461             if (value != null) {
462                 strb.append(value.toString());
463             }
464             return strb;
465         }
466 
467         /** {@inheritDoc} */
468         @Override
469         protected Object evaluate(Interpreter interpreter) {
470             return value;
471         }
472     }
473 
474     /** The base for Jexl based expressions. */
475     private abstract class JexlBasedExpression extends Expression {
476         /** The JEXL string for this expression. */
477         protected final CharSequence expr;
478         /** The JEXL node for this expression. */
479         protected final JexlNode node;
480 
481         /**
482          * Creates a JEXL interpretable expression.
483          * @param theExpr the expression as a string
484          * @param theNode the expression as an AST
485          * @param theSource the source expression if any
486          */
487         protected JexlBasedExpression(CharSequence theExpr, JexlNode theNode, Expression theSource) {
488             super(theSource);
489             this.expr = theExpr;
490             this.node = theNode;
491         }
492 
493         /** {@inheritDoc} */
494         @Override
495         public StringBuilder asString(StringBuilder strb) {
496             strb.append(isImmediate() ? IMM_CHAR : DEF_CHAR);
497             strb.append("{");
498             strb.append(expr);
499             strb.append("}");
500             return strb;
501         }
502 
503         /** {@inheritDoc} */
504         @Override
505         protected Object evaluate(Interpreter interpreter) {
506             return interpreter.interpret(node);
507         }
508 
509         /** {@inheritDoc} */
510         @Override
511         public Set<List<String>> getVariables() {
512             Set<List<String>> refs = new LinkedHashSet<List<String>>();
513             getVariables(refs);
514             return refs;
515         }
516 
517         /** {@inheritDoc} */
518         @Override
519         protected void getVariables(Set<List<String>> refs) {
520             jexl.getVariables(node, refs, null);
521         }
522     }
523 
524     /** An immediate expression: ${jexl}. */
525     private class ImmediateExpression extends JexlBasedExpression {
526         /**
527          * Creates an immediate expression.
528          * @param expr the expression as a string
529          * @param node the expression as an AST
530          * @param source the source expression if any
531          */
532         ImmediateExpression(CharSequence expr, JexlNode node, Expression source) {
533             super(expr, node, source);
534         }
535 
536         /** {@inheritDoc} */
537         @Override
538         ExpressionType getType() {
539             return ExpressionType.IMMEDIATE;
540         }
541 
542         /** {@inheritDoc} */
543         @Override
544         protected Expression prepare(Interpreter interpreter) {
545             // evaluate immediate as constant
546             Object value = evaluate(interpreter);
547             return value != null ? new ConstantExpression(value, source) : null;
548         }
549     }
550 
551     /** A deferred expression: #{jexl}. */
552     private class DeferredExpression extends JexlBasedExpression {
553         /**
554          * Creates a deferred expression.
555          * @param expr the expression as a string
556          * @param node the expression as an AST
557          * @param source the source expression if any
558          */
559         DeferredExpression(CharSequence expr, JexlNode node, Expression source) {
560             super(expr, node, source);
561         }
562 
563         /** {@inheritDoc} */
564         @Override
565         public boolean isImmediate() {
566             return false;
567         }
568 
569         /** {@inheritDoc} */
570         @Override
571         ExpressionType getType() {
572             return ExpressionType.DEFERRED;
573         }
574 
575         /** {@inheritDoc} */
576         @Override
577         protected Expression prepare(Interpreter interpreter) {
578             return new ImmediateExpression(expr, node, source);
579         }
580 
581         /** {@inheritDoc} */
582         @Override
583         protected void getVariables(Set<List<String>> refs) {
584             // noop
585         }
586     }
587 
588     /**
589      * An immediate expression nested into a deferred expression.
590      * #{...${jexl}...}
591      * Note that the deferred syntax is JEXL's, not UnifiedJEXL.
592      */
593     private class NestedExpression extends JexlBasedExpression {
594         /**
595          * Creates a nested expression.
596          * @param expr the expression as a string
597          * @param node the expression as an AST
598          * @param source the source expression if any
599          */
600         NestedExpression(CharSequence expr, JexlNode node, Expression source) {
601             super(expr, node, source);
602             if (this.source != this) {
603                 throw new IllegalArgumentException("Nested expression can not have a source");
604             }
605         }
606 
607         @Override
608         public StringBuilder asString(StringBuilder strb) {
609             strb.append(expr);
610             return strb;
611         }
612 
613         /** {@inheritDoc} */
614         @Override
615         public boolean isImmediate() {
616             return false;
617         }
618 
619         /** {@inheritDoc} */
620         @Override
621         ExpressionType getType() {
622             return ExpressionType.NESTED;
623         }
624 
625         /** {@inheritDoc} */
626         @Override
627         protected Expression prepare(Interpreter interpreter) {
628             String value = interpreter.interpret(node).toString();
629             JexlNode dnode = jexl.parse(value, jexl.isDebug() ? node.debugInfo() : null, null);
630             return new ImmediateExpression(value, dnode, this);
631         }
632 
633         /** {@inheritDoc} */
634         @Override
635         protected Object evaluate(Interpreter interpreter) {
636             return prepare(interpreter).evaluate(interpreter);
637         }
638     }
639 
640     /** A composite expression: "... ${...} ... #{...} ...". */
641     private class CompositeExpression extends Expression {
642         /** Bit encoded (deferred count > 0) bit 1, (immediate count > 0) bit 0. */
643         private final int meta;
644         /** The list of sub-expression resulting from parsing. */
645         protected final Expression[] exprs;
646 
647         /**
648          * Creates a composite expression.
649          * @param counters counters of expression per type
650          * @param list the sub-expressions
651          * @param src the source for this expresion if any
652          */
653         CompositeExpression(int[] counters, ArrayList<Expression> list, Expression src) {
654             super(src);
655             this.exprs = list.toArray(new Expression[list.size()]);
656             this.meta = (counters[ExpressionType.DEFERRED.index] > 0 ? 2 : 0)
657                     | (counters[ExpressionType.IMMEDIATE.index] > 0 ? 1 : 0);
658         }
659 
660         /** {@inheritDoc} */
661         @Override
662         public boolean isImmediate() {
663             // immediate if no deferred
664             return (meta & 2) == 0;
665         }
666 
667         /** {@inheritDoc} */
668         @Override
669         ExpressionType getType() {
670             return ExpressionType.COMPOSITE;
671         }
672 
673         /** {@inheritDoc} */
674         @Override
675         public StringBuilder asString(StringBuilder strb) {
676             for (Expression e : exprs) {
677                 e.asString(strb);
678             }
679             return strb;
680         }
681 
682         /** {@inheritDoc} */
683         @Override
684         public Set<List<String>> getVariables() {
685             Set<List<String>> refs = new LinkedHashSet<List<String>>();
686             for (Expression expr : exprs) {
687                 expr.getVariables(refs);
688             }
689             return refs;
690         }
691 
692         /** {@inheritDoc} */
693         @Override
694         protected Expression prepare(Interpreter interpreter) {
695             // if this composite is not its own source, it is already prepared
696             if (source != this) {
697                 return this;
698             }
699             // we need to prepare all sub-expressions
700             final int size = exprs.length;
701             final ExpressionBuilder builder = new ExpressionBuilder(size);
702             // tracking whether prepare will return a different expression
703             boolean eq = true;
704             for (int e = 0; e < size; ++e) {
705                 Expression expr = exprs[e];
706                 Expression prepared = expr.prepare(interpreter);
707                 // add it if not null
708                 if (prepared != null) {
709                     builder.add(prepared);
710                 }
711                 // keep track of expression equivalence
712                 eq &= expr == prepared;
713             }
714             Expression ready = eq ? this : builder.build(UnifiedJEXL.this, this);
715             return ready;
716         }
717 
718         /** {@inheritDoc} */
719         @Override
720         protected Object evaluate(Interpreter interpreter) {
721             final int size = exprs.length;
722             Object value = null;
723             // common case: evaluate all expressions & concatenate them as a string
724             StringBuilder strb = new StringBuilder();
725             for (int e = 0; e < size; ++e) {
726                 value = exprs[e].evaluate(interpreter);
727                 if (value != null) {
728                     strb.append(value.toString());
729                 }
730             }
731             value = strb.toString();
732             return value;
733         }
734     }
735 
736     /** Creates a a {@link UnifiedJEXL.Expression} from an expression string.
737      *  Uses & fills up the expression cache if any.
738      * <p>
739      * If the underlying JEXL engine is silent, errors will be logged through its logger as warnings.
740      * </p>
741      * @param expression the UnifiedJEXL string expression
742      * @return the UnifiedJEXL object expression, null if silent and an error occured
743      * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent
744      */
745     public Expression parse(String expression) {
746         Exception xuel = null;
747         Expression stmt = null;
748         try {
749             if (cache == null) {
750                 stmt = parseExpression(expression, null);
751             } else {
752                 synchronized (cache) {
753                     stmt = cache.get(expression);
754                     if (stmt == null) {
755                         stmt = parseExpression(expression, null);
756                         cache.put(expression, stmt);
757                     }
758                 }
759             }
760         } catch (JexlException xjexl) {
761             xuel = new Exception("failed to parse '" + expression + "'", xjexl);
762         } catch (Exception xany) {
763             xuel = xany;
764         } finally {
765             if (xuel != null) {
766                 if (jexl.isSilent()) {
767                     jexl.logger.warn(xuel.getMessage(), xuel.getCause());
768                     return null;
769                 }
770                 throw xuel;
771             }
772         }
773         return stmt;
774     }
775 
776     /**
777      * Creates a UnifiedJEXL.Exception from a JexlException.
778      * @param action parse, prepare, evaluate
779      * @param expr the expression
780      * @param xany the exception
781      * @return an exception containing an explicit error message
782      */
783     private Exception createException(String action, Expression expr, java.lang.Exception xany) {
784         StringBuilder strb = new StringBuilder("failed to ");
785         strb.append(action);
786         if (expr != null) {
787             strb.append(" '");
788             strb.append(expr.toString());
789             strb.append("'");
790         }
791         Throwable cause = xany.getCause();
792         if (cause != null) {
793             String causeMsg = cause.getMessage();
794             if (causeMsg != null) {
795                 strb.append(", ");
796                 strb.append(causeMsg);
797             }
798         }
799         return new Exception(strb.toString(), xany);
800     }
801 
802     /** The different parsing states. */
803     private static enum ParseState {
804         /** Parsing a constant. */
805         CONST,
806         /** Parsing after $ .*/
807         IMMEDIATE0,
808         /** Parsing after # .*/
809         DEFERRED0,
810         /** Parsing after ${ .*/
811         IMMEDIATE1,
812         /** Parsing after #{ .*/
813         DEFERRED1,
814         /** Parsing after \ .*/
815         ESCAPE
816     }
817 
818     /**
819      * Parses a unified expression.
820      * @param expr the string expression
821      * @param scope the expression scope
822      * @return the expression instance
823      * @throws JexlException if an error occur during parsing
824      */
825     private Expression parseExpression(String expr, JexlEngine.Scope scope) {
826         final int size = expr.length();
827         ExpressionBuilder builder = new ExpressionBuilder(0);
828         StringBuilder strb = new StringBuilder(size);
829         ParseState state = ParseState.CONST;
830         int inner = 0;
831         boolean nested = false;
832         int inested = -1;
833         for (int i = 0; i < size; ++i) {
834             char c = expr.charAt(i);
835             switch (state) {
836                 default: // in case we ever add new expression type
837                     throw new UnsupportedOperationException("unexpected expression type");
838                 case CONST:
839                     if (c == IMM_CHAR) {
840                         state = ParseState.IMMEDIATE0;
841                     } else if (c == DEF_CHAR) {
842                         inested = i;
843                         state = ParseState.DEFERRED0;
844                     } else if (c == '\\') {
845                         state = ParseState.ESCAPE;
846                     } else {
847                         // do buildup expr
848                         strb.append(c);
849                     }
850                     break;
851                 case IMMEDIATE0: // $
852                     if (c == '{') {
853                         state = ParseState.IMMEDIATE1;
854                         // if chars in buffer, create constant
855                         if (strb.length() > 0) {
856                             Expression cexpr = new ConstantExpression(strb.toString(), null);
857                             builder.add(cexpr);
858                             strb.delete(0, Integer.MAX_VALUE);
859                         }
860                     } else {
861                         // revert to CONST
862                         strb.append(IMM_CHAR);
863                         strb.append(c);
864                         state = ParseState.CONST;
865                     }
866                     break;
867                 case DEFERRED0: // #
868                     if (c == '{') {
869                         state = ParseState.DEFERRED1;
870                         // if chars in buffer, create constant
871                         if (strb.length() > 0) {
872                             Expression cexpr = new ConstantExpression(strb.toString(), null);
873                             builder.add(cexpr);
874                             strb.delete(0, Integer.MAX_VALUE);
875                         }
876                     } else {
877                         // revert to CONST
878                         strb.append(DEF_CHAR);
879                         strb.append(c);
880                         state = ParseState.CONST;
881                     }
882                     break;
883                 case IMMEDIATE1: // ${...
884                     if (c == '}') {
885                         // materialize the immediate expr
886                         Expression iexpr = new ImmediateExpression(
887                                 strb.toString(),
888                                 jexl.parse(strb, null, scope),
889                                 null);
890                         builder.add(iexpr);
891                         strb.delete(0, Integer.MAX_VALUE);
892                         state = ParseState.CONST;
893                     } else {
894                         // do buildup expr
895                         strb.append(c);
896                     }
897                     break;
898                 case DEFERRED1: // #{...
899                     // skip inner strings (for '}')
900                     if (c == '"' || c == '\'') {
901                         strb.append(c);
902                         i = StringParser.readString(strb, expr, i + 1, c);
903                         continue;
904                     }
905                     // nested immediate in deferred; need to balance count of '{' & '}'
906                     if (c == '{') {
907                         if (expr.charAt(i - 1) == IMM_CHAR) {
908                             inner += 1;
909                             strb.deleteCharAt(strb.length() - 1);
910                             nested = true;
911                         }
912                         continue;
913                     }
914                     // closing '}'
915                     if (c == '}') {
916                         // balance nested immediate
917                         if (inner > 0) {
918                             inner -= 1;
919                         } else {
920                             // materialize the nested/deferred expr
921                             Expression dexpr = null;
922                             if (nested) {
923                                 dexpr = new NestedExpression(
924                                         expr.substring(inested, i + 1),
925                                         jexl.parse(strb, null, scope),
926                                         null);
927                             } else {
928                                 dexpr = new DeferredExpression(
929                                         strb.toString(),
930                                         jexl.parse(strb, null, scope),
931                                         null);
932                             }
933                             builder.add(dexpr);
934                             strb.delete(0, Integer.MAX_VALUE);
935                             nested = false;
936                             state = ParseState.CONST;
937                         }
938                     } else {
939                         // do buildup expr
940                         strb.append(c);
941                     }
942                     break;
943                 case ESCAPE:
944                     if (c == DEF_CHAR) {
945                         strb.append(DEF_CHAR);
946                     } else if (c == IMM_CHAR) {
947                         strb.append(IMM_CHAR);
948                     } else {
949                         strb.append('\\');
950                         strb.append(c);
951                     }
952                     state = ParseState.CONST;
953             }
954         }
955         // we should be in that state
956         if (state != ParseState.CONST) {
957             throw new Exception("malformed expression: " + expr, null);
958         }
959         // if any chars were buffered, add them as a constant
960         if (strb.length() > 0) {
961             Expression cexpr = new ConstantExpression(strb.toString(), null);
962             builder.add(cexpr);
963         }
964         return builder.build(this, null);
965     }
966 
967     /**
968      * The enum capturing the difference between verbatim and code source fragments.
969      */
970     private static enum BlockType {
971         /** Block is to be output "as is". */
972         VERBATIM,
973         /** Block is a directive, ie a fragment of code. */
974         DIRECTIVE;
975     }
976 
977     /**
978      * Abstract the source fragments, verbatim or immediate typed text blocks.
979      * @since 2.1
980      */
981     private static final class TemplateBlock {
982         /** The type of block, verbatim or directive. */
983         private final BlockType type;
984         /** The actual contexnt. */
985         private final String body;
986 
987         /**
988          * Creates a new block. 
989          * @param theType the type
990          * @param theBlock the content
991          */
992         TemplateBlock(BlockType theType, String theBlock) {
993             type = theType;
994             body = theBlock;
995         }
996 
997         @Override
998         public String toString() {
999             return body;
1000         }
1001     }
1002 
1003     /**
1004      * A Template is a script that evaluates by writing its content through a Writer.
1005      * This is a simplified replacement for Velocity that uses JEXL (instead of OGNL/VTL) as the scripting
1006      * language.
1007      * <p>
1008      * The source text is parsed considering each line beginning with '$$' (as default pattern) as JEXL script code
1009      * and all others as Unified JEXL expressions; those expressions will be invoked from the script during
1010      * evaluation and their output gathered through a writer. 
1011      * It is thus possible to use looping or conditional construct "around" expressions generating output.
1012      * </p>
1013      * For instance:
1014      * <p><blockquote><pre>
1015      * $$ for(var x : [1, 3, 5, 42, 169]) {
1016      * $$   if (x == 42) {
1017      * Life, the universe, and everything
1018      * $$   } else if (x > 42) {
1019      * The value $(x} is over fourty-two
1020      * $$   } else {
1021      * The value ${x} is under fourty-two
1022      * $$   }
1023      * $$ }
1024      * </pre></blockquote>
1025      * Will evaluate as:
1026      * <p><blockquote><pre>
1027      * The value 1 is under fourty-two
1028      * The value 3 is under fourty-two
1029      * The value 5 is under fourty-two
1030      * Life, the universe, and everything
1031      * The value 169 is over fourty-two
1032      * </pre></blockquote>
1033      * <p>
1034      * During evaluation, the template context exposes its writer as '$jexl' which is safe to use in this case.
1035      * This allows writing directly through the writer without adding new-lines as in:
1036      * <p><blockquote><pre>
1037      * $$ for(var cell : cells) { $jexl.print(cell); $jexl.print(';') }
1038      * </pre></blockquote>
1039      * </p>
1040      * <p>
1041      * A template is expanded as one JEXL script and a list of UnifiedJEXL expressions; each UnifiedJEXL expression
1042      * being replace in the script by a call to jexl:print(expr) (the expr is in fact the expr number in the template).
1043      * This integration uses a specialized JexlContext (TemplateContext) that serves as a namespace (for jexl:)
1044      * and stores the expression array and the writer (java.io.Writer) that the 'jexl:print(...)'
1045      * delegates the output generation to.
1046      * </p>
1047      * @since 2.1
1048      */
1049     public final class Template {
1050         /** The prefix marker. */
1051         private final String prefix;
1052         /** The array of source blocks. */
1053         private final TemplateBlock[] source;
1054         /** The resulting script. */
1055         private final ASTJexlScript script;
1056         /** The UnifiedJEXL expressions called by the script. */
1057         private final Expression[] exprs;
1058 
1059         /**
1060          * Creates a new template from an input.
1061          * @param directive the prefix for lines of code; can not be "$", "${", "#" or "#{"
1062          * since this would preclude being able to differentiate directives and UnifiedJEXL expressions
1063          * @param reader the input reader
1064          * @param parms the parameter names
1065          * @throws NullPointerException if either the directive prefix or input is null
1066          * @throws IllegalArgumentException if the directive prefix is invalid
1067          */
1068         public Template(String directive, Reader reader, String... parms) {
1069             if (directive == null) {
1070                 throw new NullPointerException("null prefix");
1071             }
1072             if ("$".equals(directive)
1073                     || "${".equals(directive)
1074                     || "#".equals(directive)
1075                     || "#{".equals(directive)) {
1076                 throw new IllegalArgumentException(directive + ": is not a valid directive pattern");
1077             }
1078             if (reader == null) {
1079                 throw new NullPointerException("null input");
1080             }
1081             JexlEngine.Scope scope = new JexlEngine.Scope(parms);
1082             prefix = directive;
1083             List<TemplateBlock> blocks = readTemplate(prefix, reader);
1084             List<Expression> uexprs = new ArrayList<Expression>();
1085             StringBuilder strb = new StringBuilder();
1086             int nuexpr = 0;
1087             int codeStart = -1;
1088             for (int b = 0; b < blocks.size(); ++b) {
1089                 TemplateBlock block = blocks.get(b);
1090                 if (block.type == BlockType.VERBATIM) {
1091                     strb.append("jexl:print(");
1092                     strb.append(nuexpr++);
1093                     strb.append(");");
1094                 } else {
1095                     // keep track of first block of code, the frame creator
1096                     if (codeStart < 0) {
1097                         codeStart = b;
1098                     }
1099                     strb.append(block.body);
1100                 }
1101             }
1102             // parse the script
1103             script = getEngine().parse(strb.toString(), null, scope);
1104             scope = script.getScope();
1105             // parse the exprs using the code frame for those appearing after the first block of code
1106             for (int b = 0; b < blocks.size(); ++b) {
1107                 TemplateBlock block = blocks.get(b);
1108                 if (block.type == BlockType.VERBATIM) {
1109                     uexprs.add(UnifiedJEXL.this.parseExpression(block.body, b > codeStart ? scope : null));
1110                 }
1111             }
1112             source = blocks.toArray(new TemplateBlock[blocks.size()]);
1113             exprs = uexprs.toArray(new Expression[uexprs.size()]);
1114         }
1115 
1116         /**
1117          * Private ctor used to expand deferred expressions during prepare.
1118          * @param thePrefix the directive prefix
1119          * @param theSource the source
1120          * @param theScript the script
1121          * @param theExprs the expressions
1122          */
1123         private Template(String thePrefix, TemplateBlock[] theSource, ASTJexlScript theScript, Expression[] theExprs) {
1124             prefix = thePrefix;
1125             source = theSource;
1126             script = theScript;
1127             exprs = theExprs;
1128         }
1129 
1130         @Override
1131         public String toString() {
1132             StringBuilder strb = new StringBuilder();
1133             for (TemplateBlock block : source) {
1134                 if (block.type == BlockType.DIRECTIVE) {
1135                     strb.append(prefix);
1136                 }
1137                 strb.append(block.toString());
1138                 strb.append('\n');
1139             }
1140             return strb.toString();
1141         }
1142 
1143         /**
1144          * Recreate the template source from its inner components.
1145          * @return the template source rewritten
1146          */
1147         public String asString() {
1148             StringBuilder strb = new StringBuilder();
1149             int e = 0;
1150             for (int b = 0; b < source.length; ++b) {
1151                 TemplateBlock block = source[b];
1152                 if (block.type == BlockType.DIRECTIVE) {
1153                     strb.append(prefix);
1154                 } else {
1155                     exprs[e++].asString(strb);
1156                 }
1157             }
1158             return strb.toString();
1159         }
1160 
1161         /**
1162          * Prepares this template by expanding any contained deferred expression.
1163          * @param context the context to prepare against
1164          * @return the prepared version of the template
1165          */
1166         public Template prepare(JexlContext context) {
1167             JexlEngine.Frame frame = script.createFrame((Object[]) null);
1168             TemplateContext tcontext = new TemplateContext(context, frame, exprs, null);
1169             Expression[] immediates = new Expression[exprs.length];
1170             for (int e = 0; e < exprs.length; ++e) {
1171                 immediates[e] = exprs[e].prepare(tcontext);
1172             }
1173             return new Template(prefix, source, script, immediates);
1174         }
1175 
1176         /**
1177          * Evaluates this template.
1178          * @param context the context to use during evaluation
1179          * @param writer the writer to use for output
1180          */
1181         public void evaluate(JexlContext context, Writer writer) {
1182             evaluate(context, writer, (Object[]) null);
1183         }
1184 
1185         /**
1186          * Evaluates this template.
1187          * @param context the context to use during evaluation
1188          * @param writer the writer to use for output
1189          * @param args the arguments
1190          */
1191         public void evaluate(JexlContext context, Writer writer, Object... args) {
1192             JexlEngine.Frame frame = script.createFrame(args);
1193             TemplateContext tcontext = new TemplateContext(context, frame, exprs, writer);
1194             Interpreter interpreter = jexl.createInterpreter(tcontext, !jexl.isLenient(), false);
1195             interpreter.setFrame(frame);
1196             interpreter.interpret(script);
1197         }
1198     }
1199 
1200     /**
1201      * The type of context to use during evaluation of templates.
1202      * <p>This context exposes its writer as '$jexl' to the scripts.</p>
1203      * <p>public for introspection purpose.</p>
1204      * @since 2.1
1205      */
1206     public final class TemplateContext implements JexlContext, NamespaceResolver {
1207         /** The wrapped context. */
1208         private final JexlContext wrap;
1209         /** The array of UnifiedJEXL expressions. */
1210         private final Expression[] exprs;
1211         /** The writer used to output. */
1212         private final Writer writer;
1213         /** The call frame. */
1214         private final JexlEngine.Frame frame;
1215 
1216         /**
1217          * Creates a template context instance.
1218          * @param jcontext the base context
1219          * @param jframe the calling frame
1220          * @param expressions the list of expression from the template to evaluate
1221          * @param out the output writer
1222          */
1223         protected TemplateContext(JexlContext jcontext, JexlEngine.Frame jframe, Expression[] expressions, Writer out) {
1224             wrap = jcontext;
1225             frame = jframe;
1226             exprs = expressions;
1227             writer = out;
1228         }
1229 
1230         /**
1231          * Gets this context calling frame.
1232          * @return the engine frame
1233          */
1234         public JexlEngine.Frame getFrame() {
1235             return frame;
1236         }
1237 
1238         /** {@inheritDoc} */
1239         public Object get(String name) {
1240             if ("$jexl".equals(name)) {
1241                 return writer;
1242             } else {
1243                 return wrap.get(name);
1244             }
1245         }
1246 
1247         /** {@inheritDoc} */
1248         public void set(String name, Object value) {
1249             wrap.set(name, value);
1250         }
1251 
1252         /** {@inheritDoc} */
1253         public boolean has(String name) {
1254             return wrap.has(name);
1255         }
1256 
1257         /** {@inheritDoc} */
1258         public Object resolveNamespace(String ns) {
1259             if ("jexl".equals(ns)) {
1260                 return this;
1261             } else if (wrap instanceof NamespaceResolver) {
1262                 return ((NamespaceResolver) wrap).resolveNamespace(ns);
1263             } else {
1264                 return null;
1265             }
1266         }
1267 
1268         /**
1269          * Includes a call to another template.
1270          * <p>Evaluates a template using this template initial context and writer.</p>
1271          * @param template the template to evaluate
1272          * @param args the arguments
1273          */
1274         public void include(Template template, Object... args) {
1275             template.evaluate(wrap, writer, args);
1276         }
1277 
1278         /**
1279          * Prints an expression result.
1280          * @param e the expression number
1281          */
1282         public void print(int e) {
1283             if (e < 0 || e >= exprs.length) {
1284                 return;
1285             }
1286             Expression expr = exprs[e];
1287             if (expr.isDeferred()) {
1288                 expr = expr.prepare(wrap);
1289             }
1290             if (expr instanceof CompositeExpression) {
1291                 printComposite((CompositeExpression) expr);
1292             } else {
1293                 doPrint(expr.evaluate(this));
1294             }
1295         }
1296 
1297         /**
1298          * Prints a composite expression.
1299          * @param composite the composite expression
1300          */
1301         protected void printComposite(CompositeExpression composite) {
1302             Expression[] cexprs = composite.exprs;
1303             final int size = cexprs.length;
1304             Object value = null;
1305             for (int e = 0; e < size; ++e) {
1306                 value = cexprs[e].evaluate(this);
1307                 doPrint(value);
1308             }
1309         }
1310 
1311         /**
1312          * Prints to output.
1313          * <p>This will dynamically try to find the best suitable method in the writer through uberspection.
1314          * Subclassing Writer by adding 'print' methods should be the preferred way to specialize output.
1315          * </p>
1316          * @param arg the argument to print out
1317          */
1318         private void doPrint(Object arg) {
1319             try {
1320                 if (arg instanceof CharSequence) {
1321                     writer.write(arg.toString());
1322                 } else if (arg != null) {
1323                     Object[] value = {arg};
1324                     Uberspect uber = getEngine().getUberspect();
1325                     JexlMethod method = uber.getMethod(writer, "print", value, null);
1326                     if (method != null) {
1327                         method.invoke(writer, value);
1328                     } else {
1329                         writer.write(arg.toString());
1330                     }
1331                 }
1332             } catch (java.io.IOException xio) {
1333                 throw createException("call print", null, xio);
1334             } catch (java.lang.Exception xany) {
1335                 throw createException("invoke print", null, xany);
1336             }
1337         }
1338     }
1339 
1340     /**
1341      * Whether a sequence starts with a given set of characters (following spaces).
1342      * <p>Space characters at beginning of line before the pattern are discarded.</p>
1343      * @param sequence the sequence
1344      * @param pattern the pattern to match at start of sequence
1345      * @return the first position after end of pattern if it matches, -1 otherwise
1346      * @since 2.1
1347      */
1348     protected int startsWith(CharSequence sequence, CharSequence pattern) {
1349         int s = 0;
1350         while (Character.isSpaceChar(sequence.charAt(s))) {
1351             s += 1;
1352         }
1353         sequence = sequence.subSequence(s, sequence.length());
1354         if (pattern.length() <= sequence.length()
1355                 && sequence.subSequence(0, pattern.length()).equals(pattern)) {
1356             return s + pattern.length();
1357         } else {
1358             return -1;
1359         }
1360     }
1361 
1362     /**
1363      * Reads lines of a template grouping them by typed blocks.
1364      * @param prefix the directive prefix
1365      * @param source the source reader
1366      * @return the list of blocks
1367      * @since 2.1
1368      */
1369     protected List<TemplateBlock> readTemplate(final String prefix, Reader source) {
1370         try {
1371             int prefixLen = prefix.length();
1372             List<TemplateBlock> blocks = new ArrayList<TemplateBlock>();
1373             BufferedReader reader;
1374             if (source instanceof BufferedReader) {
1375                 reader = (BufferedReader) source;
1376             } else {
1377                 reader = new BufferedReader(source);
1378             }
1379             StringBuilder strb = new StringBuilder();
1380             BlockType type = null;
1381             while (true) {
1382                 CharSequence line = reader.readLine();
1383                 if (line == null) {
1384                     // at end
1385                     TemplateBlock block = new TemplateBlock(type, strb.toString());
1386                     blocks.add(block);
1387                     break;
1388                 } else if (type == null) {
1389                     // determine starting type if not known yet
1390                     prefixLen = startsWith(line, prefix);
1391                     if (prefixLen >= 0) {
1392                         type = BlockType.DIRECTIVE;
1393                         strb.append(line.subSequence(prefixLen, line.length()));
1394                     } else {
1395                         type = BlockType.VERBATIM;
1396                         strb.append(line.subSequence(0, line.length()));
1397                         strb.append('\n');
1398                     }
1399                 } else if (type == BlockType.DIRECTIVE) {
1400                     // switch to verbatim if necessary
1401                     prefixLen = startsWith(line, prefix);
1402                     if (prefixLen < 0) {
1403                         TemplateBlock code = new TemplateBlock(BlockType.DIRECTIVE, strb.toString());
1404                         strb.delete(0, Integer.MAX_VALUE);
1405                         blocks.add(code);
1406                         type = BlockType.VERBATIM;
1407                         strb.append(line.subSequence(0, line.length()));
1408                     } else {
1409                         strb.append(line.subSequence(prefixLen, line.length()));
1410                     }
1411                 } else if (type == BlockType.VERBATIM) {
1412                     // switch to code if necessary(
1413                     prefixLen = startsWith(line, prefix);
1414                     if (prefixLen >= 0) {
1415                         strb.append('\n');
1416                         TemplateBlock verbatim = new TemplateBlock(BlockType.VERBATIM, strb.toString());
1417                         strb.delete(0, Integer.MAX_VALUE);
1418                         blocks.add(verbatim);
1419                         type = BlockType.DIRECTIVE;
1420                         strb.append(line.subSequence(prefixLen, line.length()));
1421                     } else {
1422                         strb.append(line.subSequence(0, line.length()));
1423                     }
1424                 }
1425             }
1426             return blocks;
1427         } catch (IOException xio) {
1428             return null;
1429         }
1430     }
1431 
1432     /**
1433      * Creates a new template.
1434      * @param prefix the directive prefix
1435      * @param source the source
1436      * @param parms the parameter names
1437      * @return the template
1438      * @since 2.1
1439      */
1440     public Template createTemplate(String prefix, Reader source, String... parms) {
1441         return new Template(prefix, source, parms);
1442     }
1443 
1444     /**
1445      * Creates a new template.
1446      * @param source the source
1447      * @param parms the parameter names
1448      * @return the template
1449      * @since 2.1
1450      */
1451     public Template createTemplate(String source, String... parms) {
1452         return new Template("$$", new StringReader(source), parms);
1453     }
1454 
1455     /**
1456      * Creates a new template.
1457      * @param source the source
1458      * @return the template
1459      * @since 2.1
1460      */
1461     public Template createTemplate(String source) {
1462         return new Template("$$", new StringReader(source), (String[]) null);
1463     }
1464 }