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