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    *      https://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.jexl3.internal;
18  
19  import static org.apache.commons.jexl3.parser.JexlParser.PRAGMA_IMPORT;
20  import static org.apache.commons.jexl3.parser.JexlParser.PRAGMA_JEXLNS;
21  import static org.apache.commons.jexl3.parser.JexlParser.PRAGMA_MODULE;
22  import static org.apache.commons.jexl3.parser.JexlParser.PRAGMA_OPTIONS;
23  
24  import java.nio.charset.Charset;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.LinkedHashMap;
29  import java.util.LinkedHashSet;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Objects;
33  import java.util.Set;
34  import java.util.concurrent.atomic.AtomicBoolean;
35  import java.util.function.Consumer;
36  import java.util.function.IntFunction;
37  import java.util.function.Predicate;
38  import java.util.function.Supplier;
39  
40  import org.apache.commons.jexl3.JexlArithmetic;
41  import org.apache.commons.jexl3.JexlBuilder;
42  import org.apache.commons.jexl3.JexlCache;
43  import org.apache.commons.jexl3.JexlContext;
44  import org.apache.commons.jexl3.JexlEngine;
45  import org.apache.commons.jexl3.JexlException;
46  import org.apache.commons.jexl3.JexlFeatures;
47  import org.apache.commons.jexl3.JexlInfo;
48  import org.apache.commons.jexl3.JexlOptions;
49  import org.apache.commons.jexl3.JexlScript;
50  import org.apache.commons.jexl3.internal.introspection.SandboxUberspect;
51  import org.apache.commons.jexl3.internal.introspection.Uberspect;
52  import org.apache.commons.jexl3.introspection.JexlMethod;
53  import org.apache.commons.jexl3.introspection.JexlPermissions;
54  import org.apache.commons.jexl3.introspection.JexlSandbox;
55  import org.apache.commons.jexl3.introspection.JexlUberspect;
56  import org.apache.commons.jexl3.parser.ASTArrayAccess;
57  import org.apache.commons.jexl3.parser.ASTFunctionNode;
58  import org.apache.commons.jexl3.parser.ASTIdentifier;
59  import org.apache.commons.jexl3.parser.ASTIdentifierAccess;
60  import org.apache.commons.jexl3.parser.ASTJexlScript;
61  import org.apache.commons.jexl3.parser.ASTMethodNode;
62  import org.apache.commons.jexl3.parser.ASTNumberLiteral;
63  import org.apache.commons.jexl3.parser.ASTStringLiteral;
64  import org.apache.commons.jexl3.parser.JexlNode;
65  import org.apache.commons.jexl3.parser.JexlScriptParser;
66  import org.apache.commons.jexl3.parser.Parser;
67  import org.apache.commons.jexl3.parser.StringProvider;
68  import org.apache.commons.logging.Log;
69  import org.apache.commons.logging.LogFactory;
70  
71  /**
72   * A JexlEngine implementation.
73   * @since 2.0
74   */
75  public class Engine extends JexlEngine implements JexlUberspect.ConstantResolverFactory {
76      /**
77       * Gets the default instance of Uberspect.
78       * <p>This is lazily initialized to avoid building a default instance if there
79       * is no use for it. The main reason for not using the default Uberspect instance is to
80       * be able to use a (low level) introspector created with a given logger
81       * instead of the default one.</p>
82       * <p>Implemented as on demand holder idiom.</p>
83       */
84      private static final class UberspectHolder {
85          /** The default uberspector that handles all introspection patterns. */
86          static final Uberspect UBERSPECT =
87                  new Uberspect(LogFactory.getLog(JexlEngine.class),
88                          JexlUberspect.JEXL_STRATEGY,
89                          JexlPermissions.parse());
90  
91          /** Non-instantiable. */
92          private UberspectHolder() {}
93      }
94  
95      /**
96       * Utility class to collect variables.
97       */
98      protected static class VarCollector {
99          /**
100          * The collected variables represented as a set of list of strings.
101          */
102         private final Set<List<String>> refs = new LinkedHashSet<>();
103         /**
104          * The current variable being collected.
105          */
106         private List<String> ref = new ArrayList<>();
107         /**
108          * The node that started the collect.
109          */
110         private JexlNode root;
111         /**
112          * Whether constant array-access is considered equivalent to dot-access;
113          * if so, > 1 means collect any constant (set,map,...) instead of just
114          * strings and numbers.
115          */
116         final int mode;
117 
118         /**
119          * Constructs a new instance.
120          * @param constaa whether constant array-access is considered equivalent to dot-access
121          */
122         protected VarCollector(final int constaa) {
123             mode = constaa;
124         }
125 
126         /**
127          * Adds a 'segment' to the variable being collected.
128          * @param name the name
129          */
130         public void add(final String name) {
131             ref.add(name);
132         }
133 
134         /**
135          * Starts/stops a variable collect.
136          * @param node starts if not null, stop if null
137          */
138         public void collect(final JexlNode node) {
139             if (!ref.isEmpty()) {
140                 refs.add(ref);
141                 ref = new ArrayList<>();
142             }
143             root = node;
144         }
145 
146         /**
147          *@return the collected variables
148          */
149         public Set<List<String>> collected() {
150             return refs;
151         }
152 
153         /**
154          * @return true if currently collecting a variable, false otherwise
155          */
156         public boolean isCollecting() {
157             return root instanceof ASTIdentifier;
158         }
159     }
160 
161     /**
162      * The features allowed for property set/get methods.
163      */
164     protected static final JexlFeatures PROPERTY_FEATURES = new JexlFeatures()
165             .localVar(false)
166             .loops(false)
167             .lambda(false)
168             .script(false)
169             .arrayReferenceExpr(false)
170             .methodCall(false)
171             .register(true);
172 
173     /**
174      * Use {@link Engine#getUberspect(Log, JexlUberspect.ResolverStrategy, JexlPermissions)}.
175      * @deprecated 3.3
176      * @param logger the logger
177      * @param strategy the strategy
178      * @return an Uberspect instance
179      */
180     @Deprecated
181     public static Uberspect getUberspect(final Log logger, final JexlUberspect.ResolverStrategy strategy) {
182         return getUberspect(logger, strategy, null);
183     }
184 
185     /**
186      * Gets the default instance of Uberspect.
187      * <p>This is lazily initialized to avoid building a default instance if there
188      * is no use for it.</p>
189      * <lu>The main reason for not using the default Uberspect instance are:
190      * <li>Using a (low level) introspector created with a given logger instead of the default one</li>
191      * <li>Using a (restricted) set of permissions</li>
192      * </lu>
193      * @param logger the logger to use for the underlying Uberspect
194      * @param strategy the property resolver strategy
195      * @param permissions the introspection permissions
196      * @return Uberspect the default uberspector instance.
197      * @since 3.3
198      */
199     public static Uberspect getUberspect(
200             final Log logger,
201             final JexlUberspect.ResolverStrategy strategy,
202             final JexlPermissions permissions) {
203         if ((logger == null || logger.equals(LogFactory.getLog(JexlEngine.class)))
204             && (strategy == null || strategy == JexlUberspect.JEXL_STRATEGY)
205             && (permissions == null || permissions == JexlPermissions.UNRESTRICTED)) {
206             return UberspectHolder.UBERSPECT;
207         }
208         return new Uberspect(logger, strategy, permissions);
209     }
210 
211     /**
212      * Solves an optional option.
213      * @param conf the option as configured, may be null
214      * @param def the default value if null, shall not be null
215      * @param <T> the option type
216      * @return conf or def
217      */
218     private static <T> T option(final T conf, final T def) {
219         return conf == null ? def : conf;
220     }
221     /**
222      * The Log to which all JexlEngine messages will be logged.
223      */
224     protected final Log logger;
225     /**
226      * The JexlUberspect instance.
227      */
228     protected final JexlUberspect uberspect;
229     /**
230      * The {@link JexlArithmetic} instance.
231      */
232     protected final JexlArithmetic arithmetic;
233     /**
234      * The map of 'prefix:function' to object implementing the namespaces.
235      */
236     protected final Map<String, Object> functions;
237     /**
238      * The default class name resolver.
239      */
240     protected final FqcnResolver classNameSolver;
241     /**
242      * The maximum stack height.
243      */
244     protected final int stackOverflow;
245     /**
246      * Whether this engine considers unknown variables, methods and constructors as errors.
247      */
248     protected final boolean strict;
249     /**
250      * Whether this engine considers null in navigation expression as errors.
251      */
252     protected final boolean safe;
253     /**
254      * Whether expressions evaluated by this engine will throw exceptions (false) or return null (true) on errors.
255      * Default is false.
256      */
257     protected final boolean silent;
258     /**
259      * Whether expressions evaluated by this engine will throw JexlException.Cancel (true) or return null (false) when
260      * interrupted.
261      * Default is true when not silent and strict.
262      */
263     protected final boolean cancellable;
264     /**
265      * Whether error messages will carry debugging information.
266      */
267     protected final boolean debug;
268     /**
269      * The set of default script parsing features.
270      */
271     protected final JexlFeatures scriptFeatures;
272     /**
273      * The set of default expression parsing features.
274      */
275     protected final JexlFeatures expressionFeatures;
276     /**
277      * The default charset.
278      */
279     protected final Charset charset;
280     /**
281      * The Jexl script parser factory.
282      */
283     protected final Supplier<JexlScriptParser> parserFactory;
284     /**
285      * The atomic parsing flag; true whilst parsing.
286      */
287     protected final AtomicBoolean parsing = new AtomicBoolean();
288     /**
289      * The {@link Parser}; when parsing expressions, this engine uses the parser if it
290      * is not already in use otherwise it will create a new temporary one.
291      */
292     protected final JexlScriptParser parser;
293     /**
294      * The expression max length to hit the cache.
295      */
296     protected final int cacheThreshold;
297     /**
298      * The expression cache.
299      */
300     protected final JexlCache<Source, Object> cache;
301     /**
302      * Collect all or only dot references.
303      */
304     protected final int collectMode;
305     /**
306      * A cached version of the options.
307      */
308     protected final JexlOptions options;
309     /**
310      * The set of caches created by this engine.
311      * <p>Caches are soft-referenced by the engine so they can be cleaned on class loader change.</p>
312      */
313     protected final MetaCache metaCache;
314     /**
315      * Creates an engine with default arguments.
316      */
317     public Engine() {
318         this(new JexlBuilder());
319     }
320 
321     /**
322      * Creates a JEXL engine using the provided {@link JexlBuilder}.
323      * @param conf the builder
324      */
325     public Engine(final JexlBuilder conf) {
326         // options:
327         this.options = conf.options().copy();
328         this.strict = options.isStrict();
329         this.safe = options.isSafe();
330         this.silent = options.isSilent();
331         this.cancellable = option(conf.cancellable(), !silent && strict);
332         options.setCancellable(cancellable);
333         this.debug = option(conf.debug(), true);
334         this.collectMode = conf.collectMode();
335         this.stackOverflow = conf.stackOverflow() > 0? conf.stackOverflow() : Integer.MAX_VALUE;
336         // core properties:
337         final JexlUberspect uber = conf.uberspect() == null
338                 ? getUberspect(conf.logger(), conf.strategy(), conf.permissions())
339                 : conf.uberspect();
340         if (uber == null) {
341             throw new IllegalArgumentException("uberspect cannot be null");
342         }
343         final ClassLoader loader = conf.loader();
344         if (loader != null) {
345             uber.setClassLoader(loader);
346         }
347         final JexlSandbox sandbox = conf.sandbox();
348         if (sandbox == null) {
349             this.uberspect = uber;
350         } else {
351             this.uberspect = new SandboxUberspect(uber, sandbox);
352         }
353         this.logger = conf.logger() == null ? LogFactory.getLog(JexlEngine.class) : conf.logger();
354         this.arithmetic = conf.arithmetic() == null ? new JexlArithmetic(this.strict) : conf.arithmetic();
355         options.setMathContext(arithmetic.getMathContext());
356         options.setMathScale(arithmetic.getMathScale());
357         options.setStrictArithmetic(arithmetic.isStrict());
358         final Map<String, Object> ns = conf.namespaces();
359         this.functions = ns == null || ns.isEmpty()? Collections.emptyMap() : ns; // should we make a copy?
360         this.classNameSolver = new FqcnResolver(uberspect, conf.imports());
361         // parsing & features:
362         final JexlFeatures features = conf.features() == null ? DEFAULT_FEATURES : conf.features();
363         Predicate<String> nsTest = features.namespaceTest();
364         final Set<String> nsNames = functions.keySet();
365         if (!nsNames.isEmpty()) {
366             nsTest = nsTest == JexlFeatures.TEST_STR_FALSE ?nsNames::contains : nsTest.or(nsNames::contains);
367         }
368         this.expressionFeatures = new JexlFeatures(features).script(false).namespaceTest(nsTest);
369         this.scriptFeatures = new JexlFeatures(features).script(true).namespaceTest(nsTest);
370         this.charset = conf.charset();
371         // caching:
372         final IntFunction<JexlCache<?, ?>> factory = conf.cacheFactory();
373         this.metaCache = new MetaCache(factory == null ? SoftCache::new : factory);
374         this.cache = metaCache.createCache(conf.cache());
375         this.cacheThreshold = conf.cacheThreshold();
376         this.parserFactory = conf.parserFactory() == null ?
377                () -> new Parser(new StringProvider(";"))
378                 : conf.parserFactory();
379         this.parser = parserFactory.get();
380     }
381 
382     @Override
383     public void clearCache() {
384         if (cache != null) {
385             cache.clear();
386         }
387     }
388 
389     JexlCache<Source, Object> getCache() {
390         return cache;
391     }
392 
393     @Override
394     public Script createExpression(final JexlInfo info, final String expression) {
395         return createScript(expressionFeatures, info, expression);
396     }
397 
398     /**
399      * Creates an interpreter.
400      * @param context a JexlContext; if null, the empty context is used instead.
401      * @param frame   the interpreter frame
402      * @param opts    the evaluation options
403      * @return an Interpreter
404      */
405     protected Interpreter createInterpreter(final JexlContext context, final Frame frame, final JexlOptions opts) {
406         return new Interpreter(this, opts, context, frame);
407     }
408 
409     @Override
410     public TemplateEngine createJxltEngine(final boolean noScript, final int cacheSize, final char immediate, final char deferred) {
411         return new TemplateEngine(this, noScript, cacheSize, immediate, deferred);
412     }
413 
414     @Override
415     public Script createScript(final JexlFeatures features, final JexlInfo info, final String scriptText, final String... names) {
416         Objects.requireNonNull(scriptText, "scriptText");
417         final String source = trimSource(scriptText);
418         final Scope scope = names == null || names.length == 0? null : new Scope(null, names);
419         final JexlFeatures ftrs = features == null ? scriptFeatures : features;
420         final ASTJexlScript tree = parse(info, ftrs, source, scope);
421         return new Script(this, source, tree);
422     }
423 
424     /**
425      * Creates a template interpreter.
426      * @param args the template interpreter arguments
427      */
428     protected Interpreter createTemplateInterpreter(final TemplateInterpreter.Arguments args) {
429         return new TemplateInterpreter(args);
430     }
431 
432     /**
433      * Creates a new instance of an object using the most appropriate constructor
434      * based on the arguments.
435      * @param clazz the class to instantiate
436      * @param args  the constructor arguments
437      * @return the created object instance or null on failure when silent
438      */
439     protected Object doCreateInstance(final Object clazz, final Object... args) {
440         JexlException xjexl = null;
441         Object result = null;
442         final JexlInfo info = createInfo();
443         try {
444             JexlMethod ctor = uberspect.getConstructor(clazz, args);
445             if (ctor == null && arithmetic.narrowArguments(args)) {
446                 ctor = uberspect.getConstructor(clazz, args);
447             }
448             if (ctor != null) {
449                 result = ctor.invoke(clazz, args);
450             } else {
451                 xjexl = new JexlException.Method(info, clazz.toString(), args);
452             }
453         } catch (final JexlException xany) {
454             xjexl = xany;
455         } catch (final Exception xany) {
456             xjexl = new JexlException.Method(info, clazz.toString(), args, xany);
457         }
458         if (xjexl != null) {
459             if (silent) {
460                 if (logger.isWarnEnabled()) {
461                     logger.warn(xjexl.getMessage(), xjexl.getCause());
462                 }
463                 return null;
464             }
465             throw xjexl.clean();
466         }
467         return result;
468     }
469 
470     /**
471      * Compute a script options for evaluation.
472      * <p>This calls processPragma(...).
473      * @param script the script
474      * @param context the context
475      * @return the options
476      */
477     protected JexlOptions evalOptions(final ASTJexlScript script, final JexlContext context) {
478         final JexlOptions opts = evalOptions(context);
479         if (opts != options) {
480             // when feature lexical, try hard to run lexical
481             if (scriptFeatures.isLexical()) {
482                 opts.setLexical(true);
483             }
484             if (scriptFeatures.isLexicalShade()) {
485                 opts.setLexicalShade(true);
486             }
487             if (scriptFeatures.supportsConstCapture()) {
488                 opts.setConstCapture(true);
489             }
490         }
491         if (script != null) {
492            // process script pragmas if any
493            processPragmas(script, context, opts);
494         }
495         return opts;
496     }
497 
498     /**
499      * Extracts the engine evaluation options from context if available, the engine
500      * options otherwise.
501      * <p>If the context is an options handle and the handled options shared instance flag
502      * is false, this method creates a copy of the options making them immutable during execution.
503      * @param context the context
504      * @return the options if any
505      */
506     protected JexlOptions evalOptions(final JexlContext context) {
507         // Make a copy of the handled options if any
508         if (context instanceof JexlContext.OptionsHandle) {
509             final JexlOptions jexlo = ((JexlContext.OptionsHandle) context).getEngineOptions();
510             if (jexlo != null) {
511                 return jexlo.isSharedInstance()? jexlo : jexlo.copy();
512             }
513         } else if (context instanceof JexlEngine.Options) {
514             return evalOptions((JexlEngine.Options) context);
515         }
516         return options;
517     }
518 
519     /**
520      * Obsolete version of options evaluation.
521      * @param opts the obsolete instance of options
522      * @return the newer class of options
523      */
524     private JexlOptions evalOptions(final JexlEngine.Options opts) {
525         // This condition and block for compatibility between 3.1 and 3.2
526         final JexlOptions jexlo = options.copy();
527         final JexlEngine jexl = this;
528         jexlo.setCancellable(option(opts.isCancellable(), jexl.isCancellable()));
529         jexlo.setSilent(option(opts.isSilent(), jexl.isSilent()));
530         jexlo.setStrict(option(opts.isStrict(), jexl.isStrict()));
531         final JexlArithmetic jexla = jexl.getArithmetic();
532         jexlo.setStrictArithmetic(option(opts.isStrictArithmetic(), jexla.isStrict()));
533         jexlo.setMathContext(opts.getArithmeticMathContext());
534         jexlo.setMathScale(opts.getArithmeticMathScale());
535         return jexlo;
536     }
537 
538     @Override
539     public JexlArithmetic getArithmetic() {
540         return arithmetic;
541     }
542 
543     @Override
544     public Charset getCharset() {
545         return charset;
546     }
547 
548     /**
549      * Gets the array of local variable from a script.
550      * @param script the script
551      * @return the local variables array which may be empty (but not null) if no local variables were defined
552      * @since 3.0
553      * @deprecated 3.6.1
554      */
555     @Deprecated()
556     protected String[] getLocalVariables(final JexlScript script) {
557         return script.getLocalVariables();
558     }
559 
560     /**
561      * Solves a namespace using this engine map of functions.
562      * @param name the namespoce name
563      * @return the object associated
564      */
565     final Object getNamespace(final String name) {
566         return functions.get(name);
567     }
568 
569     /**
570      * Gets the array of parameters from a script.
571      * @param script the script
572      * @return the parameters which may be empty (but not null) if no parameters were defined
573      * @since 3.0
574      * @deprecated 3.6.1
575      */
576     @Deprecated
577     protected String[] getParameters(final JexlScript script) {
578         return script.getParameters();
579     }
580 
581     @Override
582     public Object getProperty(final JexlContext context, final Object bean, final String expr) {
583         // synthesize expr using register
584         String src = trimSource(expr);
585         src = "#0" + (src.charAt(0) == '[' ? "" : ".") + src;
586         try {
587             final Scope scope = new Scope(null, "#0");
588             final ASTJexlScript script = parse(createInfo(), PROPERTY_FEATURES, src, scope);
589             final JexlNode node = script.jjtGetChild(0);
590             final Frame frame = script.createFrame(bean);
591             final Interpreter interpreter = createInterpreter(context == null ? EMPTY_CONTEXT : context, frame, options);
592             return interpreter.visitLexicalNode(node, null);
593         } catch (final JexlException xjexl) {
594             if (silent) {
595                 if (logger.isWarnEnabled()) {
596                     logger.warn(xjexl.getMessage(), xjexl.getCause());
597                 }
598                 return null;
599             }
600             throw xjexl.clean();
601         }
602     }
603 
604     @Override
605     public Object getProperty(final Object bean, final String expr) {
606         return getProperty(null, bean, expr);
607     }
608 
609     @Override
610     public JexlUberspect getUberspect() {
611         return uberspect;
612     }
613 
614     /**
615      * Gets the list of variables accessed by a script.
616      * <p>This method will visit all nodes of a script and extract all variables whether they
617      * are written in 'dot' or 'bracketed' notation. (a.b is equivalent to a['b']).</p>
618      * @param script the script
619      * @return the set of variables, each as a list of strings (ant-ish variables use more than 1 string)
620      *         or the empty set if no variables are used
621      */
622     protected Set<List<String>> getVariables(final ASTJexlScript script) {
623         final VarCollector collector = varCollector();
624         getVariables(script, script, collector);
625         return collector.collected();
626     }
627 
628     /**
629      * Fills up the list of variables accessed by a node.
630      * @param script the owning script
631      * @param node the node
632      * @param collector the variable collector
633      */
634     protected void getVariables(final ASTJexlScript script, final JexlNode node, final VarCollector collector) {
635         if (node instanceof ASTIdentifier) {
636             final JexlNode parent = node.jjtGetParent();
637             if (parent instanceof ASTMethodNode || parent instanceof ASTFunctionNode) {
638                 // skip identifiers for methods and functions
639                 collector.collect(null);
640                 return;
641             }
642             final ASTIdentifier identifier = (ASTIdentifier) node;
643             final int symbol = identifier.getSymbol();
644             // symbols that are captured are considered "global" variables
645             if (symbol >= 0 && script != null && !script.isCapturedSymbol(symbol)) {
646                 collector.collect(null);
647             } else {
648                 // start collecting from identifier
649                 collector.collect(identifier);
650                 collector.add(identifier.getName());
651             }
652         } else if (node instanceof ASTIdentifierAccess) {
653             final JexlNode parent = node.jjtGetParent();
654             if (parent instanceof ASTMethodNode || parent instanceof ASTFunctionNode) {
655                 // skip identifiers for methods and functions
656                 collector.collect(null);
657                 return;
658             }
659             // belt and suspender since an identifier should have been seen first
660             if (collector.isCollecting()) {
661                 collector.add(((ASTIdentifierAccess) node).getName());
662             }
663         } else if (node instanceof ASTArrayAccess && collector.mode > 0) {
664             final int num = node.jjtGetNumChildren();
665             // collect only if array access is const and follows an identifier
666             boolean collecting = collector.isCollecting();
667             for (int i = 0; i < num; ++i) {
668                 final JexlNode child = node.jjtGetChild(i);
669                 if (collecting && child.isConstant()) {
670                     // collect all constants or only string and number literals
671                     final boolean collect = collector.mode > 1
672                             || child instanceof ASTStringLiteral || child instanceof ASTNumberLiteral;
673                     if (collect) {
674                         final String image = child.toString();
675                         collector.add(image);
676                     }
677                 } else {
678                     collecting = false;
679                     collector.collect(null);
680                     getVariables(script, child, collector);
681                     collector.collect(null);
682                 }
683             }
684         } else {
685             final int num = node.jjtGetNumChildren();
686             for (int i = 0; i < num; ++i) {
687                 getVariables(script, node.jjtGetChild(i), collector);
688             }
689             collector.collect(null);
690         }
691     }
692 
693     @Override
694     public Object invokeMethod(final Object obj, final String meth, final Object... args) {
695         JexlException xjexl = null;
696         Object result = null;
697         final JexlInfo info = createInfo();
698         try {
699             JexlMethod method = uberspect.getMethod(obj, meth, args);
700             if (method == null && arithmetic.narrowArguments(args)) {
701                 method = uberspect.getMethod(obj, meth, args);
702             }
703             if (method != null) {
704                 result = method.invoke(obj, args);
705             } else {
706                 xjexl = new JexlException.Method(info, meth, args);
707             }
708         } catch (final JexlException xany) {
709             xjexl = xany;
710         } catch (final Exception xany) {
711             xjexl = new JexlException.Method(info, meth, args, xany);
712         }
713         if (xjexl != null) {
714             if (!silent) {
715                 throw xjexl.clean();
716             }
717             if (logger.isWarnEnabled()) {
718                 logger.warn(xjexl.getMessage(), xjexl.getCause());
719             }
720         }
721         return result;
722     }
723 
724     @Override
725     public boolean isCancellable() {
726         return this.cancellable;
727     }
728 
729     @Override
730     public boolean isDebug() {
731         return this.debug;
732     }
733 
734     @Override
735     public boolean isSilent() {
736         return this.silent;
737     }
738 
739     @Override
740     public boolean isStrict() {
741         return this.strict;
742     }
743 
744     @Override
745     public <T> T newInstance(final Class<? extends T> clazz, final Object... args) {
746         return clazz.cast(doCreateInstance(clazz, args));
747     }
748 
749     @Override
750     public Object newInstance(final String clazz, final Object... args) {
751         return doCreateInstance(clazz, args);
752     }
753 
754     @Override
755     public JexlUberspect.ClassConstantResolver createConstantResolver(final Collection<String> imports) {
756         return imports == null || imports.isEmpty()
757                 ? classNameSolver
758                 : new FqcnResolver(classNameSolver).importPackages(imports);
759     }
760 
761     /**
762      * Sets options from this engine options.
763      * @param opts the options to set
764      * @return the options
765      */
766     public JexlOptions optionsSet(final JexlOptions opts) {
767         if (opts != null) {
768             opts.set(options);
769         }
770         return opts;
771     }
772 
773     /**
774      * Parses an expression.
775      *
776      * @param info      information structure
777      * @param parsingf  the set of parsing features
778      * @param src      the expression to parse
779      * @param scope     the script frame
780      * @return the parsed tree
781      * @throws JexlException if any error occurred during parsing
782      */
783     protected ASTJexlScript parse(final JexlInfo info, final JexlFeatures parsingf, final String src, final Scope scope) {
784         final boolean cached = src.length() < cacheThreshold && cache != null;
785         final JexlFeatures features = parsingf != null ? parsingf : DEFAULT_FEATURES;
786         final Source source = cached ? new Source(features, Scope.getSymbolsMap(scope), src) : null;
787         if (source != null) {
788             final Object c = cache.get(source);
789             if (c instanceof ASTJexlScript) {
790                 return (ASTJexlScript) c;
791             }
792         }
793         final JexlInfo ninfo = info != null ? info : createInfo();
794         final JexlEngine se = putThreadEngine(this);
795         ASTJexlScript script;
796         try {
797             // if parser not in use...
798             if (parsing.compareAndSet(false, true)) {
799                 synchronized (parsing) {
800                     try {
801                         // lets parse
802                         script = parser.parse(ninfo, features, src, scope);
803                     } finally {
804                         // no longer in use
805                         parsing.set(false);
806                     }
807                 }
808             } else {
809                 // ...otherwise parser was in use, create a new temporary one
810                 script = parserFactory.get().parse(ninfo, features, src, scope);
811             }
812             if (source != null) {
813                 cache.put(source, script);
814             }
815         } finally {
816             // restore thread local engine
817             putThreadEngine(se);
818         }
819         return script;
820     }
821 
822     /**
823      * Parses a Jexl expression or script.
824      * @param info the JexlInfo
825      * @param expr whether to parse an expression or a script
826      * @param src the source to parse
827      * @param scope the scope, maybe null
828      * @return the parsed tree
829      */
830     protected ASTJexlScript jxltParse(final JexlInfo info, final boolean expr, final String src, final Scope scope) {
831         synchronized(parsing) {
832             return parser.jxltParse(info, expr ? this.expressionFeatures : this.scriptFeatures, src, scope);
833         }
834     }
835 
836     /**
837      * Processes jexl.module.ns pragma.
838      *
839      * <p>If the value is empty, the namespace will be cleared which may be useful to debug and force unload
840      * the object bound to the namespace.</p>
841      * @param ns the namespace map
842      * @param key the key the namespace
843      * @param value the value, ie the expression to evaluate and its result bound to the namespace
844      * @param info the expression info
845      * @param context the value-as-expression evaluation context
846      */
847     private void processPragmaModule(final Map<String, Object> ns, final String key, final Object value, final JexlInfo info,
848             final JexlContext context) {
849         // jexl.module.***
850         final String module = key.substring(PRAGMA_MODULE.length());
851         if (module.isEmpty()) {
852             if (logger.isWarnEnabled()) {
853                 logger.warn(module + ": invalid module declaration");
854             }
855         } else {
856             withValueSet(value, o -> {
857                 if (!(o instanceof CharSequence)) {
858                     if (logger.isWarnEnabled()) {
859                         logger.warn(module + ": unable to define module from " + value);
860                     }
861                 } else {
862                     final String moduleSrc = o.toString();
863                     final Object functor;
864                     if (context instanceof JexlContext.ModuleProcessor) {
865                         final JexlContext.ModuleProcessor processor = (JexlContext.ModuleProcessor) context;
866                         functor = processor.processModule(this, info, module, moduleSrc);
867                     } else {
868                         final Object moduleObject = createExpression(info, moduleSrc).evaluate(context);
869                         functor = moduleObject instanceof Script ? ((Script) moduleObject).execute(context) : moduleObject;
870                     }
871                     if (functor != null) {
872                         ns.put(module, functor);
873                     } else {
874                         ns.remove(module);
875                     }
876                 }
877             });
878         }
879     }
880 
881     /**
882      * Processes jexl.namespace.ns pragma.
883      * @param ns the namespace map
884      * @param key the key
885      * @param value the value, ie the class
886      */
887     private void processPragmaNamespace(final Map<String, Object> ns, final String key, final Object value) {
888         if (value instanceof String) {
889             // jexl.namespace.***
890             final String namespaceName = key.substring(PRAGMA_JEXLNS.length());
891             if (!namespaceName.isEmpty()) {
892                 final String nsclass = value.toString();
893                 final Class<?> clazz = uberspect.getClassByName(nsclass);
894                 if (clazz == null) {
895                     if (logger.isWarnEnabled()) {
896                         logger.warn(key + ": unable to find class " + nsclass);
897                     }
898                 } else {
899                     ns.put(namespaceName, clazz);
900                 }
901             }
902         } else if (logger.isWarnEnabled()) {
903             logger.warn(key + ": ambiguous declaration " + value);
904         }
905     }
906 
907     /**
908      * Processes a script pragmas.
909      * <p>Only called from options(...)
910      * @param script the script
911      * @param context the context
912      * @param opts the options
913      */
914     protected void processPragmas(final ASTJexlScript script, final JexlContext context, final JexlOptions opts) {
915         final Map<String, Object> pragmas = script.getPragmas();
916         if (pragmas != null && !pragmas.isEmpty()) {
917             final JexlContext.PragmaProcessor processor =
918                     context instanceof JexlContext.PragmaProcessor
919                             ? (JexlContext.PragmaProcessor) context
920                             : null;
921             Map<String, Object> ns = null;
922             for (final Map.Entry<String, Object> pragma : pragmas.entrySet()) {
923                 final String key = pragma.getKey();
924                 final Object value = pragma.getValue();
925                 if (PRAGMA_OPTIONS.equals(key)) {
926                     if (value instanceof String) {
927                         // jexl.options
928                         final String[] vs = value.toString().split(" ");
929                         opts.setFlags(vs);
930                     }
931                 }  else if (PRAGMA_IMPORT.equals(key)) {
932                     // jexl.import, may use a set
933                     final Set<String> is = new LinkedHashSet<>();
934                     withValueSet(value, o -> {
935                         if (o instanceof String) {
936                             is.add(o.toString());
937                         }
938                     });
939                     if (!is.isEmpty()) {
940                         opts.setImports(is);
941                     }
942                 } else if (key.startsWith(PRAGMA_JEXLNS)) {
943                     if (ns == null)  {
944                         ns = new LinkedHashMap<>();
945                     }
946                     processPragmaNamespace(ns, key, value);
947                     if (!ns.isEmpty()) {
948                         opts.setNamespaces(ns);
949                     }
950                 } else if (key.startsWith(PRAGMA_MODULE)) {
951                     if (ns == null)  {
952                         ns = new LinkedHashMap<>();
953                     }
954                     processPragmaModule(ns, key, value, script.jexlInfo(), context);
955                     if (!ns.isEmpty()) {
956                         opts.setNamespaces(ns);
957                     }
958                 }
959                 // user-defined processor may alter options
960                 if (processor != null) {
961                     processor.processPragma(opts, key, value);
962                 }
963             }
964         }
965     }
966 
967     /**
968      * Swaps the current thread local engine.
969      * @param jexl the engine or null
970      * @return the previous thread local engine
971      */
972     protected JexlEngine putThreadEngine(final JexlEngine jexl) {
973         final JexlEngine pjexl = ENGINE.get();
974         ENGINE.set(jexl);
975         return pjexl;
976     }
977 
978     /**
979      * Swaps the current thread local context.
980      * @param tls the context or null
981      * @return the previous thread local context
982      */
983     protected JexlContext.ThreadLocal putThreadLocal(final JexlContext.ThreadLocal tls) {
984         final JexlContext.ThreadLocal local = CONTEXT.get();
985         CONTEXT.set(tls);
986         return local;
987     }
988 
989     @Override
990     public void setClassLoader(final ClassLoader loader) {
991         uberspect.setClassLoader(loader);
992         if (functions != null) {
993             final Iterable<String> names = new ArrayList<>(functions.keySet());
994             for(final String name : names) {
995                 final Object functor = functions.get(name);
996                 if (functor instanceof Class<?>) {
997                     final Class<?> fclass = (Class<?>) functor;
998                     try {
999                         final Class<?> nclass = loader.loadClass(fclass.getName());
1000                         if (nclass != fclass) {
1001                             functions.put(name, nclass);
1002                         }
1003                     } catch (final ClassNotFoundException xany) {
1004                          functions.put(name, fclass.getName());
1005                     }
1006                 }
1007             }
1008         }
1009         metaCache.clearCaches();
1010     }
1011 
1012     @Override
1013     public void setProperty(final JexlContext context, final Object bean, final String expr, final Object value) {
1014         // synthesize expr using register
1015         String src = trimSource(expr);
1016         src = "#0" + (src.charAt(0) == '[' ? "" : ".") + src + "=#1";
1017         try {
1018             final Scope scope = new Scope(null, "#0", "#1");
1019             final ASTJexlScript script = parse(createInfo(), PROPERTY_FEATURES, src, scope);
1020             final JexlNode node = script.jjtGetChild(0);
1021             final Frame frame = script.createFrame(bean, value);
1022             final Interpreter interpreter = createInterpreter(context != null ? context : EMPTY_CONTEXT, frame, options);
1023             interpreter.visitLexicalNode(node, null);
1024         } catch (final JexlException xjexl) {
1025             if (silent) {
1026                 if (logger.isWarnEnabled()) {
1027                     logger.warn(xjexl.getMessage(), xjexl.getCause());
1028                 }
1029                 return;
1030             }
1031             throw xjexl.clean();
1032         }
1033     }
1034 
1035     @Override
1036     public void setProperty(final Object bean, final String expr, final Object value) {
1037         setProperty(null, bean, expr, value);
1038     }
1039 
1040     /**
1041      * Trims the source from front and ending spaces.
1042      * @param str expression to clean
1043      * @return trimmed expression ending in a semicolon
1044      */
1045     protected String trimSource(final CharSequence str) {
1046         if (str != null) {
1047             int start = 0;
1048             int end = str.length();
1049             if (end > 0) {
1050                 // trim front spaces
1051                 while (start < end && Character.isSpaceChar(str.charAt(start))) {
1052                     ++start;
1053                 }
1054                 // trim ending spaces; end is > 0 since start >= 0
1055                 while (end > start && Character.isSpaceChar(str.charAt(end - 1))) {
1056                     --end;
1057                 }
1058                 return str.subSequence(start, end).toString();
1059             }
1060             return "";
1061         }
1062         return null;
1063     }
1064 
1065     /**
1066      * Creates a collector instance.
1067      * @return a collector instance
1068      */
1069     protected VarCollector varCollector() {
1070         return new VarCollector(this.collectMode);
1071     }
1072 
1073     /**
1074      * Utility to deal with single value or set of values.
1075      * @param value the value or the set
1076      * @param consumer the consumer of values
1077      */
1078     private void withValueSet(final Object value, final Consumer<Object> consumer) {
1079         final Set<?> values = value instanceof Set<?>
1080                 ? (Set<?>) value
1081                 : Collections.singleton(value);
1082         for (final Object o : values) {
1083             consumer.accept(o);
1084         }
1085     }
1086 
1087     /**
1088      * Creates a new cache instance.
1089      * <p>This uses the metacache instance as factory.</p>
1090      * @param capacity the cache capacity
1091      * @return a cache instance, null if capacity == 0, the JEXL cache if capacity &lt; 0
1092      */
1093     protected JexlCache<Source, Object> createCache(final int capacity) {
1094         return capacity < 0 ? cache : capacity > 0 ? metaCache.createCache(capacity) : null;
1095     }
1096 
1097 }