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 */
017package org.apache.commons.jexl2;
018
019import java.io.BufferedReader;
020import java.io.File;
021import java.io.FileReader;
022import java.io.IOException;
023import java.io.InputStreamReader;
024import java.io.StringReader;
025import java.io.Reader;
026import java.net.URL;
027import java.net.URLConnection;
028import java.lang.ref.SoftReference;
029import java.util.ArrayList;
030import java.util.Map;
031import java.util.Set;
032import java.util.Collections;
033import java.util.LinkedHashMap;
034import java.util.LinkedHashSet;
035import java.util.List;
036import java.util.Map.Entry;
037import org.apache.commons.logging.Log;
038import org.apache.commons.logging.LogFactory;
039
040import org.apache.commons.jexl2.parser.ParseException;
041import org.apache.commons.jexl2.parser.Parser;
042import org.apache.commons.jexl2.parser.JexlNode;
043import org.apache.commons.jexl2.parser.TokenMgrError;
044import org.apache.commons.jexl2.parser.ASTJexlScript;
045
046import org.apache.commons.jexl2.introspection.Uberspect;
047import org.apache.commons.jexl2.introspection.UberspectImpl;
048import org.apache.commons.jexl2.introspection.JexlMethod;
049import org.apache.commons.jexl2.parser.ASTArrayAccess;
050import org.apache.commons.jexl2.parser.ASTIdentifier;
051import org.apache.commons.jexl2.parser.ASTReference;
052
053/**
054 * <p>
055 * Creates and evaluates Expression and Script objects.
056 * Determines the behavior of Expressions &amp; Scripts during their evaluation with respect to:
057 * </p>
058 * <ul>
059 *  <li>Introspection, see {@link Uberspect}</li>
060 *  <li>Arithmetic &amp; comparison, see {@link JexlArithmetic}</li>
061 *  <li>Error reporting</li>
062 *  <li>Logging</li>
063 * </ul>
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 &amp; 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 &amp; 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 */
098public 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 &gt; 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 &amp; 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}