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