001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.commons.jexl2;
018    
019    import java.io.BufferedReader;
020    import java.io.IOException;
021    import java.io.Reader;
022    import java.io.StringReader;
023    import java.io.Writer;
024    import java.util.ArrayList;
025    import java.util.Collections;
026    import java.util.LinkedHashSet;
027    import java.util.List;
028    import java.util.Set;
029    import org.apache.commons.jexl2.introspection.JexlMethod;
030    import org.apache.commons.jexl2.introspection.Uberspect;
031    import org.apache.commons.jexl2.parser.ASTJexlScript;
032    import org.apache.commons.jexl2.parser.JexlNode;
033    import org.apache.commons.jexl2.parser.StringParser;
034    
035    /**
036     * An evaluator similar to the Unified EL evaluator used in JSP/JSF based on JEXL.
037     * It is intended to be used in configuration modules, XML based frameworks or JSP taglibs
038     * and facilitate the implementation of expression evaluation.
039     * <p>
040     * An expression can mix immediate, deferred and nested sub-expressions as well as string constants;
041     * <ul>
042     * <li>The "immediate" syntax is of the form <code>"...${jexl-expr}..."</code></li>
043     * <li>The "deferred" syntax is of the form <code>"...#{jexl-expr}..."</code></li>
044     * <li>The "nested" syntax is of the form <code>"...#{...${jexl-expr0}...}..."</code></li>
045     * <li>The "composite" syntax is of the form <code>"...${jexl-expr0}... #{jexl-expr1}..."</code></li>
046     * </ul>
047     * </p>
048     * <p>
049     * Deferred & immediate expression carry different intentions:
050     * <ul>
051     * <li>An immediate expression indicate that evaluation is intended to be performed close to
052     * the definition/parsing point.</li>
053     * <li>A deferred expression indicate that evaluation is intended to occur at a later stage.</li>
054     * </ul>
055     * </p>
056     * <p>
057     * For instance: <code>"Hello ${name}, now is #{time}"</code> is a composite "deferred" expression since one
058     * of its subexpressions is deferred. Furthermore, this (composite) expression intent is
059     * to perform two evaluations; one close to its definition and another one in a later
060     * phase.
061     * </p>
062     * <p>
063     * The API reflects this feature in 2 methods, prepare and evaluate. The prepare method
064     * will evaluate the immediate subexpression and return an expression that contains only
065     * the deferred subexpressions (& constants), a prepared expression. Such a prepared expression
066     * is suitable for a later phase evaluation that may occur with a different JexlContext.
067     * Note that it is valid to call evaluate without prepare in which case the same JexlContext
068     * is used for the 2 evaluation phases.
069     * </p>
070     * <p>
071     * In the most common use-case where deferred expressions are to be kept around as properties of objects,
072     * one should parse & prepare an expression before storing it and evaluate it each time
073     * the property storing it is accessed.
074     * </p>
075     * <p>
076     * Note that nested expression use the JEXL syntax as in:
077     * <code>"#{${bar}+'.charAt(2)'}"</code>
078     * The most common mistake leading to an invalid expression being the following:
079     * <code>"#{${bar}charAt(2)}"</code>
080     * </p>
081     * <p>Also note that methods that parse evaluate expressions may throw <em>unchecked</em> exceptions;
082     * The {@link UnifiedJEXL.Exception} are thrown when the engine instance is in "non-silent" mode
083     * but since these are RuntimeException, user-code <em>should</em> catch them where appropriate.
084     * </p>
085     * @since 2.0
086     */
087    public final class UnifiedJEXL {
088        /** The JEXL engine instance. */
089        private final JexlEngine jexl;
090        /** The expression cache. */
091        private final JexlEngine.SoftCache<String, Expression> cache;
092        /** The default cache size. */
093        private static final int CACHE_SIZE = 256;
094        /** The first character for immediate expressions. */
095        private static final char IMM_CHAR = '$';
096        /** The first character for deferred expressions. */
097        private static final char DEF_CHAR = '#';
098    
099        /**
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    }