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.util.ArrayList;
030    import java.util.Map;
031    import java.util.Set;
032    import java.util.Collections;
033    import java.util.LinkedHashMap;
034    import java.util.LinkedHashSet;
035    import java.util.List;
036    import java.util.Map.Entry;
037    import org.apache.commons.logging.Log;
038    import org.apache.commons.logging.LogFactory;
039    
040    import org.apache.commons.jexl2.parser.ParseException;
041    import org.apache.commons.jexl2.parser.Parser;
042    import org.apache.commons.jexl2.parser.JexlNode;
043    import org.apache.commons.jexl2.parser.TokenMgrError;
044    import org.apache.commons.jexl2.parser.ASTJexlScript;
045    
046    import org.apache.commons.jexl2.introspection.Uberspect;
047    import org.apache.commons.jexl2.introspection.UberspectImpl;
048    import org.apache.commons.jexl2.introspection.JexlMethod;
049    import org.apache.commons.jexl2.parser.ASTArrayAccess;
050    import org.apache.commons.jexl2.parser.ASTIdentifier;
051    import org.apache.commons.jexl2.parser.ASTReference;
052    
053    /**
054     * <p>
055     * Creates and evaluates Expression and Script objects.
056     * Determines the behavior of Expressions & Scripts during their evaluation with respect to:
057     * <ul>
058     *  <li>Introspection, see {@link Uberspect}</li>
059     *  <li>Arithmetic & comparison, see {@link JexlArithmetic}</li>
060     *  <li>Error reporting</li>
061     *  <li>Logging</li>
062     * </ul>
063     * </p>
064     * <p>The <code>setSilent</code> and <code>setLenient</code> methods allow to fine-tune an engine instance behavior
065     * according to various error control needs. The lenient/strict flag tells the engine when and if null as operand is
066     * considered an error, the silent/verbose flag tells the engine what to do with the error
067     * (log as warning or throw exception).
068     * </p>
069     * <ul>
070     * <li>When "silent" &amp; "lenient":
071     * <p> 0 & null should be indicators of "default" values so that even in an case of error,
072     * something meaningfull can still be inferred; may be convenient for configurations.
073     * </p>
074     * </li>
075     * <li>When "silent" &amp; "strict":
076     * <p>One should probably consider using null as an error case - ie, every object
077     * manipulated by JEXL should be valued; the ternary operator, especially the '?:' form
078     * can be used to workaround exceptional cases.
079     * Use case could be configuration with no implicit values or defaults.
080     * </p>
081     * </li>
082     * <li>When "verbose" &amp; "lenient":
083     * <p>The error control grain is roughly on par with JEXL 1.0</p>
084     * </li>
085     * <li>When "verbose" &amp; "strict":
086     * <p>The finest error control grain is obtained; it is the closest to Java code -
087     * still augmented by "script" capabilities regarding automated conversions & type matching.
088     * </p>
089     * </li>
090     * </ul>
091     * <p>
092     * Note that methods that evaluate expressions may throw <em>unchecked</em> exceptions;
093     * The {@link JexlException} are thrown in "non-silent" mode but since these are
094     * RuntimeException, user-code <em>should</em> catch them wherever most appropriate.
095     * </p>
096     * @since 2.0
097     */
098    public class JexlEngine {
099        /**
100         * An empty/static/non-mutable JexlContext used instead of null context.
101         */
102        public static final JexlContext EMPTY_CONTEXT = new JexlContext() {
103            /** {@inheritDoc} */
104            public Object get(String name) {
105                return null;
106            }
107    
108            /** {@inheritDoc} */
109            public boolean has(String name) {
110                return false;
111            }
112    
113            /** {@inheritDoc} */
114            public void set(String name, Object value) {
115                throw new UnsupportedOperationException("Not supported in void context.");
116            }
117        };
118    
119        /**
120         *  Gets the default instance of Uberspect.
121         * <p>This is lazily initialized to avoid building a default instance if there
122         * is no use for it. The main reason for not using the default Uberspect instance is to
123         * be able to use a (low level) introspector created with a given logger
124         * instead of the default one.</p>
125         * <p>Implemented as on demand holder idiom.</p>
126         */
127        private static final class UberspectHolder {
128            /** The default uberspector that handles all introspection patterns. */
129            private static final Uberspect UBERSPECT = new UberspectImpl(LogFactory.getLog(JexlEngine.class));
130    
131            /** Non-instantiable. */
132            private UberspectHolder() {
133            }
134        }
135        /**
136         * The Uberspect instance.
137         */
138        protected final Uberspect uberspect;
139        /**
140         * The JexlArithmetic instance.
141         */
142        protected final JexlArithmetic arithmetic;
143        /**
144         * The Log to which all JexlEngine messages will be logged.
145         */
146        protected final Log logger;
147        /**
148         * The singleton ExpressionFactory also holds a single instance of
149         * {@link Parser}.
150         * When parsing expressions, ExpressionFactory synchronizes on Parser.
151         */
152        protected final Parser parser = new Parser(new StringReader(";")); //$NON-NLS-1$
153        /**
154         * Whether expressions evaluated by this engine will throw exceptions (false) or 
155         * return null (true) on errors. Default is false.
156         */
157        // TODO could this be private?
158        protected volatile boolean silent = false;
159        /**
160         * Whether error messages will carry debugging information.
161         */
162        // TODO could this be private?
163        protected volatile boolean debug = true;
164        /**
165         *  The map of 'prefix:function' to object implementing the functions.
166         */
167        // TODO this could probably be private; is it threadsafe?
168        protected Map<String, Object> functions = Collections.emptyMap();
169        /**
170         * The expression cache.
171         */
172        // TODO is this thread-safe? Could it be made private?
173        protected SoftCache<String, ASTJexlScript> cache = null;
174        /**
175         * The default cache load factor.
176         */
177        private static final float LOAD_FACTOR = 0.75f;
178    
179        /**
180         * Creates an engine with default arguments.
181         */
182        public JexlEngine() {
183            this(null, null, null, null);
184        }
185    
186        /**
187         * Creates a JEXL engine using the provided {@link Uberspect}, (@link JexlArithmetic),
188         * a function map and logger.
189         * @param anUberspect to allow different introspection behaviour
190         * @param anArithmetic to allow different arithmetic behaviour
191         * @param theFunctions an optional map of functions (@link setFunctions)
192         * @param log the logger for various messages
193         */
194        public JexlEngine(Uberspect anUberspect, JexlArithmetic anArithmetic, Map<String, Object> theFunctions, Log log) {
195            this.uberspect = anUberspect == null ? getUberspect(log) : anUberspect;
196            if (log == null) {
197                log = LogFactory.getLog(JexlEngine.class);
198            }
199            this.logger = log;
200            this.arithmetic = anArithmetic == null ? new JexlArithmetic(true) : anArithmetic;
201            if (theFunctions != null) {
202                this.functions = theFunctions;
203            }
204        }
205    
206        /**
207         *  Gets the default instance of Uberspect.
208         * <p>This is lazily initialized to avoid building a default instance if there
209         * is no use for it. The main reason for not using the default Uberspect instance is to
210         * be able to use a (low level) introspector created with a given logger
211         * instead of the default one.</p>
212         * @param logger the logger to use for the underlying Uberspect
213         * @return Uberspect the default uberspector instance.
214         */
215        public static Uberspect getUberspect(Log logger) {
216            if (logger == null || logger.equals(LogFactory.getLog(JexlEngine.class))) {
217                return UberspectHolder.UBERSPECT;
218            }
219            return new UberspectImpl(logger);
220        }
221    
222        /**
223         * Gets this engine underlying uberspect.
224         * @return the uberspect
225         */
226        public Uberspect getUberspect() {
227            return uberspect;
228        }
229    
230        /**
231         * Gets this engine underlying arithmetic.
232         * @return the arithmetic
233         * @since 2.1
234         */
235        public JexlArithmetic getArithmetic() {
236            return arithmetic;
237        }
238    
239        /**
240         * Sets whether this engine reports debugging information when error occurs.
241         * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
242         * initialization code before expression creation &amp; evaluation.</p>
243         * @see JexlEngine#setSilent
244         * @see JexlEngine#setLenient
245         * @param flag true implies debug is on, false implies debug is off.
246         */
247        public void setDebug(boolean flag) {
248            this.debug = flag;
249        }
250    
251        /**
252         * Checks whether this engine is in debug mode.
253         * @return true if debug is on, false otherwise
254         */
255        public boolean isDebug() {
256            return this.debug;
257        }
258    
259        /**
260         * Sets whether this engine throws JexlException during evaluation when an error is triggered.
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#setDebug
264         * @see JexlEngine#setLenient
265         * @param flag true means no JexlException will occur, false allows them
266         */
267        public void setSilent(boolean flag) {
268            this.silent = flag;
269        }
270    
271        /**
272         * Checks whether this engine throws JexlException during evaluation.
273         * @return true if silent, false (default) otherwise
274         */
275        public boolean isSilent() {
276            return this.silent;
277        }
278        
279        /**
280         * Sets whether this engine considers unknown variables, methods and constructors as errors or evaluates them
281         * as null or zero.
282         * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
283         * initialization code before expression creation &amp; evaluation.</p>
284         * <p>As of 2.1, you can use a JexlThreadedArithmetic instance to allow the JexlArithmetic
285         * leniency behavior to be independently specified per thread, whilst still using a single engine.</p>
286         * @see JexlEngine#setSilent
287         * @see JexlEngine#setDebug
288         * @param flag true means no JexlException will occur, false allows them
289         */
290        @SuppressWarnings("deprecation")
291        public void setLenient(boolean flag) {
292            if (arithmetic instanceof JexlThreadedArithmetic) {
293                JexlThreadedArithmetic.setLenient(Boolean.valueOf(flag));
294            } else {
295                this.arithmetic.setLenient(flag);
296            }
297        }
298    
299        /**
300         * Checks whether this engine considers unknown variables, methods and constructors as errors.
301         * @return true if lenient, false if strict
302         */
303        public boolean isLenient() {
304            return arithmetic.isLenient();
305        }
306    
307        /**
308         * Sets whether this engine behaves in strict or lenient mode.
309         * Equivalent to setLenient(!flag).
310         * <p>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.</p>
312         * @param flag true for strict, false for lenient
313         * @since 2.1
314         */
315        public final void setStrict(boolean flag) {
316            setLenient(!flag);
317        }
318    
319        /**
320         * Checks whether this engine behaves in strict or lenient mode.
321         * Equivalent to !isLenient().
322         * @return true for strict, false for lenient
323         * @since 2.1
324         */
325        public final boolean isStrict() {
326            return !isLenient();
327        }
328    
329        /**
330         * Sets the class loader used to discover classes in 'new' expressions.
331         * <p>This method should be called as an optional step of the JexlEngine
332         * initialization code before expression creation &amp; evaluation.</p>
333         * @param loader the class loader to use
334         */
335        public void setClassLoader(ClassLoader loader) {
336            uberspect.setClassLoader(loader);
337        }
338    
339        /**
340         * Sets a cache for expressions of the defined size.
341         * <p>The cache will contain at most <code>size</code> expressions. Note that
342         * all JEXL caches are held through SoftReferences and may be garbage-collected.</p>
343         * @param size if not strictly positive, no cache is used.
344         */
345        public void setCache(int size) {
346            // since the cache is only used during parse, use same sync object
347            synchronized (parser) {
348                if (size <= 0) {
349                    cache = null;
350                } else if (cache == null || cache.size() != size) {
351                    cache = new SoftCache<String, ASTJexlScript>(size);
352                }
353            }
354        }
355    
356        /**
357         * Sets the map of function namespaces.
358         * <p>
359         * This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
360         * initialization code before expression creation &amp; evaluation.
361         * </p>
362         * <p>
363         * Each entry key is used as a prefix, each entry value used as a bean implementing
364         * methods; an expression like 'nsx:method(123)' will thus be solved by looking at
365         * a registered bean named 'nsx' that implements method 'method' in that map.
366         * If all methods are static, you may use the bean class instead of an instance as value.
367         * </p>
368         * <p>
369         * If the entry value is a class that has one contructor taking a JexlContext as argument, an instance
370         * of the namespace will be created at evaluation time. It might be a good idea to derive a JexlContext
371         * to carry the information used by the namespace to avoid variable space pollution and strongly type
372         * the constructor with this specialized JexlContext.
373         * </p>
374         * <p>
375         * The key or prefix allows to retrieve the bean that plays the role of the namespace.
376         * If the prefix is null, the namespace is the top-level namespace allowing to define
377         * top-level user defined functions ( ie: myfunc(...) )
378         * </p>
379         * <p>Note that the JexlContext is also used to try to solve top-level functions. This allows ObjectContext
380         * derived instances to call methods on the wrapped object.</p>
381         * @param funcs the map of functions that should not mutate after the call; if null
382         * is passed, the empty collection is used.
383         */
384        public void setFunctions(Map<String, Object> funcs) {
385            functions = funcs != null ? funcs : Collections.<String, Object>emptyMap();
386        }
387    
388        /**
389         * Retrieves the map of function namespaces.
390         *
391         * @return the map passed in setFunctions or the empty map if the
392         * original was null.
393         */
394        public Map<String, Object> getFunctions() {
395            return functions;
396        }
397    
398        /**
399         * An overridable through covariant return Expression creator.
400         * @param text the script text
401         * @param tree the parse AST tree
402         * @return the script instance
403         */
404        protected Expression createExpression(ASTJexlScript tree, String text) {
405            return new ExpressionImpl(this, text, tree);
406        }
407    
408        /**
409         * Creates an Expression from a String containing valid
410         * JEXL syntax.  This method parses the expression which
411         * must contain either a reference or an expression.
412         * @param expression A String containing valid JEXL syntax
413         * @return An Expression object which can be evaluated with a JexlContext
414         * @throws JexlException An exception can be thrown if there is a problem
415         *      parsing this expression, or if the expression is neither an
416         *      expression nor a reference.
417         */
418        public Expression createExpression(String expression) {
419            return createExpression(expression, null);
420        }
421    
422        /**
423         * Creates an Expression from a String containing valid
424         * JEXL syntax.  This method parses the expression which
425         * must contain either a reference or an expression.
426         * @param expression A String containing valid JEXL syntax
427         * @return An Expression object which can be evaluated with a JexlContext
428         * @param info An info structure to carry debugging information if needed
429         * @throws JexlException An exception can be thrown if there is a problem
430         *      parsing this expression, or if the expression is neither an
431         *      expression or a reference.
432         */
433        public Expression createExpression(String expression, JexlInfo info) {
434            // Parse the expression
435            ASTJexlScript tree = parse(expression, info, null);
436            if (tree.jjtGetNumChildren() > 1) {
437                logger.warn("The JEXL Expression created will be a reference"
438                        + " to the first expression from the supplied script: \"" + expression + "\" ");
439            }
440            return createExpression(tree, expression);
441        }
442    
443        /**
444         * Creates a Script from a String containing valid JEXL syntax.
445         * This method parses the script which validates the syntax.
446         *
447         * @param scriptText A String containing valid JEXL syntax
448         * @return A {@link Script} which can be executed using a {@link JexlContext}.
449         * @throws JexlException if there is a problem parsing the script.
450         */
451        public Script createScript(String scriptText) {
452            return createScript(scriptText, null, null);
453        }
454    
455        /**
456         * Creates a Script from a String containing valid JEXL syntax.
457         * This method parses the script which validates the syntax.
458         *
459         * @param scriptText A String containing valid JEXL syntax
460         * @param info An info structure to carry debugging information if needed
461         * @return A {@link Script} which can be executed using a {@link JexlContext}.
462         * @throws JexlException if there is a problem parsing the script.
463         * @deprecated Use {@link #createScript(String, JexlInfo, String[])}
464         */
465        @Deprecated
466        public Script createScript(String scriptText, JexlInfo info) {
467            if (scriptText == null) {
468                throw new NullPointerException("scriptText is null");
469            }
470            // Parse the expression
471            ASTJexlScript tree = parse(scriptText, info);
472            return createScript(tree, scriptText);
473        }
474    
475        /**
476         * Creates a Script from a String containing valid JEXL syntax.
477         * This method parses the script which validates the syntax.
478         *
479         * @param scriptText A String containing valid JEXL syntax
480         * @param names the script parameter names
481         * @return A {@link Script} which can be executed using a {@link JexlContext}.
482         * @throws JexlException if there is a problem parsing the script.
483         */
484        public Script createScript(String scriptText, String... names) {
485            return createScript(scriptText, null, names);
486        }
487    
488        /**
489         * Creates a Script from a String containing valid JEXL syntax.
490         * This method parses the script which validates the syntax.
491         * It uses an array of parameter names that will be resolved during parsing;
492         * a corresponding array of arguments containing values should be used during evaluation.
493         *
494         * @param scriptText A String containing valid JEXL syntax
495         * @param info An info structure to carry debugging information if needed
496         * @param names the script parameter names
497         * @return A {@link Script} which can be executed using a {@link JexlContext}.
498         * @throws JexlException if there is a problem parsing the script.
499         * @since 2.1
500         */
501        public Script createScript(String scriptText, JexlInfo info, String[] names) {
502            if (scriptText == null) {
503                throw new NullPointerException("scriptText is null");
504            }
505            // Parse the expression
506            ASTJexlScript tree = parse(scriptText, info, new Scope(names));
507            return createScript(tree, scriptText);
508        }
509    
510        /**
511         * An overridable through covariant return Script creator.
512         * @param text the script text
513         * @param tree the parse AST tree
514         * @return the script instance
515         */
516        protected Script createScript(ASTJexlScript tree, String text) {
517            return new ExpressionImpl(this, text, tree);
518        }
519    
520        /**
521         * Creates a Script from a {@link File} containing valid JEXL syntax.
522         * This method parses the script and validates the syntax.
523         *
524         * @param scriptFile A {@link File} containing valid JEXL syntax.
525         *      Must not be null. Must be a readable file.
526         * @return A {@link Script} which can be executed with a
527         *      {@link JexlContext}.
528         * @throws IOException if there is a problem reading the script.
529         * @throws JexlException if there is a problem parsing the script.
530         */
531        public Script createScript(File scriptFile) throws IOException {
532            if (scriptFile == null) {
533                throw new NullPointerException("scriptFile is null");
534            }
535            if (!scriptFile.canRead()) {
536                throw new IOException("Can't read scriptFile (" + scriptFile.getCanonicalPath() + ")");
537            }
538            BufferedReader reader = new BufferedReader(new FileReader(scriptFile));
539            JexlInfo info = null;
540            if (debug) {
541                info = createInfo(scriptFile.getName(), 0, 0);
542            }
543            return createScript(readerToString(reader), info, null);
544        }
545    
546        /**
547         * Creates a Script from a {@link URL} containing valid JEXL syntax.
548         * This method parses the script and validates the syntax.
549         *
550         * @param scriptUrl A {@link URL} containing valid JEXL syntax.
551         *      Must not be null. Must be a readable file.
552         * @return A {@link Script} which can be executed with a
553         *      {@link JexlContext}.
554         * @throws IOException if there is a problem reading the script.
555         * @throws JexlException if there is a problem parsing the script.
556         */
557        public Script createScript(URL scriptUrl) throws IOException {
558            if (scriptUrl == null) {
559                throw new NullPointerException("scriptUrl is null");
560            }
561            URLConnection connection = scriptUrl.openConnection();
562    
563            BufferedReader reader = new BufferedReader(
564                    new InputStreamReader(connection.getInputStream()));
565            JexlInfo info = null;
566            if (debug) {
567                info = createInfo(scriptUrl.toString(), 0, 0);
568            }
569            return createScript(readerToString(reader), info, null);
570        }
571    
572        /**
573         * Accesses properties of a bean using an expression.
574         * <p>
575         * jexl.get(myobject, "foo.bar"); should equate to
576         * myobject.getFoo().getBar(); (or myobject.getFoo().get("bar"))
577         * </p>
578         * <p>
579         * If the JEXL engine is silent, errors will be logged through its logger as warning.
580         * </p>
581         * @param bean the bean to get properties from
582         * @param expr the property expression
583         * @return the value of the property
584         * @throws JexlException if there is an error parsing the expression or during evaluation
585         */
586        public Object getProperty(Object bean, String expr) {
587            return getProperty(null, bean, expr);
588        }
589    
590        /**
591         * Accesses properties of a bean using an expression.
592         * <p>
593         * If the JEXL engine is silent, errors will be logged through its logger as warning.
594         * </p>
595         * @param context the evaluation context
596         * @param bean the bean to get properties from
597         * @param expr the property expression
598         * @return the value of the property
599         * @throws JexlException if there is an error parsing the expression or during evaluation
600         */
601        public Object getProperty(JexlContext context, Object bean, String expr) {
602            if (context == null) {
603                context = EMPTY_CONTEXT;
604            }
605            // synthetize expr using register
606            expr = "#0" + (expr.charAt(0) == '[' ? "" : ".") + expr + ";";
607            try {
608                parser.ALLOW_REGISTERS = true;
609                Scope frame = new Scope("#0");
610                ASTJexlScript script = parse(expr, null, frame);
611                JexlNode node = script.jjtGetChild(0);
612                Interpreter interpreter = createInterpreter(context);
613                // set frame
614                interpreter.setFrame(script.createFrame(bean));
615                return node.jjtAccept(interpreter, null);
616            } catch (JexlException xjexl) {
617                if (silent) {
618                    logger.warn(xjexl.getMessage(), xjexl.getCause());
619                    return null;
620                }
621                throw xjexl;
622            } finally {
623                parser.ALLOW_REGISTERS = false;
624            }
625        }
626    
627        /**
628         * Assign properties of a bean using an expression.
629         * <p>
630         * jexl.set(myobject, "foo.bar", 10); should equate to
631         * myobject.getFoo().setBar(10); (or myobject.getFoo().put("bar", 10) )
632         * </p>
633         * <p>
634         * If the JEXL engine is silent, errors will be logged through its logger as warning.
635         * </p>
636         * @param bean the bean to set properties in
637         * @param expr the property expression
638         * @param value the value of the property
639         * @throws JexlException if there is an error parsing the expression or during evaluation
640         */
641        public void setProperty(Object bean, String expr, Object value) {
642            setProperty(null, bean, expr, value);
643        }
644    
645        /**
646         * Assign properties of a bean using an expression.
647         * <p>
648         * If the JEXL engine is silent, errors will be logged through its logger as warning.
649         * </p>
650         * @param context the evaluation context
651         * @param bean the bean to set properties in
652         * @param expr the property expression
653         * @param value the value of the property
654         * @throws JexlException if there is an error parsing the expression or during evaluation
655         */
656        public void setProperty(JexlContext context, Object bean, String expr, Object value) {
657            if (context == null) {
658                context = EMPTY_CONTEXT;
659            }
660            // synthetize expr using registers
661            expr = "#0" + (expr.charAt(0) == '[' ? "" : ".") + expr + "=" + "#1" + ";";
662            try {
663                parser.ALLOW_REGISTERS = true;
664                Scope frame = new Scope("#0", "#1");
665                ASTJexlScript script = parse(expr, null, frame);
666                JexlNode node = script.jjtGetChild(0);
667                Interpreter interpreter = createInterpreter(context);
668                // set the registers
669                interpreter.setFrame(script.createFrame(bean, value));
670                node.jjtAccept(interpreter, null);
671            } catch (JexlException xjexl) {
672                if (silent) {
673                    logger.warn(xjexl.getMessage(), xjexl.getCause());
674                    return;
675                }
676                throw xjexl;
677            } finally {
678                parser.ALLOW_REGISTERS = false;
679            }
680        }
681    
682        /**
683         * Invokes an object's method by name and arguments.
684         * @param obj the method's invoker object
685         * @param meth the method's name
686         * @param args the method's arguments
687         * @return the method returned value or null if it failed and engine is silent
688         * @throws JexlException if method could not be found or failed and engine is not silent
689         */
690        public Object invokeMethod(Object obj, String meth, Object... args) {
691            JexlException xjexl = null;
692            Object result = null;
693            JexlInfo info = debugInfo();
694            try {
695                JexlMethod method = uberspect.getMethod(obj, meth, args, info);
696                if (method == null && arithmetic.narrowArguments(args)) {
697                    method = uberspect.getMethod(obj, meth, args, info);
698                }
699                if (method != null) {
700                    result = method.invoke(obj, args);
701                } else {
702                    xjexl = new JexlException(info, "failed finding method " + meth);
703                }
704            } catch (Exception xany) {
705                xjexl = new JexlException(info, "failed executing method " + meth, xany);
706            } finally {
707                if (xjexl != null) {
708                    if (silent) {
709                        logger.warn(xjexl.getMessage(), xjexl.getCause());
710                        return null;
711                    }
712                    throw xjexl;
713                }
714            }
715            return result;
716        }
717    
718        /**
719         * Creates a new instance of an object using the most appropriate constructor
720         * based on the arguments.
721         * @param <T> the type of object
722         * @param clazz the class to instantiate
723         * @param args the constructor arguments
724         * @return the created object instance or null on failure when silent
725         */
726        public <T> T newInstance(Class<? extends T> clazz, Object... args) {
727            return clazz.cast(doCreateInstance(clazz, args));
728        }
729    
730        /**
731         * Creates a new instance of an object using the most appropriate constructor
732         * based on the arguments.
733         * @param clazz the name of the class to instantiate resolved through this engine's class loader
734         * @param args the constructor arguments
735         * @return the created object instance or null on failure when silent
736         */
737        public Object newInstance(String clazz, Object... args) {
738            return doCreateInstance(clazz, args);
739        }
740    
741        /**
742         * Creates a new instance of an object using the most appropriate constructor
743         * based on the arguments.
744         * @param clazz the class to instantiate
745         * @param args the constructor arguments
746         * @return the created object instance or null on failure when silent
747         */
748        protected Object doCreateInstance(Object clazz, Object... args) {
749            JexlException xjexl = null;
750            Object result = null;
751            JexlInfo info = debugInfo();
752            try {
753                JexlMethod ctor = uberspect.getConstructorMethod(clazz, args, info);
754                if (ctor == null && arithmetic.narrowArguments(args)) {
755                    ctor = uberspect.getConstructorMethod(clazz, args, info);
756                }
757                if (ctor != null) {
758                    result = ctor.invoke(clazz, args);
759                } else {
760                    xjexl = new JexlException(info, "failed finding constructor for " + clazz.toString());
761                }
762            } catch (Exception xany) {
763                xjexl = new JexlException(info, "failed executing constructor for " + clazz.toString(), xany);
764            } finally {
765                if (xjexl != null) {
766                    if (silent) {
767                        logger.warn(xjexl.getMessage(), xjexl.getCause());
768                        return null;
769                    }
770                    throw xjexl;
771                }
772            }
773            return result;
774        }
775    
776        /**
777         * Creates an interpreter.
778         * @param context a JexlContext; if null, the EMPTY_CONTEXT is used instead.
779         * @return an Interpreter
780         */
781        protected Interpreter createInterpreter(JexlContext context) {
782            return createInterpreter(context, isStrict(), isSilent());
783        }
784    
785        /**
786         * Creates an interpreter.
787         * @param context a JexlContext; if null, the EMPTY_CONTEXT is used instead.
788         * @param strictFlag whether the interpreter runs in strict mode
789         * @param silentFlag whether the interpreter runs in silent mode
790         * @return an Interpreter
791         * @since 2.1
792         */
793        protected Interpreter createInterpreter(JexlContext context, boolean strictFlag, boolean silentFlag) {
794            return new Interpreter(this, context == null ? EMPTY_CONTEXT : context, strictFlag, silentFlag);
795        }
796    
797        /**
798         * A soft reference on cache.
799         * <p>The cache is held through a soft reference, allowing it to be GCed under
800         * memory pressure.</p>
801         * @param <K> the cache key entry type
802         * @param <V> the cache key value type
803         */
804        protected class SoftCache<K, V> {
805            /**
806             * The cache size.
807             */
808            private final int size;
809            /**
810             * The soft reference to the cache map.
811             */
812            private SoftReference<Map<K, V>> ref = null;
813    
814            /**
815             * Creates a new instance of a soft cache.
816             * @param theSize the cache size
817             */
818            SoftCache(int theSize) {
819                size = theSize;
820            }
821    
822            /**
823             * Returns the cache size.
824             * @return the cache size
825             */
826            int size() {
827                return size;
828            }
829    
830            /**
831             * Clears the cache.
832             */
833            void clear() {
834                ref = null;
835            }
836    
837            /**
838             * Produces the cache entry set.
839             * @return the cache entry set
840             */
841            Set<Entry<K, V>> entrySet() {
842                Map<K, V> map = ref != null ? ref.get() : null;
843                return map != null ? map.entrySet() : Collections.<Entry<K, V>>emptySet();
844            }
845    
846            /**
847             * Gets a value from cache.
848             * @param key the cache entry key
849             * @return the cache entry value
850             */
851            V get(K key) {
852                final Map<K, V> map = ref != null ? ref.get() : null;
853                return map != null ? map.get(key) : null;
854            }
855    
856            /**
857             * Puts a value in cache.
858             * @param key the cache entry key
859             * @param script the cache entry value
860             */
861            void put(K key, V script) {
862                Map<K, V> map = ref != null ? ref.get() : null;
863                if (map == null) {
864                    map = createCache(size);
865                    ref = new SoftReference<Map<K, V>>(map);
866                }
867                map.put(key, script);
868            }
869        }
870    
871        /**
872         * Creates a cache.
873         * @param <K> the key type
874         * @param <V> the value type
875         * @param cacheSize the cache size, must be > 0
876         * @return a Map usable as a cache bounded to the given size
877         */
878        protected <K, V> Map<K, V> createCache(final int cacheSize) {
879            return new java.util.LinkedHashMap<K, V>(cacheSize, LOAD_FACTOR, true) {
880                /** Serial version UID. */
881                private static final long serialVersionUID = 1L;
882    
883                @Override
884                protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
885                    return size() > cacheSize;
886                }
887            };
888        }
889    
890        /**
891         * Clears the expression cache.
892         * @since 2.1
893         */
894        public void clearCache() {
895            synchronized (parser) {
896                cache.clear();
897            }
898        }
899    
900        /**
901         * Gets the list of variables accessed by a script.
902         * <p>This method will visit all nodes of a script and extract all variables whether they
903         * are written in 'dot' or 'bracketed' notation. (a.b is equivalent to a['b']).</p>
904         * @param script the script
905         * @return the set of variables, each as a list of strings (ant-ish variables use more than 1 string)
906         *         or the empty set if no variables are used
907         * @since 2.1
908         */
909        public Set<List<String>> getVariables(Script script) {
910            if (script instanceof ExpressionImpl) {
911                Set<List<String>> refs = new LinkedHashSet<List<String>>();
912                getVariables(((ExpressionImpl) script).script, refs, null);
913                return refs;
914            } else {
915                return Collections.<List<String>>emptySet();
916            }
917        }
918    
919        /**
920         * Fills up the list of variables accessed by a node.
921         * @param node the node
922         * @param refs the set of variable being filled
923         * @param ref the current variable being filled
924         * @since 2.1
925         */
926        protected void getVariables(JexlNode node, Set<List<String>> refs, List<String> ref) {
927            boolean array = node instanceof ASTArrayAccess;
928            boolean reference = node instanceof ASTReference;
929            int num = node.jjtGetNumChildren();
930            if (array || reference) {
931                List<String> var = ref != null ? ref : new ArrayList<String>();
932                boolean varf = true;
933                for (int i = 0; i < num; ++i) {
934                    JexlNode child = node.jjtGetChild(i);
935                    if (array) {
936                        if (child instanceof ASTReference && child.jjtGetNumChildren() == 1) {
937                            JexlNode desc = child.jjtGetChild(0);
938                            if (varf && desc.isConstant()) {
939                                String image = desc.image;
940                                if (image == null) {
941                                    var.add(new Debugger().data(desc));
942                                } else {
943                                    var.add(image); 
944                                }
945                            } else if (desc instanceof ASTIdentifier) {
946                                if (((ASTIdentifier) desc).getRegister() < 0) {
947                                    List<String> di = new ArrayList<String>(1);
948                                    di.add(desc.image);
949                                    refs.add(di);
950                                }
951                                var = new ArrayList<String>();
952                                varf = false;
953                            }
954                            continue;
955                        } else if (child instanceof ASTIdentifier) {
956                            if (i == 0 && (((ASTIdentifier) child).getRegister() < 0)) {
957                                var.add(child.image);
958                            }
959                            continue;
960                        }
961                    } else {//if (reference) {
962                        if (child instanceof ASTIdentifier) {
963                            if (((ASTIdentifier) child).getRegister() < 0) {
964                                var.add(child.image);
965                            }
966                            continue;
967                        }
968                    }
969                    getVariables(child, refs, var);
970                }
971                if (!var.isEmpty() && var != ref) {
972                    refs.add(var);
973                }
974            } else {
975                for (int i = 0; i < num; ++i) {
976                    getVariables(node.jjtGetChild(i), refs, null);
977                }
978            }
979        }
980    
981        /**
982         * Gets the array of parameters from a script.
983         * @param script the script
984         * @return the parameters which may be empty (but not null) if no parameters were defined
985         * @since 2.1
986         */
987        protected String[] getParameters(Script script) {
988            if (script instanceof ExpressionImpl) {
989                return ((ExpressionImpl) script).getParameters();
990            } else {
991                return new String[0];
992            }
993        }
994    
995        /**
996         * Gets the array of local variable from a script.
997         * @param script the script
998         * @return the local variables array which may be empty (but not null) if no local variables were defined
999         * @since 2.1
1000         */
1001        protected String[] getLocalVariables(Script script) {
1002            if (script instanceof ExpressionImpl) {
1003                return ((ExpressionImpl) script).getLocalVariables();
1004            } else {
1005                return new String[0];
1006            }
1007        }
1008    
1009        /**
1010         * A script scope, stores the declaration of parameters and local variables.
1011         * @since 2.1
1012         */
1013        public static final class Scope {
1014            /**
1015             * The number of parameters.
1016             */
1017            private final int parms;
1018            /**
1019             * The map of named registers aka script parameters.
1020             * Each parameter is associated to a register and is materialized as an offset in the registers array used
1021             * during evaluation.
1022             */
1023            private Map<String, Integer> namedRegisters = null;
1024    
1025            /**
1026             * Creates a new scope with a list of parameters.
1027             * @param parameters the list of parameters
1028             */
1029            public Scope(String... parameters) {
1030                if (parameters != null) {
1031                    parms = parameters.length;
1032                    namedRegisters = new LinkedHashMap<String, Integer>();
1033                    for (int p = 0; p < parms; ++p) {
1034                        namedRegisters.put(parameters[p], Integer.valueOf(p));
1035                    }
1036                } else {
1037                    parms = 0;
1038                }
1039            }
1040    
1041            @Override
1042            public int hashCode() {
1043                return namedRegisters == null ? 0 : parms ^ namedRegisters.hashCode();
1044            }
1045    
1046            @Override
1047            public boolean equals(Object o) {
1048                return o instanceof Scope && equals((Scope) o);
1049            }
1050    
1051            /**
1052             * Whether this frame is equal to another.
1053             * @param frame the frame to compare to
1054             * @return true if equal, false otherwise
1055             */
1056            public boolean equals(Scope frame) {
1057                if (this == frame) {
1058                    return true;
1059                } else if (frame == null || parms != frame.parms) {
1060                    return false;
1061                } else if (namedRegisters == null) {
1062                    return frame.namedRegisters == null;
1063                } else {
1064                    return namedRegisters.equals(frame.namedRegisters);
1065                }
1066            }
1067    
1068            /**
1069             * Checks whether an identifier is a local variable or argument, ie stored in a register. 
1070             * @param name the register name
1071             * @return the register index
1072             */
1073            public Integer getRegister(String name) {
1074                return namedRegisters != null ? namedRegisters.get(name) : null;
1075            }
1076    
1077            /**
1078             * Declares a local variable.
1079             * <p>
1080             * This method creates an new entry in the named register map.
1081             * </p>
1082             * @param name the variable name
1083             * @return the register index storing this variable
1084             */
1085            public Integer declareVariable(String name) {
1086                if (namedRegisters == null) {
1087                    namedRegisters = new LinkedHashMap<String, Integer>();
1088                }
1089                Integer register = namedRegisters.get(name);
1090                if (register == null) {
1091                    register = Integer.valueOf(namedRegisters.size());
1092                    namedRegisters.put(name, register);
1093                }
1094                return register;
1095            }
1096    
1097            /**
1098             * Creates a frame by copying values up to the number of parameters.
1099             * @param values the argument values
1100             * @return the arguments array
1101             */
1102            public Frame createFrame(Object... values) {
1103                if (namedRegisters != null) {
1104                    Object[] arguments = new Object[namedRegisters.size()];
1105                    if (values != null) {
1106                        System.arraycopy(values, 0, arguments, 0, Math.min(parms, values.length));
1107                    }
1108                    return new Frame(arguments, namedRegisters.keySet().toArray(new String[0]));
1109                } else {
1110                    return null;
1111                }
1112            }
1113    
1114            /**
1115             * Gets the (maximum) number of arguments this script expects.
1116             * @return the number of parameters
1117             */
1118            public int getArgCount() {
1119                return parms;
1120            }
1121    
1122            /**
1123             * Gets this script registers, i.e. parameters and local variables.
1124             * @return the register names
1125             */
1126            public String[] getRegisters() {
1127                return namedRegisters != null ? namedRegisters.keySet().toArray(new String[0]) : new String[0];
1128            }
1129    
1130            /**
1131             * Gets this script parameters, i.e. registers assigned before creating local variables.
1132             * @return the parameter names
1133             */
1134            public String[] getParameters() {
1135                if (namedRegisters != null && parms > 0) {
1136                    String[] pa = new String[parms];
1137                    int p = 0;
1138                    for (Map.Entry<String, Integer> entry : namedRegisters.entrySet()) {
1139                        if (entry.getValue().intValue() < parms) {
1140                            pa[p++] = entry.getKey();
1141                        }
1142                    }
1143                    return pa;
1144                } else {
1145                    return null;
1146                }
1147            }
1148    
1149            /**
1150             * Gets this script local variable, i.e. registers assigned to local variables.
1151             * @return the parameter names
1152             */
1153            public String[] getLocalVariables() {
1154                if (namedRegisters != null && parms > 0) {
1155                    String[] pa = new String[parms];
1156                    int p = 0;
1157                    for (Map.Entry<String, Integer> entry : namedRegisters.entrySet()) {
1158                        if (entry.getValue().intValue() >= parms) {
1159                            pa[p++] = entry.getKey();
1160                        }
1161                    }
1162                    return pa;
1163                } else {
1164                    return null;
1165                }
1166            }
1167        }
1168    
1169        /**
1170         * A call frame, created from a scope, stores the arguments and local variables as "registers".
1171         * @since 2.1
1172         */
1173        public static final class Frame {
1174            /** Registers or arguments. */
1175            private Object[] registers = null;
1176            /** Parameter and argument names if any. */
1177            private String[] parameters = null;
1178            
1179            /**
1180             * Creates a new frame.
1181             * @param r the registers
1182             * @param p the parameters
1183             */
1184            Frame(Object[] r, String[] p) {
1185                registers = r;
1186                parameters = p;
1187            }
1188            
1189            /**
1190             * @return the registers
1191             */
1192            public Object[] getRegisters() {
1193                return registers;
1194            }
1195                    
1196            /**
1197             * @return the parameters
1198             */
1199            public String[] getParameters() {
1200                return parameters;
1201            }
1202        }
1203    
1204        /**
1205         * Parses an expression.
1206         * @param expression the expression to parse
1207         * @param info debug information structure
1208         * @return the parsed tree
1209         * @throws JexlException if any error occured during parsing
1210         * @deprecated Use {@link #parse(CharSequence, JexlInfo, Scope)} instead
1211         */
1212        @Deprecated
1213        protected ASTJexlScript parse(CharSequence expression, JexlInfo info) {
1214            return parse(expression, info, null);
1215        }
1216    
1217        /**
1218         * Parses an expression.
1219         * @param expression the expression to parse
1220         * @param info debug information structure
1221         * @param frame the script frame to use
1222         * @return the parsed tree
1223         * @throws JexlException if any error occured during parsing
1224         */
1225        protected ASTJexlScript parse(CharSequence expression, JexlInfo info, Scope frame) {
1226            String expr = cleanExpression(expression);
1227            ASTJexlScript script = null;
1228            JexlInfo dbgInfo = null;
1229            synchronized (parser) {
1230                if (cache != null) {
1231                    script = cache.get(expr);
1232                    if (script != null) {
1233                        Scope f = script.getScope();
1234                        if ((f == null && frame == null) || (f != null && f.equals(frame))) {
1235                            return script;
1236                        }
1237                    }
1238                }
1239                try {
1240                    Reader reader = new StringReader(expr);
1241                    // use first calling method of JexlEngine as debug info
1242                    if (info == null) {
1243                        dbgInfo = debugInfo();
1244                    } else {
1245                        dbgInfo = info.debugInfo();
1246                    }
1247                    parser.setFrame(frame);
1248                    script = parser.parse(reader, dbgInfo);
1249                    // reaccess in case local variables have been declared
1250                    frame = parser.getFrame();
1251                    if (frame != null) {
1252                        script.setScope(frame);
1253                    }
1254                    if (cache != null) {
1255                        cache.put(expr, script);
1256                    }
1257                } catch (TokenMgrError xtme) {
1258                    throw new JexlException.Tokenization(dbgInfo, expression, xtme);
1259                } catch (ParseException xparse) {
1260                    throw new JexlException.Parsing(dbgInfo, expression, xparse);
1261                } finally {
1262                    parser.setFrame(null);
1263                }
1264            }
1265            return script;
1266        }
1267    
1268        /**
1269         * Creates a JexlInfo instance.
1270         * @param fn url/file name
1271         * @param l line number
1272         * @param c column number
1273         * @return a JexlInfo instance
1274         */
1275        protected JexlInfo createInfo(String fn, int l, int c) {
1276            return new DebugInfo(fn, l, c);
1277        }
1278    
1279        /**
1280         * Creates and fills up debugging information.
1281         * <p>This gathers the class, method and line number of the first calling method
1282         * not owned by JexlEngine, UnifiedJEXL or {Script,Expression}Factory.</p>
1283         * @return an Info if debug is set, null otherwise
1284         */
1285        protected JexlInfo debugInfo() {
1286            DebugInfo info = null;
1287            if (debug) {
1288                Throwable xinfo = new Throwable();
1289                xinfo.fillInStackTrace();
1290                StackTraceElement[] stack = xinfo.getStackTrace();
1291                StackTraceElement se = null;
1292                Class<?> clazz = getClass();
1293                for (int s = 1; s < stack.length; ++s, se = null) {
1294                    se = stack[s];
1295                    String className = se.getClassName();
1296                    if (!className.equals(clazz.getName())) {
1297                        // go deeper if called from JexlEngine or UnifiedJEXL
1298                        if (className.equals(JexlEngine.class.getName())) {
1299                            clazz = JexlEngine.class;
1300                        } else if (className.equals(UnifiedJEXL.class.getName())) {
1301                            clazz = UnifiedJEXL.class;
1302                        } else {
1303                            break;
1304                        }
1305                    }
1306                }
1307                if (se != null) {
1308                    info = createInfo(se.getClassName() + "." + se.getMethodName(), se.getLineNumber(), 0).debugInfo();
1309                }
1310            }
1311            return info;
1312        }
1313    
1314        /**
1315         * Trims the expression from front & ending spaces.
1316         * @param str expression to clean
1317         * @return trimmed expression ending in a semi-colon
1318         */
1319        public static String cleanExpression(CharSequence str) {
1320            if (str != null) {
1321                int start = 0;
1322                int end = str.length();
1323                if (end > 0) {
1324                    // trim front spaces
1325                    while (start < end && str.charAt(start) == ' ') {
1326                        ++start;
1327                    }
1328                    // trim ending spaces
1329                    while (end > 0 && str.charAt(end - 1) == ' ') {
1330                        --end;
1331                    }
1332                    return str.subSequence(start, end).toString();
1333                }
1334                return "";
1335            }
1336            return null;
1337        }
1338    
1339        /**
1340         * Read from a reader into a local buffer and return a String with
1341         * the contents of the reader.
1342         * @param scriptReader to be read.
1343         * @return the contents of the reader as a String.
1344         * @throws IOException on any error reading the reader.
1345         */
1346        public static String readerToString(Reader scriptReader) throws IOException {
1347            StringBuilder buffer = new StringBuilder();
1348            BufferedReader reader;
1349            if (scriptReader instanceof BufferedReader) {
1350                reader = (BufferedReader) scriptReader;
1351            } else {
1352                reader = new BufferedReader(scriptReader);
1353            }
1354            try {
1355                String line;
1356                while ((line = reader.readLine()) != null) {
1357                    buffer.append(line).append('\n');
1358                }
1359                return buffer.toString();
1360            } finally {
1361                try {
1362                    reader.close();
1363                } catch (IOException xio) {
1364                    // ignore
1365                }
1366            }
1367    
1368        }
1369    }