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  
18  package org.apache.commons.jexl3;
19  
20  import java.io.BufferedReader;
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStreamReader;
24  import java.math.MathContext;
25  import java.net.URL;
26  import java.nio.charset.Charset;
27  import java.nio.file.Files;
28  import java.util.Objects;
29  
30  import org.apache.commons.jexl3.introspection.JexlUberspect;
31  
32  /**
33   * Creates and evaluates JexlExpression and JexlScript objects.
34   * Determines the behavior of expressions and scripts during their evaluation with respect to:
35   * <ul>
36   * <li>Introspection, see {@link JexlUberspect}</li>
37   * <li>Arithmetic and comparison, see {@link JexlArithmetic}</li>
38   * <li>Error reporting</li>
39   * <li>Logging</li>
40   * </ul>
41   *
42   * <p>Note that methods that evaluate expressions may throw <em>unchecked</em> exceptions;
43   * The {@link JexlException} are thrown in "non-silent" mode but since these are
44   * RuntimeException, user-code <em>should</em> catch them wherever most appropriate.</p>
45   *
46   * @since 2.0
47   */
48  public abstract class JexlEngine {
49  
50      /** Default constructor */
51      public JexlEngine() {} // Keep Javadoc happy
52  
53      /**
54       * The empty context class, public for instrospection.
55       */
56      public static final class EmptyContext implements JexlContext {
57  
58          /**
59           * Default ctor.
60           */
61          EmptyContext() {}
62  
63          @Override
64          public Object get(final String name) {
65              return null;
66          }
67  
68          @Override
69          public boolean has(final String name) {
70              return false;
71          }
72  
73          @Override
74          public void set(final String name, final Object value) {
75              throw new UnsupportedOperationException("Not supported in void context.");
76          }
77      }
78  
79      /**
80       * The  empty/static/non-mutable JexlNamespace class, public for instrospection.
81       */
82      public static final class EmptyNamespaceResolver implements JexlContext.NamespaceResolver {
83  
84          /**
85           * Default ctor.
86           */
87          EmptyNamespaceResolver() {}
88  
89          @Override
90          public Object resolveNamespace(final String name) {
91              return null;
92          }
93      }
94  
95      /** The failure marker class. */
96      private static final class FailObject {
97  
98          /**
99           * Default ctor.
100          */
101         FailObject() {}
102 
103         @Override
104         public String toString() {
105             return "tryExecute failed";
106         }
107     }
108 
109     /**
110      * Script evaluation options.
111      * <p>The JexlContext used for evaluation can implement this interface to alter behavior.</p>
112      *
113      * @deprecated 3.2
114      */
115     @Deprecated
116     public interface Options {
117 
118         /**
119          * The MathContext instance used for +,-,/,*,% operations on big decimals.
120          *
121          * @return the math context
122          */
123         MathContext getArithmeticMathContext();
124 
125         /**
126          * The BigDecimal scale used for comparison and coercion operations.
127          *
128          * @return the scale
129          */
130         int getArithmeticMathScale();
131 
132         /**
133          * The charset used for parsing.
134          *
135          * @return the charset
136          */
137         Charset getCharset();
138 
139         /**
140          * Tests whether evaluation will throw JexlException.Cancel (true) or return null (false) when interrupted.
141          *
142          * @return true when cancellable, false otherwise
143          * @since 3.1
144          */
145         Boolean isCancellable();
146 
147         /**
148          * Tests whether the engine will throw a {@link JexlException} when an error is encountered during evaluation.
149          *
150          * @return true if silent, false otherwise
151          */
152         Boolean isSilent();
153 
154         /**
155          * Tests whether the engine considers unknown variables, methods, functions and constructors as errors or
156          * evaluates them as null.
157          *
158          * @return true if strict, false otherwise
159          */
160         Boolean isStrict();
161 
162         /**
163          * Tests whether the arithmetic triggers errors during evaluation when null is used as an operand.
164          *
165          * @return true if strict, false otherwise
166          */
167         Boolean isStrictArithmetic();
168     }
169 
170     /** A marker singleton for invocation failures in tryInvoke. */
171     public static final Object TRY_FAILED = new FailObject();
172 
173     /**
174      * The thread local context.
175      */
176     protected static final ThreadLocal<JexlContext.ThreadLocal> CONTEXT =
177                        new ThreadLocal<>();
178 
179     /**
180      * The thread local engine.
181      */
182     protected static final ThreadLocal<JexlEngine> ENGINE =
183                        new ThreadLocal<>();
184 
185     /** Default features. */
186     public static final JexlFeatures DEFAULT_FEATURES = new JexlFeatures();
187 
188     /**
189      * An empty/static/non-mutable JexlContext singleton used instead of null context.
190      */
191     public static final JexlContext EMPTY_CONTEXT = new EmptyContext();
192 
193     /**
194      * An empty/static/non-mutable JexlNamespace singleton used instead of null namespace.
195      */
196     public static final JexlContext.NamespaceResolver EMPTY_NS = new EmptyNamespaceResolver();
197 
198     /** The default Jxlt cache size. */
199     private static final int JXLT_CACHE_SIZE = 256;
200 
201     /**
202      * Accesses the current thread local context.
203      *
204      * @return the context or null
205      */
206     public static JexlContext.ThreadLocal getThreadContext() {
207         return CONTEXT.get();
208     }
209 
210     /**
211      * Accesses the current thread local engine.
212      * <p>Advanced: you should only use this to retrieve the engine within a method/ctor called through the evaluation
213      * of a script/expression.</p>
214      *
215      * @return the engine or null
216      */
217     public static JexlEngine getThreadEngine() {
218         return ENGINE.get();
219     }
220 
221     /**
222      * Sets the current thread local context.
223      * <p>This should only be used carefully, for instance when re-evaluating a "stored" script that requires a
224      * given Namespace resolver. Remember to synchronize access if context is shared between threads.
225      *
226      * @param tls the thread local context to set
227      */
228     public static void setThreadContext(final JexlContext.ThreadLocal tls) {
229         CONTEXT.set(tls);
230     }
231 
232     /**
233      * Creates a string from a reader.
234      *
235      * @param reader to be read.
236      * @return the contents of the reader as a String.
237      * @throws IOException on any error reading the reader.
238      */
239     protected static String toString(final BufferedReader reader) throws IOException {
240         final StringBuilder buffer = new StringBuilder();
241         String line;
242         while ((line = reader.readLine()) != null) {
243             buffer.append(line).append('\n');
244         }
245         return buffer.toString();
246     }
247 
248     /**
249      * Clears the expression cache.
250      */
251     public abstract void clearCache();
252 
253     /**
254      * Creates an JexlExpression from a String containing valid JEXL syntax.
255      * This method parses the expression which must contain either a reference or an expression.
256      *
257      * @param info       An info structure to carry debugging information if needed
258      * @param expression A String containing valid JEXL syntax
259      * @return An {@link JexlExpression} which can be evaluated using a {@link JexlContext}
260      * @throws JexlException if there is a problem parsing the script
261      */
262     public abstract JexlExpression createExpression(JexlInfo info, String expression);
263 
264     /**
265      * Creates a JexlExpression from a String containing valid JEXL syntax.
266      * This method parses the expression which must contain either a reference or an expression.
267      *
268      * @param expression A String containing valid JEXL syntax
269      * @return An {@link JexlExpression} which can be evaluated using a {@link JexlContext}
270      * @throws JexlException if there is a problem parsing the script
271      */
272     public final JexlExpression createExpression(final String expression) {
273         return createExpression(null, expression);
274     }
275 
276     /**
277      * Create an information structure for dynamic set/get/invoke/new.
278      * <p>This gathers the class, method and line number of the first calling method
279      * outside of o.a.c.jexl3.</p>
280      *
281      * @return a JexlInfo instance
282      */
283     public JexlInfo createInfo() {
284         return isDebug()? new JexlInfo() : new JexlInfo("jexl", 1, 1);
285     }
286 
287     /**
288      * Creates a JexlInfo instance.
289      *
290      * @param fn url/file/template/script user given name
291      * @param l  line number
292      * @param c  column number
293      * @return a JexlInfo instance
294      */
295     public JexlInfo createInfo(final String fn, final int l, final int c) {
296         return new JexlInfo(fn, l, c);
297     }
298 
299     /**
300      * Creates a new {@link JxltEngine} instance using this engine.
301      *
302      * @return a JEXL Template engine
303      */
304     public JxltEngine createJxltEngine() {
305         return createJxltEngine(true);
306     }
307 
308     /**
309      * Creates a new {@link JxltEngine} instance using this engine.
310      *
311      * @param noScript  whether the JxltEngine only allows Jexl expressions or scripts
312      * @return a JEXL Template engine
313      */
314     public JxltEngine createJxltEngine(final boolean noScript) {
315         return createJxltEngine(noScript, JXLT_CACHE_SIZE, '$', '#');
316     }
317 
318     /**
319      * Creates a new instance of {@link JxltEngine} using this engine.
320      *
321      * @param noScript  whether the JxltEngine only allows JEXL expressions or scripts
322      * @param cacheSize the number of expressions in this cache, default is 256
323      * @param immediate the immediate template expression character, default is '$'
324      * @param deferred  the deferred template expression character, default is '#'
325      * @return a JEXL Template engine
326      */
327     public abstract JxltEngine createJxltEngine(boolean noScript, int cacheSize, char immediate, char deferred);
328 
329     /**
330      * Creates a Script from a {@link File} containing valid JEXL syntax.
331      * This method parses the script and validates the syntax.
332      *
333      * @param scriptFile A {@link File} containing valid JEXL syntax. Must not be null. Must be a readable file.
334      * @return A {@link JexlScript} which can be executed with a {@link JexlContext}.
335      * @throws JexlException if there is a problem reading or parsing the script.
336      */
337     public final JexlScript createScript(final File scriptFile) {
338         return createScript(null, null, readSource(scriptFile), (String[]) null);
339     }
340 
341     /**
342      * Creates a Script from a {@link File} containing valid JEXL syntax.
343      * This method parses the script and validates the syntax.
344      *
345      * @param scriptFile A {@link File} containing valid JEXL syntax. Must not be null. Must be a readable file.
346      * @param names      The script parameter names used during parsing; a corresponding array of arguments containing
347      * values should be used during evaluation.
348      * @return A {@link JexlScript} which can be executed with a {@link JexlContext}.
349      * @throws JexlException if there is a problem reading or parsing the script.
350      */
351     public final JexlScript createScript(final File scriptFile, final String... names) {
352         return createScript(null, null, readSource(scriptFile), names);
353     }
354 
355     /**
356      * Creates a JexlScript from a String containing valid JEXL syntax.
357      * This method parses the script and validates the syntax.
358      *
359      * @param features A set of features that will be enforced during parsing
360      * @param info   An info structure to carry debugging information if needed
361      * @param source A string containing valid JEXL syntax
362      * @param names  The script parameter names used during parsing; a corresponding array of arguments containing
363      * values should be used during evaluation
364      * @return A {@link JexlScript} which can be executed using a {@link JexlContext}
365      * @throws JexlException if there is a problem parsing the script
366      */
367     public abstract JexlScript createScript(JexlFeatures features, JexlInfo info, String source, String... names);
368 
369     /**
370      * Creates a Script from a {@link File} containing valid JEXL syntax.
371      * This method parses the script and validates the syntax.
372      *
373      * @param info       An info structure to carry debugging information if needed
374      * @param scriptFile A {@link File} containing valid JEXL syntax. Must not be null. Must be a readable file.
375      * @param names      The script parameter names used during parsing; a corresponding array of arguments containing
376      * values should be used during evaluation.
377      * @return A {@link JexlScript} which can be executed with a {@link JexlContext}.
378      * @throws JexlException if there is a problem reading or parsing the script.
379      */
380     public final JexlScript createScript(final JexlInfo info, final File scriptFile, final String... names) {
381         return createScript(null, info, readSource(scriptFile), names);
382     }
383 
384     /**
385      * Creates a JexlScript from a String containing valid JEXL syntax.
386      * This method parses the script and validates the syntax.
387      *
388      * @param info   An info structure to carry debugging information if needed
389      * @param source A string containing valid JEXL syntax
390      * @param names  The script parameter names used during parsing; a corresponding array of arguments containing
391      * values should be used during evaluation
392      * @return A {@link JexlScript} which can be executed using a {@link JexlContext}
393      * @throws JexlException if there is a problem parsing the script
394      */
395     public final JexlScript createScript(final JexlInfo info, final String source, final String... names) {
396         return createScript(null, info, source, names);
397     }
398 
399     /**
400      * Creates a Script from a {@link URL} containing valid JEXL syntax.
401      * This method parses the script and validates the syntax.
402      *
403      * @param info      An info structure to carry debugging information if needed
404      * @param scriptUrl A {@link URL} containing valid JEXL syntax. Must not be null.
405      * @param names     The script parameter names used during parsing; a corresponding array of arguments containing
406      * values should be used during evaluation.
407      * @return A {@link JexlScript} which can be executed with a {@link JexlContext}.
408      * @throws JexlException if there is a problem reading or parsing the script.
409      */
410     public final JexlScript createScript(final JexlInfo info, final URL scriptUrl, final String... names) {
411         return createScript(null, info, readSource(scriptUrl), names);
412     }
413 
414     /**
415      * Creates a Script from a String containing valid JEXL syntax.
416      * This method parses the script and validates the syntax.
417      *
418      * @param scriptText A String containing valid JEXL syntax
419      * @return A {@link JexlScript} which can be executed using a {@link JexlContext}
420      * @throws JexlException if there is a problem parsing the script.
421      */
422     public final JexlScript createScript(final String scriptText) {
423         return createScript(null, null, scriptText, (String[]) null);
424     }
425 
426     /**
427      * Creates a Script from a String containing valid JEXL syntax.
428      * This method parses the script and validates the syntax.
429      *
430      * @param source A String containing valid JEXL syntax
431      * @param names      The script parameter names used during parsing; a corresponding array of arguments containing
432      * values should be used during evaluation
433      * @return A {@link JexlScript} which can be executed using a {@link JexlContext}
434      * @throws JexlException if there is a problem parsing the script
435      */
436     public final JexlScript createScript(final String source, final String... names) {
437         return createScript(null, null, source, names);
438     }
439 
440     /**
441      * Creates a Script from a {@link URL} containing valid JEXL syntax.
442      * This method parses the script and validates the syntax.
443      *
444      * @param scriptUrl A {@link URL} containing valid JEXL syntax. Must not be null.
445      * @return A {@link JexlScript} which can be executed with a {@link JexlContext}.
446      * @throws JexlException if there is a problem reading or parsing the script.
447      */
448     public final JexlScript createScript(final URL scriptUrl) {
449         return createScript(null, readSource(scriptUrl), (String[]) null);
450     }
451 
452     /**
453      * Creates a Script from a {@link URL} containing valid JEXL syntax.
454      * This method parses the script and validates the syntax.
455      *
456      * @param scriptUrl A {@link URL} containing valid JEXL syntax. Must not be null.
457      * @param names     The script parameter names used during parsing; a corresponding array of arguments containing
458      * values should be used during evaluation.
459      * @return A {@link JexlScript} which can be executed with a {@link JexlContext}.
460      * @throws JexlException if there is a problem reading or parsing the script.
461      */
462     public final JexlScript createScript(final URL scriptUrl, final String... names) {
463         return createScript(null, null, readSource(scriptUrl), names);
464     }
465 
466     /**
467      * Gets this engine underlying {@link JexlArithmetic}.
468      *
469      * @return the arithmetic
470      */
471     public abstract JexlArithmetic getArithmetic();
472 
473     /**
474      * Gets the charset used for parsing.
475      *
476      * @return the charset
477      */
478     public abstract Charset getCharset();
479 
480     /**
481      * Accesses properties of a bean using an expression.
482      * <p>
483      * If the JEXL engine is silent, errors will be logged through its logger as warning.
484      * </p>
485      *
486      * @param context the evaluation context
487      * @param bean    the bean to get properties from
488      * @param expr    the property expression
489      * @return the value of the property
490      * @throws JexlException if there is an error parsing the expression or during evaluation
491      */
492     public abstract Object getProperty(JexlContext context, Object bean, String expr);
493 
494     /**
495      * Accesses properties of a bean using an expression.
496      * <p>
497      * jexl.get(myobject, "foo.bar"); should equate to
498      * myobject.getFoo().getBar(); (or myobject.getFoo().get("bar"))
499      * </p>
500      * <p>
501      * If the JEXL engine is silent, errors will be logged through its logger as warning.
502      * </p>
503      *
504      * @param bean the bean to get properties from
505      * @param expr the property expression
506      * @return the value of the property
507      * @throws JexlException if there is an error parsing the expression or during evaluation
508      */
509     public abstract Object getProperty(Object bean, String expr);
510 
511     /**
512      * Gets this engine underlying {@link JexlUberspect}.
513      *
514      * @return the uberspect
515      */
516     public abstract JexlUberspect getUberspect();
517 
518     /**
519      * Invokes an object's method by name and arguments.
520      *
521      * @param obj  the method's invoker object
522      * @param meth the method's name
523      * @param args the method's arguments
524      * @return the method returned value or null if it failed and engine is silent
525      * @throws JexlException if method could not be found or failed and engine is not silent
526      */
527     public abstract Object invokeMethod(Object obj, String meth, Object... args);
528 
529     /**
530      * Checks whether this engine will throw JexlException.Cancel (true) or return null (false) when interrupted
531      * during an execution.
532      *
533      * @return true if cancellable, false otherwise
534      */
535     public abstract boolean isCancellable();
536 
537     /**
538      * Checks whether this engine is in debug mode.
539      *
540      * <p>If set to true which is the default, when calling {@link JexlEngine#newInstance}, {@link JexlEngine#invokeMethod},
541      * {@link JexlEngine#setProperty}, {@link JexlEngine#getProperty} or if no {@link JexlInfo} instance is provided when
542      * creating a script and an error occurs during execution, the error message will contain the stack-trace (class/method/line)
543      * location of the caller for the methods, of the creator for the script; this may not be desirable in rare environments where
544      * vulnerability assessments are strict.</p>
545      *
546      * @see JexlBuilder#debug(boolean)
547      *
548      * @return true if debug is on, false otherwise
549      */
550     public abstract boolean isDebug();
551 
552     /**
553      * Checks whether this engine throws JexlException during evaluation.
554      *
555      * @return true if silent, false (default) otherwise
556      */
557     public abstract boolean isSilent();
558 
559     /**
560      * Checks whether this engine considers unknown variables, methods, functions and constructors as errors.
561      *
562      * @return true if strict, false otherwise
563      */
564     public abstract boolean isStrict();
565 
566     /**
567      * Creates a new instance of an object using the most appropriate constructor based on the arguments.
568      *
569      * @param <T>   the type of object
570      * @param clazz the class to instantiate
571      * @param args  the constructor arguments
572      * @return the created object instance or null on failure when silent
573      */
574     public abstract <T> T newInstance(Class<? extends T> clazz, Object... args);
575 
576     /**
577      * Creates a new instance of an object using the most appropriate constructor based on the arguments.
578      *
579      * @param clazz the name of the class to instantiate resolved through this engine's class loader
580      * @param args  the constructor arguments
581      * @return the created object instance or null on failure when silent
582      */
583     public abstract Object newInstance(String clazz, Object... args);
584 
585     /**
586      * Reads a JEXL source from a File.
587      *
588      * @param file the script file
589      * @return the source
590      */
591     protected String readSource(final File file) {
592         Objects.requireNonNull(file, "file");
593         try (BufferedReader reader = Files.newBufferedReader(file.toPath(), getCharset())) {
594             return toString(reader);
595         } catch (final IOException xio) {
596             throw new JexlException(createInfo(file.toString(), 1, 1), "could not read source File", xio);
597         }
598     }
599 
600     /**
601      * Reads a JEXL source from an URL.
602      *
603      * @param url the script url
604      * @return the source
605      */
606     protected String readSource(final URL url) {
607         Objects.requireNonNull(url, "url");
608         try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), getCharset()))) {
609             return toString(reader);
610         } catch (final IOException xio) {
611             throw new JexlException(createInfo(url.toString(), 1, 1), "could not read source URL", xio);
612         }
613     }
614 
615     /**
616      * Sets the class loader used to discover classes in 'new' expressions.
617      * <p>This method is <em>not</em> thread safe; it may be called after JexlEngine
618      * initialization and allow scripts to use new classes definitions.</p>
619      *
620      * @param loader the class loader to use
621      */
622     public abstract void setClassLoader(ClassLoader loader);
623 
624     /**
625      * Assign properties of a bean using an expression.
626      * <p>
627      * If the JEXL engine is silent, errors will be logged through
628      * its logger as warning.
629      * </p>
630      *
631      * @param context the evaluation context
632      * @param bean    the bean to set properties in
633      * @param expr    the property expression
634      * @param value   the value of the property
635      * @throws JexlException if there is an error parsing the expression or during evaluation
636      */
637     public abstract void setProperty(JexlContext context, Object bean, String expr, Object value);
638 
639     /**
640      * Assign properties of a bean using an expression.
641      * <p>
642      * jexl.set(myobject, "foo.bar", 10); should equate to
643      * myobject.getFoo().setBar(10); (or myobject.getFoo().put("bar", 10) )
644      * </p>
645      * <p>
646      * If the JEXL engine is silent, errors will be logged through its logger as warning.
647      * </p>
648      *
649      * @param bean  the bean to set properties in
650      * @param expr  the property expression
651      * @param value the value of the property
652      * @throws JexlException if there is an error parsing the expression or during evaluation
653      */
654     public abstract void setProperty(Object bean, String expr, Object value);
655 }