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