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