    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 *      https://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 java.io.BufferedReader;
    021import java.io.File;
    022import java.io.IOException;
    023import java.io.InputStreamReader;
    024import java.math.MathContext;
    025import java.net.URL;
    026import java.nio.charset.Charset;
    027import java.nio.file.Files;
    028import java.util.Objects;
    029
    030import org.apache.commons.jexl3.introspection.JexlUberspect;
    031
    032/**
    033 * Creates and evaluates JexlExpression and JexlScript objects.
    034 * Determines the behavior of expressions and scripts during their evaluation with respect to:
    035 * <ul>
    036 * <li>Introspection, see {@link JexlUberspect}</li>
    037 * <li>Arithmetic and comparison, see {@link JexlArithmetic}</li>
    038 * <li>Error reporting</li>
    039 * <li>Logging</li>
    040 * </ul>
    041 *
    042 * <p>Note that methods that evaluate expressions may throw <em>unchecked</em> exceptions;
    043 * The {@link JexlException} are thrown in "non-silent" mode but since these are
    044 * RuntimeException, user-code <em>should</em> catch them wherever most appropriate.</p>
    045 *
    046 * @since 2.0
    047 */
    048public abstract class JexlEngine {
    049
    050    /** Default constructor */
    051    public JexlEngine() {} // Keep Javadoc happy
    052
    053    /**
    054     * The empty context class, public for instrospection.
    055     */
    056    public static final class EmptyContext implements JexlContext {
    057        /**
    058         * Default ctor.
    059         */
    060        EmptyContext() {}
    061
    062        @Override
    063        public Object get(final String name) {
    064            return null;
    065        }
    066
    067        @Override
    068        public boolean has(final String name) {
    069            return false;
    070        }
    071
    072        @Override
    073        public void set(final String name, final Object value) {
    074            throw new UnsupportedOperationException("Not supported in void context.");
    075        }
    076    }
    077
    078    /**
    079     * The  empty/static/non-mutable JexlNamespace class, public for instrospection.
    080     */
    081    public static final class EmptyNamespaceResolver implements JexlContext.NamespaceResolver {
    082        /**
    083         * Default ctor.
    084         */
    085        EmptyNamespaceResolver() {}
    086
    087        @Override
    088        public Object resolveNamespace(final String name) {
    089            return null;
    090        }
    091    }
    092
    093    /** The failure marker class. */
    094    private static final class FailObject {
    095        /**
    096         * Default ctor.
    097         */
    098        FailObject() {}
    099
    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 ThreadLocal<JexlContext.ThreadLocal> CONTEXT =
    171                       new ThreadLocal<>();
    172
    173    /**
    174     * The thread local engine.
    175     */
    176    protected static final ThreadLocal<JexlEngine> ENGINE =
    177                       new 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}