    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        private 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        private 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        private 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}