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