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