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