View Javadoc

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