001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.jexl3;
019
020import org.apache.commons.jexl3.introspection.JexlUberspect;
021
022import java.io.BufferedReader;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.IOException;
026import java.io.InputStreamReader;
027import java.math.MathContext;
028import java.net.URL;
029import java.nio.charset.Charset;
030
031/**
032 * Creates and evaluates JexlExpression and JexlScript objects.
033 * Determines the behavior of expressions and scripts during their evaluation with respect to:
034 * <ul>
035 * <li>Introspection, see {@link JexlUberspect}</li>
036 * <li>Arithmetic and comparison, see {@link JexlArithmetic}</li>
037 * <li>Error reporting</li>
038 * <li>Logging</li>
039 * </ul>
040 *
041 * <p>Note that methods that evaluate expressions may throw <em>unchecked</em> exceptions;
042 * The {@link JexlException} are thrown in "non-silent" mode but since these are
043 * RuntimeException, user-code <em>should</em> catch them wherever most appropriate.</p>
044 *
045 * @since 2.0
046 */
047public abstract class JexlEngine {
048
049    /** A marker singleton for invocation failures in tryInvoke. */
050    public static final Object TRY_FAILED = new FailObject();
051
052    /** The failure marker class. */
053    private static final class FailObject {
054        /**
055         * Default ctor.
056         */
057        FailObject() {}
058
059        @Override
060        public String toString() {
061            return "tryExecute failed";
062        }
063    }
064
065    /**
066     * The thread local context.
067     */
068    protected static final java.lang.ThreadLocal<JexlContext.ThreadLocal> CONTEXT =
069                       new java.lang.ThreadLocal<>();
070
071    /**
072     * Accesses the current thread local context.
073     *
074     * @return the context or null
075     */
076    public static JexlContext.ThreadLocal getThreadContext() {
077        return CONTEXT.get();
078    }
079
080    /**
081     * The thread local engine.
082     */
083    protected static final java.lang.ThreadLocal<JexlEngine> ENGINE =
084                       new java.lang.ThreadLocal<>();
085
086    /**
087     * Accesses the current thread local engine.
088     * <p>Advanced: you should only use this to retrieve the engine within a method/ctor called through the evaluation
089     * of a script/expression.</p>
090     * @return the engine or null
091     */
092    public static JexlEngine getThreadEngine() {
093        return ENGINE.get();
094    }
095
096    /**
097     * Sets the current thread local context.
098     * <p>This should only be used carefully, for instance when re-evaluating a "stored" script that requires a
099     * 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}