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