    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 java.io.BufferedReader;
    021import java.io.File;
    022import java.io.FileInputStream;
    023import java.io.IOException;
    024import java.io.InputStreamReader;
    025import java.math.MathContext;
    026import java.net.URL;
    027import java.nio.charset.Charset;
    028
    029import org.apache.commons.jexl3.introspection.JexlUberspect;
    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    /**
    050     * The empty context class, public for instrospection.
    051     */
    052    public static final class EmptyContext implements JexlContext {
    053        /**
    054         * Default ctor.
    055         */
    056        EmptyContext() {}
    057
    058        @Override
    059        public Object get(final String name) {
    060            return null;
    061        }
    062
    063        @Override
    064        public boolean has(final String name) {
    065            return false;
    066        }
    067
    068        @Override
    069        public void set(final String name, final Object value) {
    070            throw new UnsupportedOperationException("Not supported in void context.");
    071        }
    072    }
    073
    074    /**
    075     * The  empty/static/non-mutable JexlNamespace class, public for instrospection.
    076     */
    077    public static final class EmptyNamespaceResolver implements JexlContext.NamespaceResolver {
    078        /**
    079         * Default ctor.
    080         */
    081        EmptyNamespaceResolver() {}
    082
    083        @Override
    084        public Object resolveNamespace(final String name) {
    085            return null;
    086        }
    087    }
    088
    089    /** The failure marker class. */
    090    private static final class FailObject {
    091        /**
    092         * Default ctor.
    093         */
    094        FailObject() {}
    095
    096        @Override
    097        public String toString() {
    098            return "tryExecute failed";
    099        }
    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}