    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.util.ArrayList;
    020    import org.apache.commons.jexl2.parser.JexlNode;
    021    import org.apache.commons.jexl2.parser.StringParser;
    022
    023    /**
    024     * An evaluator similar to the Unified EL evaluator used in JSP/JSF based on JEXL.
    025     * It is intended to be used in configuration modules, XML based frameworks or JSP taglibs
    026     * and facilitate the implementation of expression evaluation.
    027     * <p>
    028     * An expression can mix immediate, deferred and nested sub-expressions as well as string constants;
    029     * <ul>
    030     * <li>The "immediate" syntax is of the form <code>"...${jexl-expr}..."</code></li>
    031     * <li>The "deferred" syntax is of the form <code>"...#{jexl-expr}..."</code></li>
    032     * <li>The "nested" syntax is of the form <code>"...#{...${jexl-expr0}...}..."</code></li>
    033     * <li>The "composite" syntax is of the form <code>"...${jexl-expr0}... #{jexl-expr1}..."</code></li>
    034     * </ul>
    035     * </p>
    036     * <p>
    037     * Deferred & immediate expression carry different intentions:
    038     * <ul>
    039     * <li>An immediate expression indicate that evaluation is intended to be performed close to
    040     * the definition/parsing point.</li>
    041     * <li>A deferred expression indicate that evaluation is intended to occur at a later stage.</li>
    042     * </ul>
    043     * </p>
    044     * <p>
    045     * For instance: <code>"Hello ${name}, now is #{time}"</code> is a composite "deferred" expression since one
    046     * of its subexpressions is deferred. Furthermore, this (composite) expression intent is
    047     * to perform two evaluations; one close to its definition and another one in a later
    048     * phase.
    049     * </p>
    050     * <p>
    051     * The API reflects this feature in 2 methods, prepare and evaluate. The prepare method
    052     * will evaluate the immediate subexpression and return an expression that contains only
    053     * the deferred subexpressions (& constants), a prepared expression. Such a prepared expression
    054     * is suitable for a later phase evaluation that may occur with a different JexlContext.
    055     * Note that it is valid to call evaluate without prepare in which case the same JexlContext
    056     * is used for the 2 evaluation phases.
    057     * </p>
    058     * <p>
    059     * In the most common use-case where deferred expressions are to be kept around as properties of objects,
    060     * one should parse & prepare an expression before storing it and evaluate it each time
    061     * the property storing it is accessed.
    062     * </p>
    063     * <p>
    064     * Note that nested expression use the JEXL syntax as in:
    065     * <code>"#{${bar}+'.charAt(2)'}"</code>
    066     * The most common mistake leading to an invalid expression being the following:
    067     * <code>"#{${bar}charAt(2)}"</code>
    068     * </p>
    069     * <p>Also note that methods that parse evaluate expressions may throw <em>unchecked</em> ecxeptions;
    070     * The {@link UnifiedJEXL.Exception} are thrown when the engine instance is in "non-silent" mode
    071     * but since these are RuntimeException, user-code <em>should</em> catch them where appropriate.
    072     * </p>
    073     * @since 2.0
    074     */
    075    public final class UnifiedJEXL {
    076        /** The JEXL engine instance. */
    077        private final JexlEngine jexl;
    078        /** The expression cache. */
    079        private final JexlEngine.SoftCache<String,Expression> cache;
    080        /** The default cache size. */
    081        private static final int CACHE_SIZE = 256;
    082        /**
    083         * Creates a new instance of UnifiedJEXL with a default size cache.
    084         * @param aJexl the JexlEngine to use.
    085         */
    086        public UnifiedJEXL(JexlEngine aJexl) {
    087            this(aJexl, CACHE_SIZE);
    088        }
    089
    090        /**
    091         * Creates a new instance of UnifiedJEXL creating a local cache.
    092         * @param aJexl the JexlEngine to use.
    093         * @param cacheSize the number of expressions in this cache
    094         */
    095        public UnifiedJEXL(JexlEngine aJexl, int cacheSize) {
    096            this.jexl = aJexl;
    097            this.cache = aJexl.new SoftCache<String,Expression>(cacheSize);
    098        }
    099
    100        /**
    101         * Types of expressions.
    102         * Each instance carries a counter index per (composite sub-) expression type.
    103         * @see ExpressionBuilder
    104         */
    105        private static enum ExpressionType {
    106            /** Constant expression, count index 0. */
    107            CONSTANT(0),
    108            /** Immediate expression, count index 1. */
    109            IMMEDIATE(1),
    110            /** Deferred expression, count index 2. */
    111            DEFERRED(2),
    112            /** Nested (which are deferred) expressions, count index 2. */
    113            NESTED(2),
    114            /** Composite expressions are not counted, index -1. */
    115            COMPOSITE(-1);
    116            /** The index in arrays of expression counters for composite expressions. */
    117            private final int index;
    118            /**
    119             * Creates an ExpressionType.
    120             * @param idx the index for this type in counters arrays.
    121             */
    122            ExpressionType(int idx) {
    123                this.index = idx;
    124            }
    125        }
    126
    127        /**
    128         * A helper class to build expressions.
    129         * Keeps count of sub-expressions by type.
    130         */
    131        private static class ExpressionBuilder {
    132            /** Per expression type counters. */
    133            private final int[] counts;
    134            /** The list of expressions. */
    135            private final ArrayList<Expression> expressions;
    136
    137            /**
    138             * Creates a builder.
    139             * @param size the initial expression array size
    140             */
    141            ExpressionBuilder(int size) {
    142                counts = new int[]{0, 0, 0};
    143                expressions = new ArrayList<Expression>(size <= 0 ? 3 : size);
    144            }
    145
    146            /**
    147             * Adds an expression to the list of expressions, maintain per-type counts.
    148             * @param expr the expression to add
    149             */
    150            void add(Expression expr) {
    151                counts[expr.getType().index] += 1;
    152                expressions.add(expr);
    153            }
    154
    155            /**
    156             * Builds an expression from a source, performs checks.
    157             * @param el the unified el instance
    158             * @param source the source expression
    159             * @return an expression
    160             */
    161            Expression build(UnifiedJEXL el, Expression source) {
    162                int sum = 0;
    163                for (int count : counts) {
    164                    sum += count;
    165                }
    166                if (expressions.size() != sum) {
    167                    StringBuilder error = new StringBuilder("parsing algorithm error, exprs: ");
    168                    error.append(expressions.size());
    169                    error.append(", constant:");
    170                    error.append(counts[ExpressionType.CONSTANT.index]);
    171                    error.append(", immediate:");
    172                    error.append(counts[ExpressionType.IMMEDIATE.index]);
    173                    error.append(", deferred:");
    174                    error.append(counts[ExpressionType.DEFERRED.index]);
    175                    throw new IllegalStateException(error.toString());
    176                }
    177                // if only one sub-expr, no need to create a composite
    178                if (expressions.size() == 1) {
    179                    return expressions.get(0);
    180                } else {
    181                    return el.new CompositeExpression(counts, expressions, source);
    182                }
    183            }
    184        }
    185
    186        /**
    187         * Gets the JexlEngine underlying the UnifiedJEXL.
    188         * @return the JexlEngine
    189         */
    190        public JexlEngine getEngine() {
    191            return jexl;
    192        }
    193
    194        /**
    195         * The sole type of (runtime) exception the UnifiedJEXL can throw.
    196         */
    197        public static class Exception extends RuntimeException {
    198            /** Serial version UID. */
    199            private static final long serialVersionUID = -8201402995815975726L;
    200            /**
    201             * Creates a UnifiedJEXL.Exception.
    202             * @param msg the exception message
    203             * @param cause the exception cause
    204             */
    205            public Exception(String msg, Throwable cause) {
    206                super(msg, cause);
    207            }
    208        }
    209
    210        /**
    211         * The abstract base class for all expressions, immediate '${...}' and deferred '#{...}'.
    212         */
    213        public abstract class Expression {
    214            /** The source of this expression (see {@link UnifiedJEXL.Expression#prepare}). */
    215            protected final Expression source;
    216            /**
    217             * Creates an expression.
    218             * @param src the source expression if any
    219             */
    220            Expression(Expression src) {
    221                this.source = src != null ? src : this;
    222            }
    223
    224            /**
    225             * Formats this expression, adding its source string representation in
    226             * comments if available: 'expression /*= source *\/'' .
    227             * @return the formatted expression string
    228             */
    229            @Override
    230            public String toString() {
    231                StringBuilder strb = new StringBuilder();
    232                if (source != this) {
    233                    strb.append(source.toString());
    234                    strb.append(" /*= ");
    235                }
    236                asString(strb);
    237                if (source != this) {
    238                    strb.append(" */");
    239                }
    240                return strb.toString();
    241            }
    242
    243            /**
    244             * Generates this expression's string representation.
    245             * @return the string representation
    246             */
    247            public String asString() {
    248                StringBuilder strb = new StringBuilder();
    249                asString(strb);
    250                return strb.toString();
    251            }
    252
    253            /**
    254             * Adds this expression's string representation to a StringBuilder.
    255             * @param strb the builder to fill
    256             */
    257            abstract void asString(StringBuilder strb);
    258
    259            /**
    260             * When the expression is dependant upon immediate and deferred sub-expressions,
    261             * evaluates the immediate sub-expressions with the context passed as parameter
    262             * and returns this expression deferred form.
    263             * <p>
    264             * In effect, this binds the result of the immediate sub-expressions evaluation in the
    265             * context, allowing to differ evaluation of the remaining (deferred) expression within another context.
    266             * This only has an effect to nested & composite expressions that contain differed & immediate sub-expressions.
    267             * </p>
    268             * <p>
    269             * If the underlying JEXL engine is silent, errors will be logged through its logger as warning.
    270             * </p>
    271             * @param context the context to use for immediate expression evaluations
    272             * @return  an expression or null if an error occurs and the {@link JexlEngine} is silent
    273             * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent
    274             */
    275            public abstract Expression prepare(JexlContext context);
    276
    277            /**
    278             * Evaluates this expression.
    279             * <p>
    280             * If the underlying JEXL engine is silent, errors will be logged through its logger as warning.
    281             * </p>
    282             * @param context the variable context
    283             * @return the result of this expression evaluation or null if an error occurs and the {@link JexlEngine} is
    284             * silent
    285             * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent
    286             */
    287            public abstract Object evaluate(JexlContext context);
    288
    289            /**
    290             * Checks whether this expression is immediate.
    291             * @return true if immediate, false otherwise
    292             */
    293            public boolean isImmediate() {
    294                return true;
    295            }
    296
    297            /**
    298             * Checks whether this expression is deferred.
    299             * @return true if deferred, false otherwise
    300             */
    301            public final boolean isDeferred() {
    302                return !isImmediate();
    303            }
    304
    305            /**
    306             * Retrieves this expression's source expression.
    307             * If this expression was prepared, this allows to retrieve the
    308             * original expression that lead to it.
    309             * Other expressions return themselves.
    310             * @return the source expression
    311             */
    312            public final Expression getSource() {
    313                return source;
    314            }
    315
    316            /**
    317             * Gets this expression type.
    318             * @return its type
    319             */
    320            abstract ExpressionType getType();
    321
    322            /**
    323             * Prepares a sub-expression for interpretation.
    324             * @param interpreter a JEXL interpreter
    325             * @return a prepared expression
    326             * @throws JexlException (only for nested & composite)
    327             */
    328            abstract Expression prepare(Interpreter interpreter);
    329
    330            /**
    331             * Intreprets a sub-expression.
    332             * @param interpreter a JEXL interpreter
    333             * @return the result of interpretation
    334             * @throws JexlException (only for nested & composite)
    335             */
    336            abstract Object evaluate(Interpreter interpreter);
    337        }
    338
    339
    340        /** A constant expression. */
    341        private class ConstantExpression extends Expression {
    342            /** The constant held by this expression. */
    343            private final Object value;
    344            /**
    345             * Creates a constant expression.
    346             * <p>
    347             * If the wrapped constant is a string, it is treated
    348             * as a JEXL strings with respect to escaping.
    349             * </p>
    350             * @param val the constant value
    351             * @param source the source expression if any
    352             */
    353            ConstantExpression(Object val, Expression source) {
    354                super(source);
    355                if (val == null) {
    356                    throw new NullPointerException("constant can not be null");
    357                }
    358                if (val instanceof String) {
    359                    val = StringParser.buildString((String) val, false);
    360                }
    361                this.value = val;
    362            }
    363
    364            /** {@inheritDoc} */
    365            @Override
    366            public String asString() {
    367                StringBuilder strb = new StringBuilder();
    368                strb.append('"');
    369                asString(strb);
    370                strb.append('"');
    371                return strb.toString();
    372            }
    373
    374            /** {@inheritDoc} */
    375            @Override
    376            ExpressionType getType() {
    377                return ExpressionType.CONSTANT;
    378            }
    379
    380            /** {@inheritDoc} */
    381            @Override
    382            void asString(StringBuilder strb) {
    383                String str = value.toString();
    384                if (value instanceof String || value instanceof CharSequence) {
    385                    for (int i = 0, size = str.length(); i < size; ++i) {
    386                        char c = str.charAt(i);
    387                        if (c == '"' || c == '\\') {
    388                            strb.append('\\');
    389                        }
    390                        strb.append(c);
    391                    }
    392                } else {
    393                    strb.append(str);
    394                }
    395            }
    396
    397            /** {@inheritDoc} */
    398            @Override
    399            public Expression prepare(JexlContext context) {
    400                return this;
    401            }
    402
    403            /** {@inheritDoc} */
    404            @Override
    405            Expression prepare(Interpreter interpreter) {
    406                return this;
    407            }
    408
    409            /** {@inheritDoc} */
    410            @Override
    411            public Object evaluate(JexlContext context) {
    412                return value;
    413            }
    414
    415            /** {@inheritDoc} */
    416            @Override
    417            Object evaluate(Interpreter interpreter) {
    418                return value;
    419            }
    420        }
    421
    422
    423        /** The base for Jexl based expressions. */
    424        private abstract class JexlBasedExpression extends Expression {
    425            /** The JEXL string for this expression. */
    426            protected final CharSequence expr;
    427            /** The JEXL node for this expression. */
    428            protected final JexlNode node;
    429            /**
    430             * Creates a JEXL interpretable expression.
    431             * @param theExpr the expression as a string
    432             * @param theNode the expression as an AST
    433             * @param theSource the source expression if any
    434             */
    435            protected JexlBasedExpression(CharSequence theExpr, JexlNode theNode, Expression theSource) {
    436                super(theSource);
    437                this.expr = theExpr;
    438                this.node = theNode;
    439            }
    440
    441            /** {@inheritDoc} */
    442            @Override
    443            public String toString() {
    444                StringBuilder strb = new StringBuilder(expr.length() + 3);
    445                if (source != this) {
    446                    strb.append(source.toString());
    447                    strb.append(" /*= ");
    448                }
    449                strb.append(isImmediate() ? '$' : '#');
    450                strb.append("{");
    451                strb.append(expr);
    452                strb.append("}");
    453                if (source != this) {
    454                    strb.append(" */");
    455                }
    456                return strb.toString();
    457            }
    458
    459            /** {@inheritDoc} */
    460            @Override
    461            public void asString(StringBuilder strb) {
    462                strb.append(isImmediate() ? '$' : '#');
    463                strb.append("{");
    464                strb.append(expr);
    465                strb.append("}");
    466            }
    467
    468            /** {@inheritDoc} */
    469            @Override
    470            public Expression prepare(JexlContext context) {
    471                return this;
    472            }
    473
    474            /** {@inheritDoc} */
    475            @Override
    476            Expression prepare(Interpreter interpreter) {
    477                return this;
    478            }
    479
    480            /** {@inheritDoc} */
    481            @Override
    482            public Object evaluate(JexlContext context) {
    483                return UnifiedJEXL.this.evaluate(context, this);
    484            }
    485
    486            /** {@inheritDoc} */
    487            @Override
    488            Object evaluate(Interpreter interpreter) {
    489                return interpreter.interpret(node);
    490            }
    491        }
    492
    493
    494        /** An immediate expression: ${jexl}. */
    495        private class ImmediateExpression extends JexlBasedExpression {
    496            /**
    497             * Creates an immediate expression.
    498             * @param expr the expression as a string
    499             * @param node the expression as an AST
    500             * @param source the source expression if any
    501             */
    502            ImmediateExpression(CharSequence expr, JexlNode node, Expression source) {
    503                super(expr, node, source);
    504            }
    505
    506            /** {@inheritDoc} */
    507            @Override
    508            ExpressionType getType() {
    509                return ExpressionType.IMMEDIATE;
    510            }
    511
    512            /** {@inheritDoc} */
    513            @Override
    514            public boolean isImmediate() {
    515                return true;
    516            }
    517        }
    518
    519        /** An immediate expression: ${jexl}. */
    520        private class DeferredExpression extends JexlBasedExpression {
    521            /**
    522             * Creates a deferred expression.
    523             * @param expr the expression as a string
    524             * @param node the expression as an AST
    525             * @param source the source expression if any
    526             */
    527            DeferredExpression(CharSequence expr, JexlNode node, Expression source) {
    528                super(expr, node, source);
    529            }
    530
    531            /** {@inheritDoc} */
    532            @Override
    533            ExpressionType getType() {
    534                return ExpressionType.DEFERRED;
    535            }
    536
    537            /** {@inheritDoc} */
    538            @Override
    539            public boolean isImmediate() {
    540                return false;
    541            }
    542        }
    543
    544        /**
    545         * A deferred expression that nests an immediate expression.
    546         * #{...${jexl}...}
    547         * Note that the deferred syntax is JEXL's, not UnifiedJEXL.
    548         */
    549        private class NestedExpression extends DeferredExpression {
    550            /**
    551             * Creates a nested expression.
    552             * @param expr the expression as a string
    553             * @param node the expression as an AST
    554             * @param source the source expression if any
    555             */
    556            NestedExpression(CharSequence expr, JexlNode node, Expression source) {
    557                super(expr, node, source);
    558                if (this.source != this) {
    559                    throw new IllegalArgumentException("Nested expression can not have a source");
    560                }
    561            }
    562
    563            /** {@inheritDoc} */
    564            @Override
    565            ExpressionType getType() {
    566                return ExpressionType.NESTED;
    567            }
    568
    569            /** {@inheritDoc} */
    570            @Override
    571            public String toString() {
    572                return expr.toString();
    573            }
    574
    575            /** {@inheritDoc} */
    576            @Override
    577            public Expression prepare(JexlContext context) {
    578                return UnifiedJEXL.this.prepare(context, this);
    579            }
    580
    581            /** {@inheritDoc} */
    582            @Override
    583            public Expression prepare(Interpreter interpreter) {
    584                String value = interpreter.interpret(node).toString();
    585                JexlNode dnode = toNode(value, jexl.isDebug()? node.getInfo() : null);
    586                return new DeferredExpression(value, dnode, this);
    587            }
    588
    589            /** {@inheritDoc} */
    590            @Override
    591            public Object evaluate(Interpreter interpreter) {
    592                return prepare(interpreter).evaluate(interpreter);
    593            }
    594        }
    595
    596
    597        /** A composite expression: "... ${...} ... #{...} ...". */
    598        private class CompositeExpression extends Expression {
    599            /** Bit encoded (deferred count > 0) bit 1, (immediate count > 0) bit 0. */
    600            private final int meta;
    601            /** The list of sub-expression resulting from parsing. */
    602            private final Expression[] exprs;
    603            /**
    604             * Creates a composite expression.
    605             * @param counters counters of expression per type
    606             * @param list the sub-expressions
    607             * @param src the source for this expresion if any
    608             */
    609            CompositeExpression(int[] counters, ArrayList<Expression> list, Expression src) {
    610                super(src);
    611                this.exprs = list.toArray(new Expression[list.size()]);
    612                this.meta = (counters[ExpressionType.DEFERRED.index] > 0 ? 2 : 0)
    613                          | (counters[ExpressionType.IMMEDIATE.index] > 0 ? 1 : 0);
    614            }
    615
    616            /** {@inheritDoc} */
    617            @Override
    618            ExpressionType getType() {
    619                return ExpressionType.COMPOSITE;
    620            }
    621
    622            /** {@inheritDoc} */
    623            @Override
    624            public boolean isImmediate() {
    625                // immediate if no deferred
    626                return (meta & 2) == 0;
    627            }
    628
    629            /** {@inheritDoc} */
    630            @Override
    631            void asString(StringBuilder strb) {
    632                for (Expression e : exprs) {
    633                    e.asString(strb);
    634                }
    635            }
    636
    637            /** {@inheritDoc} */
    638            @Override
    639            public Expression prepare(JexlContext context) {
    640                return UnifiedJEXL.this.prepare(context, this);
    641            }
    642
    643            /** {@inheritDoc} */
    644            @Override
    645            Expression prepare(Interpreter interpreter) {
    646                // if this composite is not its own source, it is already prepared
    647                if (source != this) {
    648                    return this;
    649                }
    650                // we need to eval immediate expressions if there are some deferred/nested
    651                // ie both immediate & deferred counts > 0, bits 1 & 0 set, (1 << 1) & 1 == 3
    652                final boolean evalImmediate = meta == 3;
    653                final int size = exprs.length;
    654                final ExpressionBuilder builder = new ExpressionBuilder(size);
    655                // tracking whether prepare will return a different expression
    656                boolean eq = true;
    657                for (int e = 0; e < size; ++e) {
    658                    Expression expr = exprs[e];
    659                    Expression prepared = expr.prepare(interpreter);
    660                    if (evalImmediate && prepared instanceof ImmediateExpression) {
    661                        // evaluate immediate as constant
    662                        Object value = prepared.evaluate(interpreter);
    663                        prepared = value == null ? null : new ConstantExpression(value, prepared);
    664                    }
    665                    // add it if not null
    666                    if (prepared != null) {
    667                        builder.add(prepared);
    668                    }
    669                    // keep track of expression equivalence
    670                    eq &= expr == prepared;
    671                }
    672                Expression ready = eq ? this : builder.build(UnifiedJEXL.this, this);
    673                return ready;
    674            }
    675
    676            /** {@inheritDoc} */
    677            @Override
    678            public Object evaluate(JexlContext context) {
    679                return UnifiedJEXL.this.evaluate(context, this);
    680            }
    681
    682            /** {@inheritDoc} */
    683            @Override
    684            Object evaluate(Interpreter interpreter) {
    685                final int size = exprs.length;
    686                Object value = null;
    687                // common case: evaluate all expressions & concatenate them as a string
    688                StringBuilder strb = new StringBuilder();
    689                for (int e = 0; e < size; ++e) {
    690                    value = exprs[e].evaluate(interpreter);
    691                    if (value != null) {
    692                        strb.append(value.toString());
    693                    }
    694                }
    695                value = strb.toString();
    696                return value;
    697            }
    698        }
    699
    700        /** Creates a a {@link UnifiedJEXL.Expression} from an expression string.
    701         *  Uses & fills up the expression cache if any.
    702         * <p>
    703         * If the underlying JEXL engine is silent, errors will be logged through its logger as warnings.
    704         * </p>
    705         * @param expression the UnifiedJEXL string expression
    706         * @return the UnifiedJEXL object expression, null if silent and an error occured
    707         * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent
    708         */
    709        public Expression parse(String expression) {
    710            Exception xuel = null;
    711            Expression stmt = null;
    712            try {
    713                if (cache == null) {
    714                    stmt = parseExpression(expression);
    715                } else {
    716                    synchronized (cache) {
    717                        stmt = cache.get(expression);
    718                        if (stmt == null) {
    719                            stmt = parseExpression(expression);
    720                            cache.put(expression, stmt);
    721                        }
    722                    }
    723                }
    724            } catch (JexlException xjexl) {
    725                xuel = new Exception("failed to parse '" + expression + "'", xjexl);
    726            } catch (Exception xany) {
    727                xuel = xany;
    728            } finally {
    729                if (xuel != null) {
    730                    if (jexl.isSilent()) {
    731                        jexl.logger.warn(xuel.getMessage(), xuel.getCause());
    732                        return null;
    733                    }
    734                    throw xuel;
    735                }
    736            }
    737            return stmt;
    738        }
    739
    740        /**
    741         * Prepares an expression (nested & composites), handles exception reporting.
    742         *
    743         * @param context the JEXL context to use
    744         * @param expr the expression to prepare
    745         * @return a prepared expression
    746         * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent
    747         */
    748        Expression prepare(JexlContext context, Expression expr) {
    749            try {
    750                Interpreter interpreter = jexl.createInterpreter(context);
    751                interpreter.setSilent(false);
    752                return expr.prepare(interpreter);
    753            } catch (JexlException xjexl) {
    754                Exception xuel = createException("prepare", expr, xjexl);
    755                if (jexl.isSilent()) {
    756                    jexl.logger.warn(xuel.getMessage(), xuel.getCause());
    757                    return null;
    758                }
    759                throw xuel;
    760            }
    761        }
    762
    763        /**
    764         * Evaluates an expression (nested & composites), handles exception reporting.
    765         *
    766         * @param context the JEXL context to use
    767         * @param expr the expression to prepare
    768         * @return the result of the evaluation
    769         * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent
    770         */
    771        Object evaluate(JexlContext context, Expression expr) {
    772            try {
    773                Interpreter interpreter = jexl.createInterpreter(context);
    774                interpreter.setSilent(false);
    775                return expr.evaluate(interpreter);
    776            } catch (JexlException xjexl) {
    777                Exception xuel = createException("evaluate", expr, xjexl);
    778                if (jexl.isSilent()) {
    779                    jexl.logger.warn(xuel.getMessage(), xuel.getCause());
    780                    return null;
    781                }
    782                throw xuel;
    783            }
    784        }
    785
    786        /**
    787         * Use the JEXL parser to create the AST for an expression.
    788         * @param expression the expression to parse
    789         * @return the AST
    790         * @throws JexlException if an error occur during parsing
    791         */
    792        private JexlNode toNode(CharSequence expression) {
    793            return jexl.parse(expression, null);
    794        }
    795
    796        /**
    797         * Use the JEXL parser to create the AST for an expression.
    798         * @param expression the expression to parse
    799         * @param info debug information
    800         * @return the AST
    801         * @throws JexlException if an error occur during parsing
    802         */
    803        private JexlNode toNode(CharSequence expression, JexlInfo info) {
    804            return jexl.parse(expression, info);
    805        }
    806
    807        /**
    808         * Creates a UnifiedJEXL.Exception from a JexlException.
    809         * @param action parse, prepare, evaluate
    810         * @param expr the expression
    811         * @param xany the exception
    812         * @return an exception containing an explicit error message
    813         */
    814        private Exception createException(String action, Expression expr, java.lang.Exception xany) {
    815            StringBuilder strb = new StringBuilder("failed to ");
    816            strb.append(action);
    817            strb.append(" '");
    818            strb.append(expr.toString());
    819            strb.append("'");
    820            Throwable cause = xany.getCause();
    821            if (cause != null) {
    822                String causeMsg = cause.getMessage();
    823                if (causeMsg != null) {
    824                    strb.append(", ");
    825                    strb.append(causeMsg);
    826                }
    827            }
    828            return new Exception(strb.toString(), xany);
    829        }
    830
    831
    832        /** The different parsing states. */
    833        private static enum ParseState {
    834            /** Parsing a constant. */
    835            CONST,
    836            /** Parsing after $ .*/
    837            IMMEDIATE0,
    838            /** Parsing after # .*/
    839            DEFERRED0,
    840            /** Parsing after ${ .*/
    841            IMMEDIATE1,
    842            /** Parsing after #{ .*/
    843            DEFERRED1,
    844            /** Parsing after \ .*/
    845            ESCAPE
    846        }
    847
    848        /**
    849         * Parses a unified expression.
    850         * @param expr the string expression
    851         * @return the expression instance
    852         * @throws JexlException if an error occur during parsing
    853         */
    854        private Expression parseExpression(String expr) {
    855            final int size = expr.length();
    856            ExpressionBuilder builder = new ExpressionBuilder(0);
    857            StringBuilder strb = new StringBuilder(size);
    858            ParseState state = ParseState.CONST;
    859            int inner = 0;
    860            boolean nested = false;
    861            int inested = -1;
    862            for (int i = 0; i < size; ++i) {
    863                char c = expr.charAt(i);
    864                switch (state) {
    865                    default: // in case we ever add new expression type
    866                        throw new UnsupportedOperationException("unexpected expression type");
    867                    case CONST:
    868                        if (c == '$') {
    869                            state = ParseState.IMMEDIATE0;
    870                        } else if (c == '#') {
    871                            inested = i;
    872                            state = ParseState.DEFERRED0;
    873                        } else if (c == '\\') {
    874                            state = ParseState.ESCAPE;
    875                        } else {
    876                            // do buildup expr
    877                            strb.append(c);
    878                        }
    879                        break;
    880                    case IMMEDIATE0: // $
    881                        if (c == '{') {
    882                            state = ParseState.IMMEDIATE1;
    883                            // if chars in buffer, create constant
    884                            if (strb.length() > 0) {
    885                                Expression cexpr = new ConstantExpression(strb.toString(), null);
    886                                builder.add(cexpr);
    887                                strb.delete(0, Integer.MAX_VALUE);
    888                            }
    889                        } else {
    890                            // revert to CONST
    891                            strb.append('$');
    892                            strb.append(c);
    893                            state = ParseState.CONST;
    894                        }
    895                        break;
    896                    case DEFERRED0: // #
    897                        if (c == '{') {
    898                            state = ParseState.DEFERRED1;
    899                            // if chars in buffer, create constant
    900                            if (strb.length() > 0) {
    901                                Expression cexpr = new ConstantExpression(strb.toString(), null);
    902                                builder.add(cexpr);
    903                                strb.delete(0, Integer.MAX_VALUE);
    904                            }
    905                        } else {
    906                            // revert to CONST
    907                            strb.append('#');
    908                            strb.append(c);
    909                            state = ParseState.CONST;
    910                        }
    911                        break;
    912                    case IMMEDIATE1: // ${...
    913                        if (c == '}') {
    914                            // materialize the immediate expr
    915                            Expression iexpr = new ImmediateExpression(strb.toString(), toNode(strb), null);
    916                            builder.add(iexpr);
    917                            strb.delete(0, Integer.MAX_VALUE);
    918                            state = ParseState.CONST;
    919                        } else {
    920                            // do buildup expr
    921                            strb.append(c);
    922                        }
    923                        break;
    924                    case DEFERRED1: // #{...
    925                        // skip inner strings (for '}')
    926                        if (c == '"' || c == '\'') {
    927                            strb.append(c);
    928                            i = StringParser.readString(strb, expr, i + 1, c);
    929                            continue;
    930                        }
    931                        // nested immediate in deferred; need to balance count of '{' & '}'
    932                        if (c == '{') {
    933                            if (expr.charAt(i - 1) == '$') {
    934                                inner += 1;
    935                                strb.deleteCharAt(strb.length() - 1);
    936                                nested = true;
    937                            }
    938                            continue;
    939                        }
    940                        // closing '}'
    941                        if (c == '}') {
    942                            // balance nested immediate
    943                            if (inner > 0) {
    944                                inner -= 1;
    945                            } else {
    946                                // materialize the nested/deferred expr
    947                                Expression dexpr = null;
    948                                if (nested) {
    949                                    dexpr = new NestedExpression(expr.substring(inested, i + 1), toNode(strb), null);
    950                                } else {
    951                                    dexpr = new DeferredExpression(strb.toString(), toNode(strb), null);
    952                                }
    953                                builder.add(dexpr);
    954                                strb.delete(0, Integer.MAX_VALUE);
    955                                nested = false;
    956                                state = ParseState.CONST;
    957                            }
    958                        } else {
    959                            // do buildup expr
    960                            strb.append(c);
    961                        }
    962                        break;
    963                    case ESCAPE:
    964                        if (c == '#') {
    965                            strb.append('#');
    966                        } else if (c == '$') {
    967                            strb.append('$');
    968                        } else {
    969                            strb.append('\\');
    970                            strb.append(c);
    971                        }
    972                        state = ParseState.CONST;
    973                }
    974            }
    975            // we should be in that state
    976            if (state != ParseState.CONST) {
    977                throw new Exception("malformed expression: " + expr, null);
    978            }
    979            // if any chars were buffered, add them as a constant
    980            if (strb.length() > 0) {
    981                Expression cexpr = new ConstantExpression(strb.toString(), null);
    982                builder.add(cexpr);
    983            }
    984            return builder.build(this, null);
    985        }
    986    }