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