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