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 */
017package org.apache.commons.jexl2;
018
019import java.io.BufferedReader;
020import java.io.IOException;
021import java.io.Reader;
022import java.io.StringReader;
023import java.io.Writer;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.LinkedHashSet;
027import java.util.List;
028import java.util.Set;
029import org.apache.commons.jexl2.introspection.JexlMethod;
030import org.apache.commons.jexl2.introspection.Uberspect;
031import org.apache.commons.jexl2.parser.ASTJexlScript;
032import org.apache.commons.jexl2.parser.JexlNode;
033import 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 * </p>
042 * <ul>
043 * <li>The "immediate" syntax is of the form <code>"...${jexl-expr}..."</code></li>
044 * <li>The "deferred" syntax is of the form <code>"...#{jexl-expr}..."</code></li>
045 * <li>The "nested" syntax is of the form <code>"...#{...${jexl-expr0}...}..."</code></li>
046 * <li>The "composite" syntax is of the form <code>"...${jexl-expr0}... #{jexl-expr1}..."</code></li>
047 * </ul>
048 * <p>
049 * Deferred &amp; immediate expression carry different intentions:
050 * </p>
051 * <ul>
052 * <li>An immediate expression indicate that evaluation is intended to be performed close to
053 * the definition/parsing point.</li>
054 * <li>A deferred expression indicate that evaluation is intended to occur at a later stage.</li>
055 * </ul>
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 (&amp; 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 &amp; 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 */
087public 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 &amp; composite expressions that contain differed &amp; 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 &amp; 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 &amp; 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 &amp; 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     * <p>For instance:
1014     * </p>
1015     * <blockquote><pre>
1016     * $$ for(var x : [1, 3, 5, 42, 169]) {
1017     * $$   if (x == 42) {
1018     * Life, the universe, and everything
1019     * $$   } else if (x &gt; 42) {
1020     * The value $(x} is over fourty-two
1021     * $$   } else {
1022     * The value ${x} is under fourty-two
1023     * $$   }
1024     * $$ }
1025     * </pre></blockquote>
1026     * <p>
1027     * Will evaluate as:
1028     * </p>
1029     * <blockquote><pre>
1030     * The value 1 is under fourty-two
1031     * The value 3 is under fourty-two
1032     * The value 5 is under fourty-two
1033     * Life, the universe, and everything
1034     * The value 169 is over fourty-two
1035     * </pre></blockquote>
1036     * <p>
1037     * During evaluation, the template context exposes its writer as '$jexl' which is safe to use in this case.
1038     * This allows writing directly through the writer without adding new-lines as in:
1039     * </p>
1040     * <blockquote><pre>
1041     * $$ for(var cell : cells) { $jexl.print(cell); $jexl.print(';') }
1042     * </pre></blockquote>
1043     * <p>
1044     * A template is expanded as one JEXL script and a list of UnifiedJEXL expressions; each UnifiedJEXL expression
1045     * being replace in the script by a call to jexl:print(expr) (the expr is in fact the expr number in the template).
1046     * This integration uses a specialized JexlContext (TemplateContext) that serves as a namespace (for jexl:)
1047     * and stores the expression array and the writer (java.io.Writer) that the 'jexl:print(...)'
1048     * delegates the output generation to.
1049     * </p>
1050     * @since 2.1
1051     */
1052    public final class Template {
1053        /** The prefix marker. */
1054        private final String prefix;
1055        /** The array of source blocks. */
1056        private final TemplateBlock[] source;
1057        /** The resulting script. */
1058        private final ASTJexlScript script;
1059        /** The UnifiedJEXL expressions called by the script. */
1060        private final Expression[] exprs;
1061
1062        /**
1063         * Creates a new template from an input.
1064         * @param directive the prefix for lines of code; can not be "$", "${", "#" or "#{"
1065         * since this would preclude being able to differentiate directives and UnifiedJEXL expressions
1066         * @param reader the input reader
1067         * @param parms the parameter names
1068         * @throws NullPointerException if either the directive prefix or input is null
1069         * @throws IllegalArgumentException if the directive prefix is invalid
1070         */
1071        public Template(String directive, Reader reader, String... parms) {
1072            if (directive == null) {
1073                throw new NullPointerException("null prefix");
1074            }
1075            if ("$".equals(directive)
1076                    || "${".equals(directive)
1077                    || "#".equals(directive)
1078                    || "#{".equals(directive)) {
1079                throw new IllegalArgumentException(directive + ": is not a valid directive pattern");
1080            }
1081            if (reader == null) {
1082                throw new NullPointerException("null input");
1083            }
1084            JexlEngine.Scope scope = new JexlEngine.Scope(parms);
1085            prefix = directive;
1086            List<TemplateBlock> blocks = readTemplate(prefix, reader);
1087            List<Expression> uexprs = new ArrayList<Expression>();
1088            StringBuilder strb = new StringBuilder();
1089            int nuexpr = 0;
1090            int codeStart = -1;
1091            for (int b = 0; b < blocks.size(); ++b) {
1092                TemplateBlock block = blocks.get(b);
1093                if (block.type == BlockType.VERBATIM) {
1094                    strb.append("jexl:print(");
1095                    strb.append(nuexpr++);
1096                    strb.append(");");
1097                } else {
1098                    // keep track of first block of code, the frame creator
1099                    if (codeStart < 0) {
1100                        codeStart = b;
1101                    }
1102                    strb.append(block.body);
1103                }
1104            }
1105            // parse the script
1106            script = getEngine().parse(strb.toString(), null, scope);
1107            scope = script.getScope();
1108            // parse the exprs using the code frame for those appearing after the first block of code
1109            for (int b = 0; b < blocks.size(); ++b) {
1110                TemplateBlock block = blocks.get(b);
1111                if (block.type == BlockType.VERBATIM) {
1112                    uexprs.add(UnifiedJEXL.this.parseExpression(block.body, b > codeStart ? scope : null));
1113                }
1114            }
1115            source = blocks.toArray(new TemplateBlock[blocks.size()]);
1116            exprs = uexprs.toArray(new Expression[uexprs.size()]);
1117        }
1118
1119        /**
1120         * Private ctor used to expand deferred expressions during prepare.
1121         * @param thePrefix the directive prefix
1122         * @param theSource the source
1123         * @param theScript the script
1124         * @param theExprs the expressions
1125         */
1126        private Template(String thePrefix, TemplateBlock[] theSource, ASTJexlScript theScript, Expression[] theExprs) {
1127            prefix = thePrefix;
1128            source = theSource;
1129            script = theScript;
1130            exprs = theExprs;
1131        }
1132
1133        @Override
1134        public String toString() {
1135            StringBuilder strb = new StringBuilder();
1136            for (TemplateBlock block : source) {
1137                if (block.type == BlockType.DIRECTIVE) {
1138                    strb.append(prefix);
1139                }
1140                strb.append(block.toString());
1141                strb.append('\n');
1142            }
1143            return strb.toString();
1144        }
1145
1146        /**
1147         * Recreate the template source from its inner components.
1148         * @return the template source rewritten
1149         */
1150        public String asString() {
1151            StringBuilder strb = new StringBuilder();
1152            int e = 0;
1153            for (int b = 0; b < source.length; ++b) {
1154                TemplateBlock block = source[b];
1155                if (block.type == BlockType.DIRECTIVE) {
1156                    strb.append(prefix);
1157                } else {
1158                    exprs[e++].asString(strb);
1159                }
1160            }
1161            return strb.toString();
1162        }
1163
1164        /**
1165         * Prepares this template by expanding any contained deferred expression.
1166         * @param context the context to prepare against
1167         * @return the prepared version of the template
1168         */
1169        public Template prepare(JexlContext context) {
1170            JexlEngine.Frame frame = script.createFrame((Object[]) null);
1171            TemplateContext tcontext = new TemplateContext(context, frame, exprs, null);
1172            Expression[] immediates = new Expression[exprs.length];
1173            for (int e = 0; e < exprs.length; ++e) {
1174                immediates[e] = exprs[e].prepare(tcontext);
1175            }
1176            return new Template(prefix, source, script, immediates);
1177        }
1178
1179        /**
1180         * Evaluates this template.
1181         * @param context the context to use during evaluation
1182         * @param writer the writer to use for output
1183         */
1184        public void evaluate(JexlContext context, Writer writer) {
1185            evaluate(context, writer, (Object[]) null);
1186        }
1187
1188        /**
1189         * Evaluates this template.
1190         * @param context the context to use during evaluation
1191         * @param writer the writer to use for output
1192         * @param args the arguments
1193         */
1194        public void evaluate(JexlContext context, Writer writer, Object... args) {
1195            JexlEngine.Frame frame = script.createFrame(args);
1196            TemplateContext tcontext = new TemplateContext(context, frame, exprs, writer);
1197            Interpreter interpreter = jexl.createInterpreter(tcontext, !jexl.isLenient(), false);
1198            interpreter.setFrame(frame);
1199            interpreter.interpret(script);
1200        }
1201    }
1202
1203    /**
1204     * The type of context to use during evaluation of templates.
1205     * <p>This context exposes its writer as '$jexl' to the scripts.</p>
1206     * <p>public for introspection purpose.</p>
1207     * @since 2.1
1208     */
1209    public final class TemplateContext implements JexlContext, NamespaceResolver {
1210        /** The wrapped context. */
1211        private final JexlContext wrap;
1212        /** The array of UnifiedJEXL expressions. */
1213        private final Expression[] exprs;
1214        /** The writer used to output. */
1215        private final Writer writer;
1216        /** The call frame. */
1217        private final JexlEngine.Frame frame;
1218
1219        /**
1220         * Creates a template context instance.
1221         * @param jcontext the base context
1222         * @param jframe the calling frame
1223         * @param expressions the list of expression from the template to evaluate
1224         * @param out the output writer
1225         */
1226        protected TemplateContext(JexlContext jcontext, JexlEngine.Frame jframe, Expression[] expressions, Writer out) {
1227            wrap = jcontext;
1228            frame = jframe;
1229            exprs = expressions;
1230            writer = out;
1231        }
1232
1233        /**
1234         * Gets this context calling frame.
1235         * @return the engine frame
1236         */
1237        public JexlEngine.Frame getFrame() {
1238            return frame;
1239        }
1240
1241        /** {@inheritDoc} */
1242        public Object get(String name) {
1243            if ("$jexl".equals(name)) {
1244                return writer;
1245            } else {
1246                return wrap.get(name);
1247            }
1248        }
1249
1250        /** {@inheritDoc} */
1251        public void set(String name, Object value) {
1252            wrap.set(name, value);
1253        }
1254
1255        /** {@inheritDoc} */
1256        public boolean has(String name) {
1257            return wrap.has(name);
1258        }
1259
1260        /** {@inheritDoc} */
1261        public Object resolveNamespace(String ns) {
1262            if ("jexl".equals(ns)) {
1263                return this;
1264            } else if (wrap instanceof NamespaceResolver) {
1265                return ((NamespaceResolver) wrap).resolveNamespace(ns);
1266            } else {
1267                return null;
1268            }
1269        }
1270
1271        /**
1272         * Includes a call to another template.
1273         * <p>Evaluates a template using this template initial context and writer.</p>
1274         * @param template the template to evaluate
1275         * @param args the arguments
1276         */
1277        public void include(Template template, Object... args) {
1278            template.evaluate(wrap, writer, args);
1279        }
1280
1281        /**
1282         * Prints an expression result.
1283         * @param e the expression number
1284         */
1285        public void print(int e) {
1286            if (e < 0 || e >= exprs.length) {
1287                return;
1288            }
1289            Expression expr = exprs[e];
1290            if (expr.isDeferred()) {
1291                expr = expr.prepare(wrap);
1292            }
1293            if (expr instanceof CompositeExpression) {
1294                printComposite((CompositeExpression) expr);
1295            } else {
1296                doPrint(expr.evaluate(this));
1297            }
1298        }
1299
1300        /**
1301         * Prints a composite expression.
1302         * @param composite the composite expression
1303         */
1304        protected void printComposite(CompositeExpression composite) {
1305            Expression[] cexprs = composite.exprs;
1306            final int size = cexprs.length;
1307            Object value = null;
1308            for (int e = 0; e < size; ++e) {
1309                value = cexprs[e].evaluate(this);
1310                doPrint(value);
1311            }
1312        }
1313
1314        /**
1315         * Prints to output.
1316         * <p>This will dynamically try to find the best suitable method in the writer through uberspection.
1317         * Subclassing Writer by adding 'print' methods should be the preferred way to specialize output.
1318         * </p>
1319         * @param arg the argument to print out
1320         */
1321        private void doPrint(Object arg) {
1322            try {
1323                if (arg instanceof CharSequence) {
1324                    writer.write(arg.toString());
1325                } else if (arg != null) {
1326                    Object[] value = {arg};
1327                    Uberspect uber = getEngine().getUberspect();
1328                    JexlMethod method = uber.getMethod(writer, "print", value, null);
1329                    if (method != null) {
1330                        method.invoke(writer, value);
1331                    } else {
1332                        writer.write(arg.toString());
1333                    }
1334                }
1335            } catch (java.io.IOException xio) {
1336                throw createException("call print", null, xio);
1337            } catch (java.lang.Exception xany) {
1338                throw createException("invoke print", null, xany);
1339            }
1340        }
1341    }
1342
1343    /**
1344     * Whether a sequence starts with a given set of characters (following spaces).
1345     * <p>Space characters at beginning of line before the pattern are discarded.</p>
1346     * @param sequence the sequence
1347     * @param pattern the pattern to match at start of sequence
1348     * @return the first position after end of pattern if it matches, -1 otherwise
1349     * @since 2.1
1350     */
1351    protected int startsWith(CharSequence sequence, CharSequence pattern) {
1352        int s = 0;
1353        while (Character.isSpaceChar(sequence.charAt(s))) {
1354            s += 1;
1355        }
1356        sequence = sequence.subSequence(s, sequence.length());
1357        if (pattern.length() <= sequence.length()
1358                && sequence.subSequence(0, pattern.length()).equals(pattern)) {
1359            return s + pattern.length();
1360        } else {
1361            return -1;
1362        }
1363    }
1364
1365    /**
1366     * Reads lines of a template grouping them by typed blocks.
1367     * @param prefix the directive prefix
1368     * @param source the source reader
1369     * @return the list of blocks
1370     * @since 2.1
1371     */
1372    protected List<TemplateBlock> readTemplate(final String prefix, Reader source) {
1373        try {
1374            int prefixLen = prefix.length();
1375            List<TemplateBlock> blocks = new ArrayList<TemplateBlock>();
1376            BufferedReader reader;
1377            if (source instanceof BufferedReader) {
1378                reader = (BufferedReader) source;
1379            } else {
1380                reader = new BufferedReader(source);
1381            }
1382            StringBuilder strb = new StringBuilder();
1383            BlockType type = null;
1384            while (true) {
1385                CharSequence line = reader.readLine();
1386                if (line == null) {
1387                    // at end
1388                    TemplateBlock block = new TemplateBlock(type, strb.toString());
1389                    blocks.add(block);
1390                    break;
1391                } else if (type == null) {
1392                    // determine starting type if not known yet
1393                    prefixLen = startsWith(line, prefix);
1394                    if (prefixLen >= 0) {
1395                        type = BlockType.DIRECTIVE;
1396                        strb.append(line.subSequence(prefixLen, line.length()));
1397                    } else {
1398                        type = BlockType.VERBATIM;
1399                        strb.append(line.subSequence(0, line.length()));
1400                        strb.append('\n');
1401                    }
1402                } else if (type == BlockType.DIRECTIVE) {
1403                    // switch to verbatim if necessary
1404                    prefixLen = startsWith(line, prefix);
1405                    if (prefixLen < 0) {
1406                        TemplateBlock code = new TemplateBlock(BlockType.DIRECTIVE, strb.toString());
1407                        strb.delete(0, Integer.MAX_VALUE);
1408                        blocks.add(code);
1409                        type = BlockType.VERBATIM;
1410                        strb.append(line.subSequence(0, line.length()));
1411                    } else {
1412                        strb.append(line.subSequence(prefixLen, line.length()));
1413                    }
1414                } else if (type == BlockType.VERBATIM) {
1415                    // switch to code if necessary(
1416                    prefixLen = startsWith(line, prefix);
1417                    if (prefixLen >= 0) {
1418                        strb.append('\n');
1419                        TemplateBlock verbatim = new TemplateBlock(BlockType.VERBATIM, strb.toString());
1420                        strb.delete(0, Integer.MAX_VALUE);
1421                        blocks.add(verbatim);
1422                        type = BlockType.DIRECTIVE;
1423                        strb.append(line.subSequence(prefixLen, line.length()));
1424                    } else {
1425                        strb.append(line.subSequence(0, line.length()));
1426                    }
1427                }
1428            }
1429            return blocks;
1430        } catch (IOException xio) {
1431            return null;
1432        }
1433    }
1434
1435    /**
1436     * Creates a new template.
1437     * @param prefix the directive prefix
1438     * @param source the source
1439     * @param parms the parameter names
1440     * @return the template
1441     * @since 2.1
1442     */
1443    public Template createTemplate(String prefix, Reader source, String... parms) {
1444        return new Template(prefix, source, parms);
1445    }
1446
1447    /**
1448     * Creates a new template.
1449     * @param source the source
1450     * @param parms the parameter names
1451     * @return the template
1452     * @since 2.1
1453     */
1454    public Template createTemplate(String source, String... parms) {
1455        return new Template("$$", new StringReader(source), parms);
1456    }
1457
1458    /**
1459     * Creates a new template.
1460     * @param source the source
1461     * @return the template
1462     * @since 2.1
1463     */
1464    public Template createTemplate(String source) {
1465        return new Template("$$", new StringReader(source), (String[]) null);
1466    }
1467}