    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.File;
    021    import java.io.FileReader;
    022    import java.io.IOException;
    023    import java.io.InputStreamReader;
    024    import java.io.StringReader;
    025    import java.io.Reader;
    026    import java.net.URL;
    027    import java.net.URLConnection;
    028    import java.lang.ref.SoftReference;
    029    import java.lang.reflect.Constructor;
    030    import java.util.Map;
    031    import java.util.Set;
    032    import java.util.Collections;
    033    import java.util.Map.Entry;
    034    import org.apache.commons.logging.Log;
    035    import org.apache.commons.logging.LogFactory;
    036
    037    import org.apache.commons.jexl2.parser.ParseException;
    038    import org.apache.commons.jexl2.parser.Parser;
    039    import org.apache.commons.jexl2.parser.JexlNode;
    040    import org.apache.commons.jexl2.parser.TokenMgrError;
    041    import org.apache.commons.jexl2.parser.ASTJexlScript;
    042
    043    import org.apache.commons.jexl2.introspection.Uberspect;
    044    import org.apache.commons.jexl2.introspection.UberspectImpl;
    045    import org.apache.commons.jexl2.introspection.JexlMethod;
    046
    047    /**
    048     * <p>
    049     * Creates and evaluates Expression and Script objects.
    050     * Determines the behavior of Expressions & Scripts during their evaluation with respect to:
    051     * <ul>
    052     *  <li>Introspection, see {@link Uberspect}</li>
    053     *  <li>Arithmetic & comparison, see {@link JexlArithmetic}</li>
    054     *  <li>Error reporting</li>
    055     *  <li>Logging</li>
    056     * </ul>
    057     * </p>
    058     * <p>The <code>setSilent</code>and<code>setLenient</code> methods allow to fine-tune an engine instance behavior
    059     * according to various error control needs.
    060     * </p>
    061     * <ul>
    062     * <li>When "silent" & "lenient" (not-strict):
    063     * <p> 0 & null should be indicators of "default" values so that even in an case of error,
    064     * something meaningfull can still be inferred; may be convenient for configurations.
    065     * </p>
    066     * </li>
    067     * <li>When "silent" & "strict":
    068     * <p>One should probably consider using null as an error case - ie, every object
    069     * manipulated by JEXL should be valued; the ternary operator, especially the '?:' form
    070     * can be used to workaround exceptional cases.
    071     * Use case could be configuration with no implicit values or defaults.
    072     * </p>
    073     * </li>
    074     * <li>When "not-silent" & "not-strict":
    075     * <p>The error control grain is roughly on par with JEXL 1.0</p>
    076     * </li>
    077     * <li>When "not-silent" & "strict":
    078     * <p>The finest error control grain is obtained; it is the closest to Java code -
    079     * still augmented by "script" capabilities regarding automated conversions & type matching.
    080     * </p>
    081     * </li>
    082     * </ul>
    083     * <p>
    084     * Note that methods that evaluate expressions may throw <em>unchecked</em> exceptions;
    085     * The {@link JexlException} are thrown in "non-silent" mode but since these are
    086     * RuntimeException, user-code <em>should</em> catch them wherever most appropriate.
    087     * </p>
    088     * @since 2.0
    089     */
    090    public class JexlEngine {
    091        /**
    092         * An empty/static/non-mutable JexlContext used instead of null context.
    093         */
    094        public static final JexlContext EMPTY_CONTEXT = new JexlContext() {
    095            /** {@inheritDoc} */
    096            public Object get(String name) {
    097                return null;
    098            }
    099            /** {@inheritDoc} */
    100            public boolean has(String name) {
    101                return false;
    102            }
    103            /** {@inheritDoc} */
    104            public void set(String name, Object value) {
    105                throw new UnsupportedOperationException("Not supported in void context.");
    106            }
    107        };
    108
    109        /**
    110         *  Gets the default instance of Uberspect.
    111         * <p>This is lazily initialized to avoid building a default instance if there
    112         * is no use for it. The main reason for not using the default Uberspect instance is to
    113         * be able to use a (low level) introspector created with a given logger
    114         * instead of the default one.</p>
    115         * <p>Implemented as on demand holder idiom.</p>
    116         */
    117        private static final class UberspectHolder {
    118            /** The default uberspector that handles all introspection patterns. */
    119            private static final Uberspect UBERSPECT = new UberspectImpl(LogFactory.getLog(JexlEngine.class));
    120            /** Non-instantiable. */
    121            private UberspectHolder() {}
    122        }
    123
    124        /**
    125         * The Uberspect instance.
    126         */
    127        protected final Uberspect uberspect;
    128        /**
    129         * The JexlArithmetic instance.
    130         */
    131        protected final JexlArithmetic arithmetic;
    132        /**
    133         * The Log to which all JexlEngine messages will be logged.
    134         */
    135        protected final Log logger;
    136        /**
    137         * The singleton ExpressionFactory also holds a single instance of
    138         * {@link Parser}.
    139         * When parsing expressions, ExpressionFactory synchronizes on Parser.
    140         */
    141        protected final Parser parser = new Parser(new StringReader(";")); //$NON-NLS-1$
    142        /**
    143         * Whether expressions evaluated by this engine will throw exceptions (false) or
    144         * return null (true). Default is false.
    145         */
    146        protected boolean silent = false;
    147        /**
    148         * Whether error messages will carry debugging information.
    149         */
    150        protected boolean debug = true;
    151        /**
    152         *  The map of 'prefix:function' to object implementing the function.
    153         */
    154        protected Map<String, Object> functions = Collections.emptyMap();
    155        /**
    156         * The expression cache.
    157         */
    158        protected SoftCache<String, ASTJexlScript> cache = null;
    159        /**
    160         * The default cache load factor.
    161         */
    162        private static final float LOAD_FACTOR = 0.75f;
    163
    164        /**
    165         * Creates an engine with default arguments.
    166         */
    167        public JexlEngine() {
    168            this(null, null, null, null);
    169        }
    170
    171        /**
    172         * Creates a JEXL engine using the provided {@link Uberspect}, (@link JexlArithmetic),
    173         * a function map and logger.
    174         * @param anUberspect to allow different introspection behaviour
    175         * @param anArithmetic to allow different arithmetic behaviour
    176         * @param theFunctions an optional map of functions (@link setFunctions)
    177         * @param log the logger for various messages
    178         */
    179        public JexlEngine(Uberspect anUberspect, JexlArithmetic anArithmetic, Map<String, Object> theFunctions, Log log) {
    180            this.uberspect = anUberspect == null ? getUberspect(log) : anUberspect;
    181            if (log == null) {
    182                log = LogFactory.getLog(JexlEngine.class);
    183            }
    184            this.logger = log;
    185            this.arithmetic = anArithmetic == null ? new JexlArithmetic(true) : anArithmetic;
    186            if (theFunctions != null) {
    187                this.functions = theFunctions;
    188            }
    189        }
    190
    191
    192        /**
    193         *  Gets the default instance of Uberspect.
    194         * <p>This is lazily initialized to avoid building a default instance if there
    195         * is no use for it. The main reason for not using the default Uberspect instance is to
    196         * be able to use a (low level) introspector created with a given logger
    197         * instead of the default one.</p>
    198         * @param logger the logger to use for the underlying Uberspect
    199         * @return Uberspect the default uberspector instance.
    200         */
    201        public static Uberspect getUberspect(Log logger) {
    202            if (logger == null || logger.equals(LogFactory.getLog(JexlEngine.class))) {
    203                return UberspectHolder.UBERSPECT;
    204            }
    205            return new UberspectImpl(logger);
    206        }
    207
    208        /**
    209         * Gets this engine underlying uberspect.
    210         * @return the uberspect
    211         */
    212        public Uberspect getUberspect() {
    213            return uberspect;
    214        }
    215
    216        /**
    217         * Sets whether this engine reports debugging information when error occurs.
    218         * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
    219         * initialization code before expression creation &amp; evaluation.</p>
    220         * @see JexlEngine#setSilent
    221         * @see JexlEngine#setLenient
    222         * @param flag true implies debug is on, false implies debug is off.
    223         */
    224        public void setDebug(boolean flag) {
    225            this.debug = flag;
    226        }
    227
    228        /**
    229         * Checks whether this engine is in debug mode.
    230         * @return true if debug is on, false otherwise
    231         */
    232        public boolean isDebug() {
    233            return this.debug;
    234        }
    235
    236        /**
    237         * Sets whether this engine throws JexlException during evaluation when an error is triggered.
    238         * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
    239         * initialization code before expression creation &amp; evaluation.</p>
    240         * @see JexlEngine#setDebug
    241         * @see JexlEngine#setLenient
    242         * @param flag true means no JexlException will occur, false allows them
    243         */
    244        public void setSilent(boolean flag) {
    245            this.silent = flag;
    246        }
    247
    248        /**
    249         * Checks whether this engine throws JexlException during evaluation.
    250         * @return true if silent, false (default) otherwise
    251         */
    252        public boolean isSilent() {
    253            return this.silent;
    254        }
    255
    256        /**
    257         * Sets whether this engine triggers errors during evaluation when null is used as
    258         * an operand.
    259         * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
    260         * initialization code before expression creation &amp; evaluation.</p>
    261         * @see JexlEngine#setSilent
    262         * @see JexlEngine#setDebug
    263         * @param flag true means no JexlException will occur, false allows them
    264         */
    265        public void setLenient(boolean flag) {
    266            this.arithmetic.setLenient(flag);
    267        }
    268
    269        /**
    270         * Checks whether this engine triggers errors during evaluation when null is used as
    271         * an operand.
    272         * @return true if lenient, false if strict
    273         */
    274        public boolean isLenient() {
    275            return this.arithmetic.isLenient();
    276        }
    277
    278        /**
    279         * Sets the class loader used to discover classes in 'new' expressions.
    280         * <p>This method should be called as an optional step of the JexlEngine
    281         * initialization code before expression creation &amp; evaluation.</p>
    282         * @param loader the class loader to use
    283         */
    284        public void setClassLoader(ClassLoader loader) {
    285            uberspect.setClassLoader(loader);
    286        }
    287
    288        /**
    289         * Sets a cache of the defined size for expressions.
    290         * @param size if not strictly positive, no cache is used.
    291         */
    292        public void setCache(int size) {
    293            // since the cache is only used during parse, use same sync object
    294            synchronized (parser) {
    295                if (size <= 0) {
    296                    cache = null;
    297                } else if (cache == null || cache.size() != size) {
    298                    cache = new SoftCache<String, ASTJexlScript>(size);
    299                }
    300            }
    301        }
    302
    303        /**
    304         * Sets the map of function namespaces.
    305         * <p>
    306         * This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
    307         * initialization code before expression creation &amp; evaluation.
    308         * </p>
    309         * <p>
    310         * Each entry key is used as a prefix, each entry value used as a bean implementing
    311         * methods; an expression like 'nsx:method(123)' will thus be solved by looking at
    312         * a registered bean named 'nsx' that implements method 'method' in that map.
    313         * If all methods are static, you may use the bean class instead of an instance as value.
    314         * </p>
    315         * <p>
    316         * If the entry value is a class that has one contructor taking a JexlContext as argument, an instance
    317         * of the namespace will be created at evaluation time. It might be a good idea to derive a JexlContext
    318         * to carry the information used by the namespace to avoid variable space pollution and strongly type
    319         * the constructor with this specialized JexlContext.
    320         * </p>
    321         * <p>
    322         * The key or prefix allows to retrieve the bean that plays the role of the namespace.
    323         * If the prefix is null, the namespace is the top-level namespace allowing to define
    324         * top-level user defined functions ( ie: myfunc(...) )
    325         * </p>
    326         * @param funcs the map of functions that should not mutate after the call; if null
    327         * is passed, the empty collection is used.
    328         */
    329        public void setFunctions(Map<String, Object> funcs) {
    330            functions = funcs != null ? funcs : Collections.<String, Object>emptyMap();
    331        }
    332
    333        /**
    334         * Retrieves the map of function namespaces.
    335         *
    336         * @return the map passed in setFunctions or the empty map if the
    337         * original was null.
    338         */
    339        public Map<String, Object> getFunctions() {
    340            return functions;
    341        }
    342
    343        /**
    344         * An overridable through covariant return Expression creator.
    345         * @param text the script text
    346         * @param tree the parse AST tree
    347         * @return the script instance
    348         */
    349        protected Expression createExpression(ASTJexlScript tree, String text) {
    350            return new ExpressionImpl(this, text, tree);
    351        }
    352
    353        /**
    354         * Creates an Expression from a String containing valid
    355         * JEXL syntax.  This method parses the expression which
    356         * must contain either a reference or an expression.
    357         * @param expression A String containing valid JEXL syntax
    358         * @return An Expression object which can be evaluated with a JexlContext
    359         * @throws JexlException An exception can be thrown if there is a problem
    360         *      parsing this expression, or if the expression is neither an
    361         *      expression nor a reference.
    362         */
    363        public Expression createExpression(String expression) {
    364            return createExpression(expression, null);
    365        }
    366
    367        /**
    368         * Creates an Expression from a String containing valid
    369         * JEXL syntax.  This method parses the expression which
    370         * must contain either a reference or an expression.
    371         * @param expression A String containing valid JEXL syntax
    372         * @return An Expression object which can be evaluated with a JexlContext
    373         * @param info An info structure to carry debugging information if needed
    374         * @throws JexlException An exception can be thrown if there is a problem
    375         *      parsing this expression, or if the expression is neither an
    376         *      expression or a reference.
    377         */
    378        public Expression createExpression(String expression, JexlInfo info) {
    379            // Parse the expression
    380            ASTJexlScript tree = parse(expression, info);
    381            if (tree.jjtGetNumChildren() > 1) {
    382                logger.warn("The JEXL Expression created will be a reference"
    383                          + " to the first expression from the supplied script: \"" + expression + "\" ");
    384            }
    385            return createExpression(tree, expression);
    386        }
    387
    388        /**
    389         * Creates a Script from a String containing valid JEXL syntax.
    390         * This method parses the script which validates the syntax.
    391         *
    392         * @param scriptText A String containing valid JEXL syntax
    393         * @return A {@link Script} which can be executed using a {@link JexlContext}.
    394         * @throws JexlException if there is a problem parsing the script.
    395         */
    396        public Script createScript(String scriptText) {
    397            return createScript(scriptText, null);
    398        }
    399
    400        /**
    401         * Creates a Script from a String containing valid JEXL syntax.
    402         * This method parses the script which validates the syntax.
    403         *
    404         * @param scriptText A String containing valid JEXL syntax
    405         * @param info An info structure to carry debugging information if needed
    406         * @return A {@link Script} which can be executed using a {@link JexlContext}.
    407         * @throws JexlException if there is a problem parsing the script.
    408         */
    409        public Script createScript(String scriptText, JexlInfo info) {
    410            if (scriptText == null) {
    411                throw new NullPointerException("scriptText is null");
    412            }
    413            // Parse the expression
    414            ASTJexlScript tree = parse(scriptText, info);
    415            return createScript(tree, scriptText);
    416        }
    417
    418        /**
    419         * An overridable through covariant return Script creator.
    420         * @param text the script text
    421         * @param tree the parse AST tree
    422         * @return the script instance
    423         */
    424        protected Script createScript(ASTJexlScript tree, String text) {
    425            return new ExpressionImpl(this, text, tree);
    426        }
    427
    428        /**
    429         * Creates a Script from a {@link File} containing valid JEXL syntax.
    430         * This method parses the script and validates the syntax.
    431         *
    432         * @param scriptFile A {@link File} containing valid JEXL syntax.
    433         *      Must not be null. Must be a readable file.
    434         * @return A {@link Script} which can be executed with a
    435         *      {@link JexlContext}.
    436         * @throws IOException if there is a problem reading the script.
    437         * @throws JexlException if there is a problem parsing the script.
    438         */
    439        public Script createScript(File scriptFile) throws IOException {
    440            if (scriptFile == null) {
    441                throw new NullPointerException("scriptFile is null");
    442            }
    443            if (!scriptFile.canRead()) {
    444                throw new IOException("Can't read scriptFile (" + scriptFile.getCanonicalPath() + ")");
    445            }
    446            BufferedReader reader = new BufferedReader(new FileReader(scriptFile));
    447            JexlInfo info = null;
    448            if (debug) {
    449                info = createInfo(scriptFile.getName(), 0, 0);
    450            }
    451            return createScript(readerToString(reader), info);
    452        }
    453
    454        /**
    455         * Creates a Script from a {@link URL} containing valid JEXL syntax.
    456         * This method parses the script and validates the syntax.
    457         *
    458         * @param scriptUrl A {@link URL} containing valid JEXL syntax.
    459         *      Must not be null. Must be a readable file.
    460         * @return A {@link Script} which can be executed with a
    461         *      {@link JexlContext}.
    462         * @throws IOException if there is a problem reading the script.
    463         * @throws JexlException if there is a problem parsing the script.
    464         */
    465        public Script createScript(URL scriptUrl) throws IOException {
    466            if (scriptUrl == null) {
    467                throw new NullPointerException("scriptUrl is null");
    468            }
    469            URLConnection connection = scriptUrl.openConnection();
    470
    471            BufferedReader reader = new BufferedReader(
    472                    new InputStreamReader(connection.getInputStream()));
    473            JexlInfo info = null;
    474            if (debug) {
    475                info = createInfo(scriptUrl.toString(), 0, 0);
    476            }
    477            return createScript(readerToString(reader), info);
    478        }
    479
    480        /**
    481         * Accesses properties of a bean using an expression.
    482         * <p>
    483         * jexl.get(myobject, "foo.bar"); should equate to
    484         * myobject.getFoo().getBar(); (or myobject.getFoo().get("bar"))
    485         * </p>
    486         * <p>
    487         * If the JEXL engine is silent, errors will be logged through its logger as warning.
    488         * </p>
    489         * @param bean the bean to get properties from
    490         * @param expr the property expression
    491         * @return the value of the property
    492         * @throws JexlException if there is an error parsing the expression or during evaluation
    493         */
    494        public Object getProperty(Object bean, String expr) {
    495            return getProperty(null, bean, expr);
    496        }
    497
    498        /**
    499         * Accesses properties of a bean using an expression.
    500         * <p>
    501         * If the JEXL engine is silent, errors will be logged through its logger as warning.
    502         * </p>
    503         * @param context the evaluation context
    504         * @param bean the bean to get properties from
    505         * @param expr the property expression
    506         * @return the value of the property
    507         * @throws JexlException if there is an error parsing the expression or during evaluation
    508         */
    509        public Object getProperty(JexlContext context, Object bean, String expr) {
    510            if (context == null) {
    511                context = EMPTY_CONTEXT;
    512            }
    513            // lets build 1 unique & unused identifiers wrt context
    514            String r0 = "$0";
    515            for (int s = 0; context.has(r0); ++s) {
    516                r0 = r0 + s;
    517            }
    518            expr = r0 + (expr.charAt(0) == '[' ? "" : ".") + expr + ";";
    519            try {
    520                JexlNode tree = parse(expr, null);
    521                JexlNode node = tree.jjtGetChild(0);
    522                Interpreter interpreter = createInterpreter(context);
    523                // ensure 4 objects in register array
    524                Object[] r = {r0, bean, r0, bean};
    525                interpreter.setRegisters(r);
    526                return node.jjtAccept(interpreter, null);
    527            } catch (JexlException xjexl) {
    528                if (silent) {
    529                    logger.warn(xjexl.getMessage(), xjexl.getCause());
    530                    return null;
    531                }
    532                throw xjexl;
    533            }
    534        }
    535
    536        /**
    537         * Assign properties of a bean using an expression.
    538         * <p>
    539         * jexl.set(myobject, "foo.bar", 10); should equate to
    540         * myobject.getFoo().setBar(10); (or myobject.getFoo().put("bar", 10) )
    541         * </p>
    542         * <p>
    543         * If the JEXL engine is silent, errors will be logged through its logger as warning.
    544         * </p>
    545         * @param bean the bean to set properties in
    546         * @param expr the property expression
    547         * @param value the value of the property
    548         * @throws JexlException if there is an error parsing the expression or during evaluation
    549         */
    550        public void setProperty(Object bean, String expr, Object value) {
    551            setProperty(null, bean, expr, value);
    552        }
    553
    554        /**
    555         * Assign properties of a bean using an expression.
    556         * <p>
    557         * If the JEXL engine is silent, errors will be logged through its logger as warning.
    558         * </p>
    559         * @param context the evaluation context
    560         * @param bean the bean to set properties in
    561         * @param expr the property expression
    562         * @param value the value of the property
    563         * @throws JexlException if there is an error parsing the expression or during evaluation
    564         */
    565        public void setProperty(JexlContext context, Object bean, String expr, Object value) {
    566            if (context == null) {
    567                context = EMPTY_CONTEXT;
    568            }
    569            // lets build 2 unique & unused identifiers wrt context
    570            String r0 = "$0", r1 = "$1";
    571            for (int s = 0; context.has(r0); ++s) {
    572                r0 = r0 + s;
    573            }
    574            for (int s = 0; context.has(r1); ++s) {
    575                r1 = r1 + s;
    576            }
    577            // synthetize expr
    578            expr = r0 + (expr.charAt(0) == '[' ? "" : ".") + expr + "=" + r1 + ";";
    579            try {
    580                JexlNode tree = parse(expr, null);
    581                JexlNode node = tree.jjtGetChild(0);
    582                Interpreter interpreter = createInterpreter(context);
    583                // set the registers
    584                Object[] r = {r0, bean, r1, value};
    585                interpreter.setRegisters(r);
    586                node.jjtAccept(interpreter, null);
    587            } catch (JexlException xjexl) {
    588                if (silent) {
    589                    logger.warn(xjexl.getMessage(), xjexl.getCause());
    590                    return;
    591                }
    592                throw xjexl;
    593            }
    594        }
    595
    596        /**
    597         * Invokes an object's method by name and arguments.
    598         * @param obj the method's invoker object
    599         * @param meth the method's name
    600         * @param args the method's arguments
    601         * @return the method returned value or null if it failed and engine is silent
    602         * @throws JexlException if method could not be found or failed and engine is not silent
    603         */
    604        public Object invokeMethod(Object obj, String meth, Object... args) {
    605            JexlException xjexl = null;
    606            Object result = null;
    607            JexlInfo info = debugInfo();
    608            try {
    609                JexlMethod method = uberspect.getMethod(obj, meth, args, info);
    610                if (method == null && arithmetic.narrowArguments(args)) {
    611                    method = uberspect.getMethod(obj, meth, args, info);
    612                }
    613                if (method != null) {
    614                    result = method.invoke(obj, args);
    615                } else {
    616                    xjexl = new JexlException(info, "failed finding method " + meth);
    617                }
    618            } catch (Exception xany) {
    619                xjexl = new JexlException(info, "failed executing method " + meth, xany);
    620            } finally {
    621                if (xjexl != null) {
    622                    if (silent) {
    623                        logger.warn(xjexl.getMessage(), xjexl.getCause());
    624                        return null;
    625                    }
    626                    throw xjexl;
    627                }
    628            }
    629            return result;
    630        }
    631
    632        /**
    633         * Creates a new instance of an object using the most appropriate constructor
    634         * based on the arguments.
    635         * @param <T> the type of object
    636         * @param clazz the class to instantiate
    637         * @param args the constructor arguments
    638         * @return the created object instance or null on failure when silent
    639         */
    640        public <T> T newInstance(Class<? extends T> clazz, Object...args) {
    641            return clazz.cast(doCreateInstance(clazz, args));
    642        }
    643
    644        /**
    645         * Creates a new instance of an object using the most appropriate constructor
    646         * based on the arguments.
    647         * @param clazz the name of the class to instantiate resolved through this engine's class loader
    648         * @param args the constructor arguments
    649         * @return the created object instance or null on failure when silent
    650         */
    651        public Object newInstance(String clazz, Object...args) {
    652           return doCreateInstance(clazz, args);
    653        }
    654
    655        /**
    656         * Creates a new instance of an object using the most appropriate constructor
    657         * based on the arguments.
    658         * @param clazz the class to instantiate
    659         * @param args the constructor arguments
    660         * @return the created object instance or null on failure when silent
    661         */
    662        protected Object doCreateInstance(Object clazz, Object...args) {
    663            JexlException xjexl = null;
    664            Object result = null;
    665            JexlInfo info = debugInfo();
    666            try {
    667                Constructor<?> ctor = uberspect.getConstructor(clazz, args, info);
    668                if (ctor == null && arithmetic.narrowArguments(args)) {
    669                    ctor = uberspect.getConstructor(clazz, args, info);
    670                }
    671                if (ctor != null) {
    672                    result = ctor.newInstance(args);
    673                } else {
    674                    xjexl = new JexlException(info, "failed finding constructor for " + clazz.toString());
    675                }
    676            } catch (Exception xany) {
    677                xjexl = new JexlException(info, "failed executing constructor for " + clazz.toString(), xany);
    678            } finally {
    679                if (xjexl != null) {
    680                    if (silent) {
    681                        logger.warn(xjexl.getMessage(), xjexl.getCause());
    682                        return null;
    683                    }
    684                    throw xjexl;
    685                }
    686            }
    687            return result;
    688        }
    689
    690        /**
    691         * Creates an interpreter.
    692         * @param context a JexlContext; if null, the EMPTY_CONTEXT is used instead.
    693         * @return an Interpreter
    694         */
    695        protected Interpreter createInterpreter(JexlContext context) {
    696            if (context == null) {
    697                context = EMPTY_CONTEXT;
    698            }
    699            return new Interpreter(this, context);
    700        }
    701
    702        /**
    703         * A soft reference on cache.
    704         * <p>The cache is held through a soft reference, allowing it to be GCed under
    705         * memory pressure.</p>
    706         * @param <K> the cache key entry type
    707         * @param <V> the cache key value type
    708         */
    709        protected class SoftCache<K, V> {
    710            /**
    711             * The cache size.
    712             */
    713            private final int size;
    714            /**
    715             * The soft reference to the cache map.
    716             */
    717            private SoftReference<Map<K, V>> ref = null;
    718
    719            /**
    720             * Creates a new instance of a soft cache.
    721             * @param theSize the cache size
    722             */
    723            SoftCache(int theSize) {
    724                size = theSize;
    725            }
    726
    727            /**
    728             * Returns the cache size.
    729             * @return the cache size
    730             */
    731            int size() {
    732                return size;
    733            }
    734
    735            /**
    736             * Produces the cache entry set.
    737             * @return the cache entry set
    738             */
    739            Set<Entry<K, V>> entrySet() {
    740                Map<K, V> map = ref != null ? ref.get() : null;
    741                return map != null ? map.entrySet() : Collections.<Entry<K, V>>emptySet();
    742            }
    743
    744            /**
    745             * Gets a value from cache.
    746             * @param key the cache entry key
    747             * @return the cache entry value
    748             */
    749            V get(K key) {
    750                final Map<K, V> map = ref != null ? ref.get() : null;
    751                return map != null ? map.get(key) : null;
    752            }
    753
    754            /**
    755             * Puts a value in cache.
    756             * @param key the cache entry key
    757             * @param script the cache entry value
    758             */
    759            void put(K key, V script) {
    760                Map<K, V> map = ref != null ? ref.get() : null;
    761                if (map == null) {
    762                    map = createCache(size);
    763                    ref = new SoftReference<Map<K, V>>(map);
    764                }
    765                map.put(key, script);
    766            }
    767        }
    768
    769        /**
    770         * Creates a cache.
    771         * @param <K> the key type
    772         * @param <V> the value type
    773         * @param cacheSize the cache size, must be > 0
    774         * @return a Map usable as a cache bounded to the given size
    775         */
    776        protected <K, V> Map<K, V> createCache(final int cacheSize) {
    777            return new java.util.LinkedHashMap<K, V>(cacheSize, LOAD_FACTOR, true) {
    778                /** Serial version UID. */
    779                private static final long serialVersionUID = 3801124242820219131L;
    780
    781                @Override
    782                protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    783                    return size() > cacheSize;
    784                }
    785            };
    786        }
    787
    788        /**
    789         * Parses an expression.
    790         * @param expression the expression to parse
    791         * @param info debug information structure
    792         * @return the parsed tree
    793         * @throws JexlException if any error occured during parsing
    794         */
    795        protected ASTJexlScript parse(CharSequence expression, JexlInfo info) {
    796            String expr = cleanExpression(expression);
    797            ASTJexlScript tree = null;
    798            synchronized (parser) {
    799                if (cache != null) {
    800                    tree = cache.get(expr);
    801                    if (tree != null) {
    802                        return tree;
    803                    }
    804                }
    805                try {
    806                    Reader reader = new StringReader(expr);
    807                    // use first calling method of JexlEngine as debug info
    808                    if (info == null) {
    809                        info = debugInfo();
    810                    }
    811                    tree = parser.parse(reader, info);
    812                    if (cache != null) {
    813                        cache.put(expr, tree);
    814                    }
    815                } catch (TokenMgrError xtme) {
    816                    throw new JexlException(info, "tokenization failed", xtme);
    817                } catch (ParseException xparse) {
    818                    throw new JexlException(info, "parsing failed", xparse);
    819                }
    820            }
    821            return tree;
    822        }
    823
    824        /**
    825         * Creates a JexlInfo instance.
    826         * @param fn url/file name
    827         * @param l line number
    828         * @param c column number
    829         * @return a JexlInfo instance
    830         */
    831        protected JexlInfo createInfo(String fn, int l, int c) {
    832            return new DebugInfo(fn, l, c);
    833        }
    834
    835        /**
    836         * Creates and fills up debugging information.
    837         * <p>This gathers the class, method and line number of the first calling method
    838         * not owned by JexlEngine, UnifiedJEXL or {Script,Expression}Factory.</p>
    839         * @return an Info if debug is set, null otherwise
    840         */
    841        protected JexlInfo debugInfo() {
    842            JexlInfo info = null;
    843            if (debug) {
    844                Throwable xinfo = new Throwable();
    845                xinfo.fillInStackTrace();
    846                StackTraceElement[] stack = xinfo.getStackTrace();
    847                StackTraceElement se = null;
    848                Class<?> clazz = getClass();
    849                for (int s = 1; s < stack.length; ++s, se = null) {
    850                    se = stack[s];
    851                    String className = se.getClassName();
    852                    if (!className.equals(clazz.getName())) {
    853                        // go deeper if called from JexlEngine or UnifiedJEXL
    854                        if (className.equals(JexlEngine.class.getName())) {
    855                            clazz = JexlEngine.class;
    856                        } else if (className.equals(UnifiedJEXL.class.getName())) {
    857                            clazz = UnifiedJEXL.class;
    858                        } else {
    859                            break;
    860                        }
    861                    }
    862                }
    863                if (se != null) {
    864                    info = createInfo(se.getClassName() + "." + se.getMethodName(), se.getLineNumber(), 0);
    865                }
    866            }
    867            return info;
    868        }
    869
    870        /**
    871         * Trims the expression from front & ending spaces.
    872         * @param str expression to clean
    873         * @return trimmed expression ending in a semi-colon
    874         */
    875        public static final String cleanExpression(CharSequence str) {
    876            if (str != null) {
    877                int start = 0;
    878                int end = str.length();
    879                if (end > 0) {
    880                    // trim front spaces
    881                    while (start < end && str.charAt(start) == ' ') {
    882                        ++start;
    883                    }
    884                    // trim ending spaces
    885                    while (end > 0 && str.charAt(end - 1) == ' ') {
    886                        --end;
    887                    }
    888                    return str.subSequence(start, end).toString();
    889                }
    890                return "";
    891            }
    892            return null;
    893        }
    894
    895        /**
    896         * Read from a reader into a StringBuffer and return a String with
    897         * the contents of the reader.
    898         * @param scriptReader to be read.
    899         * @return the contents of the reader as a String.
    900         * @throws IOException on any error reading the reader.
    901         */
    902        public static final String readerToString(Reader scriptReader) throws IOException {
    903            StringBuilder buffer = new StringBuilder();
    904            BufferedReader reader;
    905            if (scriptReader instanceof BufferedReader) {
    906                reader = (BufferedReader) scriptReader;
    907            } else {
    908                reader = new BufferedReader(scriptReader);
    909            }
    910            try {
    911                String line;
    912                while ((line = reader.readLine()) != null) {
    913                    buffer.append(line).append('\n');
    914                }
    915                return buffer.toString();
    916            } finally {
    917                try {
    918                    reader.close();
    919                } catch(IOException xio) {
    920                    // ignore
    921                }
    922            }
    923
    924        }
    925    }