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