    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    }