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         final ClassLoader loader = conf.loader();
341         if (loader != null) {
342             uber.setClassLoader(loader);
343         }
344         final JexlSandbox sandbox = conf.sandbox();
345         if (sandbox == null) {
346             this.uberspect = uber;
347         } else {
348             this.uberspect = new SandboxUberspect(uber, sandbox);
349         }
350         this.logger = conf.logger() == null ? LogFactory.getLog(JexlEngine.class) : conf.logger();
351         this.arithmetic = conf.arithmetic() == null ? new JexlArithmetic(this.strict) : conf.arithmetic();
352         options.setMathContext(arithmetic.getMathContext());
353         options.setMathScale(arithmetic.getMathScale());
354         options.setStrictArithmetic(arithmetic.isStrict());
355         final Map<String, Object> ns = conf.namespaces();
356         this.functions = ns == null || ns.isEmpty()? Collections.emptyMap() : ns; // should we make a copy?
357         this.classNameSolver = new FqcnResolver(uberspect, conf.imports());
358         // parsing & features:
359         final JexlFeatures features = conf.features() == null ? DEFAULT_FEATURES : conf.features();
360         Predicate<String> nsTest = features.namespaceTest();
361         final Set<String> nsNames = functions.keySet();
362         if (!nsNames.isEmpty()) {
363             nsTest = nsTest == JexlFeatures.TEST_STR_FALSE ?nsNames::contains : nsTest.or(nsNames::contains);
364         }
365         this.expressionFeatures = new JexlFeatures(features).script(false).namespaceTest(nsTest);
366         this.scriptFeatures = new JexlFeatures(features).script(true).namespaceTest(nsTest);
367         this.charset = conf.charset();
368         // caching:
369         final IntFunction<JexlCache<?, ?>> factory = conf.cacheFactory();
370         this.metaCache = new MetaCache(factory == null ? SoftCache::new : factory);
371         this.cache = metaCache.createCache(conf.cache());
372         this.cacheThreshold = conf.cacheThreshold();
373         if (uberspect == null) {
374             throw new IllegalArgumentException("uberspect cannot be null");
375         }
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 = debug ? createInfo() : null;
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      */
554     protected String[] getLocalVariables(final JexlScript script) {
555         return script.getLocalVariables();
556     }
557 
558     /**
559      * Solves a namespace using this engine map of functions.
560      * @param name the namespoce name
561      * @return the object associated
562      */
563     final Object getNamespace(final String name) {
564         return functions.get(name);
565     }
566 
567     /**
568      * Gets the array of parameters from a script.
569      * @param script the script
570      * @return the parameters which may be empty (but not null) if no parameters were defined
571      * @since 3.0
572      */
573     protected String[] getParameters(final JexlScript script) {
574         return script.getParameters();
575     }
576 
577     @Override
578     public Object getProperty(final JexlContext context, final Object bean, final String expr) {
579         // synthesize expr using register
580         String src = trimSource(expr);
581         src = "#0" + (src.charAt(0) == '[' ? "" : ".") + src;
582         try {
583             final Scope scope = new Scope(null, "#0");
584             final ASTJexlScript script = parse(null, PROPERTY_FEATURES, src, scope);
585             final JexlNode node = script.jjtGetChild(0);
586             final Frame frame = script.createFrame(bean);
587             final Interpreter interpreter = createInterpreter(context == null ? EMPTY_CONTEXT : context, frame, options);
588             return interpreter.visitLexicalNode(node, null);
589         } catch (final JexlException xjexl) {
590             if (silent) {
591                 if (logger.isWarnEnabled()) {
592                     logger.warn(xjexl.getMessage(), xjexl.getCause());
593                 }
594                 return null;
595             }
596             throw xjexl.clean();
597         }
598     }
599 
600     @Override
601     public Object getProperty(final Object bean, final String expr) {
602         return getProperty(null, bean, expr);
603     }
604 
605     @Override
606     public JexlUberspect getUberspect() {
607         return uberspect;
608     }
609 
610     /**
611      * Gets the list of variables accessed by a script.
612      * <p>This method will visit all nodes of a script and extract all variables whether they
613      * are written in 'dot' or 'bracketed' notation. (a.b is equivalent to a['b']).</p>
614      * @param script the script
615      * @return the set of variables, each as a list of strings (ant-ish variables use more than 1 string)
616      *         or the empty set if no variables are used
617      */
618     protected Set<List<String>> getVariables(final ASTJexlScript script) {
619         final VarCollector collector = varCollector();
620         getVariables(script, script, collector);
621         return collector.collected();
622     }
623 
624     /**
625      * Fills up the list of variables accessed by a node.
626      * @param script the owning script
627      * @param node the node
628      * @param collector the variable collector
629      */
630     protected void getVariables(final ASTJexlScript script, final JexlNode node, final VarCollector collector) {
631         if (node instanceof ASTIdentifier) {
632             final JexlNode parent = node.jjtGetParent();
633             if (parent instanceof ASTMethodNode || parent instanceof ASTFunctionNode) {
634                 // skip identifiers for methods and functions
635                 collector.collect(null);
636                 return;
637             }
638             final ASTIdentifier identifier = (ASTIdentifier) node;
639             final int symbol = identifier.getSymbol();
640             // symbols that are captured are considered "global" variables
641             if (symbol >= 0 && script != null && !script.isCapturedSymbol(symbol)) {
642                 collector.collect(null);
643             } else {
644                 // start collecting from identifier
645                 collector.collect(identifier);
646                 collector.add(identifier.getName());
647             }
648         } else if (node instanceof ASTIdentifierAccess) {
649             final JexlNode parent = node.jjtGetParent();
650             if (parent instanceof ASTMethodNode || parent instanceof ASTFunctionNode) {
651                 // skip identifiers for methods and functions
652                 collector.collect(null);
653                 return;
654             }
655             // belt and suspender since an identifier should have been seen first
656             if (collector.isCollecting()) {
657                 collector.add(((ASTIdentifierAccess) node).getName());
658             }
659         } else if (node instanceof ASTArrayAccess && collector.mode > 0) {
660             final int num = node.jjtGetNumChildren();
661             // collect only if array access is const and follows an identifier
662             boolean collecting = collector.isCollecting();
663             for (int i = 0; i < num; ++i) {
664                 final JexlNode child = node.jjtGetChild(i);
665                 if (collecting && child.isConstant()) {
666                     // collect all constants or only string and number literals
667                     final boolean collect = collector.mode > 1
668                             || child instanceof ASTStringLiteral || child instanceof ASTNumberLiteral;
669                     if (collect) {
670                         final String image = child.toString();
671                         collector.add(image);
672                     }
673                 } else {
674                     collecting = false;
675                     collector.collect(null);
676                     getVariables(script, child, collector);
677                     collector.collect(null);
678                 }
679             }
680         } else {
681             final int num = node.jjtGetNumChildren();
682             for (int i = 0; i < num; ++i) {
683                 getVariables(script, node.jjtGetChild(i), collector);
684             }
685             collector.collect(null);
686         }
687     }
688 
689     @Override
690     public Object invokeMethod(final Object obj, final String meth, final Object... args) {
691         JexlException xjexl = null;
692         Object result = null;
693         final JexlInfo info = debug ? createInfo() : null;
694         try {
695             JexlMethod method = uberspect.getMethod(obj, meth, args);
696             if (method == null && arithmetic.narrowArguments(args)) {
697                 method = uberspect.getMethod(obj, meth, args);
698             }
699             if (method != null) {
700                 result = method.invoke(obj, args);
701             } else {
702                 xjexl = new JexlException.Method(info, meth, args);
703             }
704         } catch (final JexlException xany) {
705             xjexl = xany;
706         } catch (final Exception xany) {
707             xjexl = new JexlException.Method(info, meth, args, xany);
708         }
709         if (xjexl != null) {
710             if (!silent) {
711                 throw xjexl.clean();
712             }
713             if (logger.isWarnEnabled()) {
714                 logger.warn(xjexl.getMessage(), xjexl.getCause());
715             }
716         }
717         return result;
718     }
719 
720     @Override
721     public boolean isCancellable() {
722         return this.cancellable;
723     }
724 
725     @Override
726     public boolean isDebug() {
727         return this.debug;
728     }
729 
730     @Override
731     public boolean isSilent() {
732         return this.silent;
733     }
734 
735     @Override
736     public boolean isStrict() {
737         return this.strict;
738     }
739 
740     @Override
741     public <T> T newInstance(final Class<? extends T> clazz, final Object... args) {
742         return clazz.cast(doCreateInstance(clazz, args));
743     }
744 
745     @Override
746     public Object newInstance(final String clazz, final Object... args) {
747         return doCreateInstance(clazz, args);
748     }
749 
750     @Override
751     public JexlUberspect.ClassConstantResolver createConstantResolver(final Collection<String> imports) {
752         return imports == null || imports.isEmpty()
753                 ? classNameSolver
754                 : new FqcnResolver(classNameSolver).importPackages(imports);
755     }
756 
757     /**
758      * Sets options from this engine options.
759      * @param opts the options to set
760      * @return the options
761      */
762     public JexlOptions optionsSet(final JexlOptions opts) {
763         if (opts != null) {
764             opts.set(options);
765         }
766         return opts;
767     }
768 
769     /**
770      * Parses an expression.
771      *
772      * @param info      information structure
773      * @param parsingf  the set of parsing features
774      * @param src      the expression to parse
775      * @param scope     the script frame
776      * @return the parsed tree
777      * @throws JexlException if any error occurred during parsing
778      */
779     protected ASTJexlScript parse(final JexlInfo info, final JexlFeatures parsingf, final String src, final Scope scope) {
780         final boolean cached = src.length() < cacheThreshold && cache != null;
781         final JexlFeatures features = parsingf != null ? parsingf : DEFAULT_FEATURES;
782         final Source source = cached ? new Source(features, Scope.getSymbolsMap(scope), src) : null;
783         if (source != null) {
784             final Object c = cache.get(source);
785             if (c instanceof ASTJexlScript) {
786                 return (ASTJexlScript) c;
787             }
788         }
789         final JexlInfo ninfo = info == null && debug ? createInfo() : info;
790         final JexlEngine se = putThreadEngine(this);
791         ASTJexlScript script;
792         try {
793             // if parser not in use...
794             if (parsing.compareAndSet(false, true)) {
795                 synchronized (parsing) {
796                     try {
797                         // lets parse
798                         script = parser.parse(ninfo, features, src, scope);
799                     } finally {
800                         // no longer in use
801                         parsing.set(false);
802                     }
803                 }
804             } else {
805                 // ...otherwise parser was in use, create a new temporary one
806                 script = parserFactory.get().parse(ninfo, features, src, scope);
807             }
808             if (source != null) {
809                 cache.put(source, script);
810             }
811         } finally {
812             // restore thread local engine
813             putThreadEngine(se);
814         }
815         return script;
816     }
817 
818     /**
819      * Parses a Jexl expression or script.
820      * @param info the JexlInfo
821      * @param expr whether to parse an expression or a script
822      * @param src the source to parse
823      * @param scope the scope, maybe null
824      * @return the parsed tree
825      */
826     protected ASTJexlScript jxltParse(final JexlInfo info, final boolean expr, final String src, final Scope scope) {
827         synchronized(parsing) {
828             return parser.jxltParse(info, expr ? this.expressionFeatures : this.scriptFeatures, src, scope);
829         }
830     }
831 
832     /**
833      * Processes jexl.module.ns pragma.
834      *
835      * <p>If the value is empty, the namespace will be cleared which may be useful to debug and force unload
836      * the object bound to the namespace.</p>
837      * @param ns the namespace map
838      * @param key the key the namespace
839      * @param value the value, ie the expression to evaluate and its result bound to the namespace
840      * @param info the expression info
841      * @param context the value-as-expression evaluation context
842      */
843     private void processPragmaModule(final Map<String, Object> ns, final String key, final Object value, final JexlInfo info,
844             final JexlContext context) {
845         // jexl.module.***
846         final String module = key.substring(PRAGMA_MODULE.length());
847         if (module.isEmpty()) {
848             if (logger.isWarnEnabled()) {
849                 logger.warn(module + ": invalid module declaration");
850             }
851         } else {
852             withValueSet(value, o -> {
853                 if (!(o instanceof CharSequence)) {
854                     if (logger.isWarnEnabled()) {
855                         logger.warn(module + ": unable to define module from " + value);
856                     }
857                 } else {
858                     final String moduleSrc = o.toString();
859                     final Object functor;
860                     if (context instanceof JexlContext.ModuleProcessor) {
861                         final JexlContext.ModuleProcessor processor = (JexlContext.ModuleProcessor) context;
862                         functor = processor.processModule(this, info, module, moduleSrc);
863                     } else {
864                         final Object moduleObject = createExpression(info, moduleSrc).evaluate(context);
865                         functor = moduleObject instanceof Script ? ((Script) moduleObject).execute(context) : moduleObject;
866                     }
867                     if (functor != null) {
868                         ns.put(module, functor);
869                     } else {
870                         ns.remove(module);
871                     }
872                 }
873             });
874         }
875     }
876 
877     /**
878      * Processes jexl.namespace.ns pragma.
879      * @param ns the namespace map
880      * @param key the key
881      * @param value the value, ie the class
882      */
883     private void processPragmaNamespace(final Map<String, Object> ns, final String key, final Object value) {
884         if (value instanceof String) {
885             // jexl.namespace.***
886             final String namespaceName = key.substring(PRAGMA_JEXLNS.length());
887             if (!namespaceName.isEmpty()) {
888                 final String nsclass = value.toString();
889                 final Class<?> clazz = uberspect.getClassByName(nsclass);
890                 if (clazz == null) {
891                     if (logger.isWarnEnabled()) {
892                         logger.warn(key + ": unable to find class " + nsclass);
893                     }
894                 } else {
895                     ns.put(namespaceName, clazz);
896                 }
897             }
898         } else if (logger.isWarnEnabled()) {
899             logger.warn(key + ": ambiguous declaration " + value);
900         }
901     }
902 
903     /**
904      * Processes a script pragmas.
905      * <p>Only called from options(...)
906      * @param script the script
907      * @param context the context
908      * @param opts the options
909      */
910     protected void processPragmas(final ASTJexlScript script, final JexlContext context, final JexlOptions opts) {
911         final Map<String, Object> pragmas = script.getPragmas();
912         if (pragmas != null && !pragmas.isEmpty()) {
913             final JexlContext.PragmaProcessor processor =
914                     context instanceof JexlContext.PragmaProcessor
915                             ? (JexlContext.PragmaProcessor) context
916                             : null;
917             Map<String, Object> ns = null;
918             for (final Map.Entry<String, Object> pragma : pragmas.entrySet()) {
919                 final String key = pragma.getKey();
920                 final Object value = pragma.getValue();
921                 if (PRAGMA_OPTIONS.equals(key)) {
922                     if (value instanceof String) {
923                         // jexl.options
924                         final String[] vs = value.toString().split(" ");
925                         opts.setFlags(vs);
926                     }
927                 }  else if (PRAGMA_IMPORT.equals(key)) {
928                     // jexl.import, may use a set
929                     final Set<String> is = new LinkedHashSet<>();
930                     withValueSet(value, o -> {
931                         if (o instanceof String) {
932                             is.add(o.toString());
933                         }
934                     });
935                     if (!is.isEmpty()) {
936                         opts.setImports(is);
937                     }
938                 } else if (key.startsWith(PRAGMA_JEXLNS)) {
939                     if (ns == null)  {
940                         ns = new LinkedHashMap<>();
941                     }
942                     processPragmaNamespace(ns, key, value);
943                     if (!ns.isEmpty()) {
944                         opts.setNamespaces(ns);
945                     }
946                 } else if (key.startsWith(PRAGMA_MODULE)) {
947                     if (ns == null)  {
948                         ns = new LinkedHashMap<>();
949                     }
950                     processPragmaModule(ns, key, value, script.jexlInfo(), context);
951                     if (!ns.isEmpty()) {
952                         opts.setNamespaces(ns);
953                     }
954                 }
955                 // user-defined processor may alter options
956                 if (processor != null) {
957                     processor.processPragma(opts, key, value);
958                 }
959             }
960         }
961     }
962 
963     /**
964      * Swaps the current thread local engine.
965      * @param jexl the engine or null
966      * @return the previous thread local engine
967      */
968     protected JexlEngine putThreadEngine(final JexlEngine jexl) {
969         final JexlEngine pjexl = ENGINE.get();
970         ENGINE.set(jexl);
971         return pjexl;
972     }
973 
974     /**
975      * Swaps the current thread local context.
976      * @param tls the context or null
977      * @return the previous thread local context
978      */
979     protected JexlContext.ThreadLocal putThreadLocal(final JexlContext.ThreadLocal tls) {
980         final JexlContext.ThreadLocal local = CONTEXT.get();
981         CONTEXT.set(tls);
982         return local;
983     }
984 
985     @Override
986     public void setClassLoader(final ClassLoader loader) {
987         uberspect.setClassLoader(loader);
988         if (functions != null) {
989             final Iterable<String> names = new ArrayList<>(functions.keySet());
990             for(final String name : names) {
991                 final Object functor = functions.get(name);
992                 if (functor instanceof Class<?>) {
993                     final Class<?> fclass = (Class<?>) functor;
994                     try {
995                         final Class<?> nclass = loader.loadClass(fclass.getName());
996                         if (nclass != fclass) {
997                             functions.put(name, nclass);
998                         }
999                     } catch (final ClassNotFoundException xany) {
1000                          functions.put(name, fclass.getName());
1001                     }
1002                 }
1003             }
1004         }
1005         metaCache.clearCaches();
1006     }
1007 
1008     @Override
1009     public void setProperty(final JexlContext context, final Object bean, final String expr, final Object value) {
1010         // synthesize expr using register
1011         String src = trimSource(expr);
1012         src = "#0" + (src.charAt(0) == '[' ? "" : ".") + src + "=#1";
1013         try {
1014             final Scope scope = new Scope(null, "#0", "#1");
1015             final ASTJexlScript script = parse(null, PROPERTY_FEATURES, src, scope);
1016             final JexlNode node = script.jjtGetChild(0);
1017             final Frame frame = script.createFrame(bean, value);
1018             final Interpreter interpreter = createInterpreter(context != null ? context : EMPTY_CONTEXT, frame, options);
1019             interpreter.visitLexicalNode(node, null);
1020         } catch (final JexlException xjexl) {
1021             if (silent) {
1022                 if (logger.isWarnEnabled()) {
1023                     logger.warn(xjexl.getMessage(), xjexl.getCause());
1024                 }
1025                 return;
1026             }
1027             throw xjexl.clean();
1028         }
1029     }
1030 
1031     @Override
1032     public void setProperty(final Object bean, final String expr, final Object value) {
1033         setProperty(null, bean, expr, value);
1034     }
1035 
1036     /**
1037      * Trims the source from front and ending spaces.
1038      * @param str expression to clean
1039      * @return trimmed expression ending in a semicolon
1040      */
1041     protected String trimSource(final CharSequence str) {
1042         if (str != null) {
1043             int start = 0;
1044             int end = str.length();
1045             if (end > 0) {
1046                 // trim front spaces
1047                 while (start < end && Character.isSpaceChar(str.charAt(start))) {
1048                     ++start;
1049                 }
1050                 // trim ending spaces; end is > 0 since start >= 0
1051                 while (end > start && Character.isSpaceChar(str.charAt(end - 1))) {
1052                     --end;
1053                 }
1054                 return str.subSequence(start, end).toString();
1055             }
1056             return "";
1057         }
1058         return null;
1059     }
1060 
1061     /**
1062      * Creates a collector instance.
1063      * @return a collector instance
1064      */
1065     protected VarCollector varCollector() {
1066         return new VarCollector(this.collectMode);
1067     }
1068 
1069     /**
1070      * Utility to deal with single value or set of values.
1071      * @param value the value or the set
1072      * @param consumer the consumer of values
1073      */
1074     private void withValueSet(final Object value, final Consumer<Object> consumer) {
1075         final Set<?> values = value instanceof Set<?>
1076                 ? (Set<?>) value
1077                 : Collections.singleton(value);
1078         for (final Object o : values) {
1079             consumer.accept(o);
1080         }
1081     }
1082 
1083     /**
1084      * Creates a new cache instance.
1085      * <p>This uses the metacache instance as factory.</p>
1086      * @param capacity the cache capacity
1087      * @return a cache instance, null if capacity == 0, the JEXL cache if capacity &lt; 0
1088      */
1089     protected JexlCache<Source, Object> createCache(final int capacity) {
1090         return capacity < 0 ? cache : capacity > 0 ? metaCache.createCache(capacity) : null;
1091     }
1092 
1093 }