    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.scripting;
    019
    020import java.io.BufferedReader;
    021import java.io.IOException;
    022import java.io.PrintWriter;
    023import java.io.Reader;
    024import java.io.Writer;
    025import java.lang.ref.Reference;
    026import java.lang.ref.SoftReference;
    027import java.util.Objects;
    028
    029import javax.script.AbstractScriptEngine;
    030import javax.script.Bindings;
    031import javax.script.Compilable;
    032import javax.script.CompiledScript;
    033import javax.script.ScriptContext;
    034import javax.script.ScriptEngine;
    035import javax.script.ScriptEngineFactory;
    036import javax.script.ScriptException;
    037import javax.script.SimpleBindings;
    038
    039import org.apache.commons.jexl3.JexlBuilder;
    040import org.apache.commons.jexl3.JexlContext;
    041import org.apache.commons.jexl3.JexlEngine;
    042import org.apache.commons.jexl3.JexlException;
    043import org.apache.commons.jexl3.JexlScript;
    044import org.apache.commons.jexl3.introspection.JexlPermissions;
    045import org.apache.commons.logging.Log;
    046import org.apache.commons.logging.LogFactory;
    047
    048/**
    049 * Implements the JEXL ScriptEngine for JSF-223.
    050 * <p>
    051 * This implementation gives access to both ENGINE_SCOPE and GLOBAL_SCOPE bindings.
    052 * When a JEXL script accesses a variable for read or write,
    053 * this implementation checks first ENGINE and then GLOBAL scope.
    054 * The first one found is used.
    055 * If no variable is found, and the JEXL script is writing to a variable,
    056 * it will be stored in the ENGINE scope.
    057 * </p>
    058 * <p>
    059 * The implementation also creates the "JEXL" script object as an instance of the
    060 * class {@link JexlScriptObject} for access to utility methods and variables.
    061 * </p>
    062 * See
    063 * <a href="https://java.sun.com/javase/6/docs/api/javax/script/package-summary.html">Java Scripting API</a>
    064 * Javadoc.
    065 *
    066 * @since 2.0
    067 */
    068public class JexlScriptEngine extends AbstractScriptEngine implements Compilable {
    069
    070    /**
    071     * Holds singleton JexlScriptEngineFactory (IODH).
    072     */
    073    private static final class FactorySingletonHolder {
    074
    075        /** The engine factory singleton instance. */
    076        static final JexlScriptEngineFactory DEFAULT_FACTORY = new JexlScriptEngineFactory();
    077
    078        /** Non instantiable. */
    079        private FactorySingletonHolder() {}
    080    }
    081
    082    /**
    083     * Wrapper to help convert a JEXL JexlScript into a JSR-223 CompiledScript.
    084     */
    085    private final class JexlCompiledScript extends CompiledScript {
    086
    087        /** The underlying JEXL expression instance. */
    088        private final JexlScript script;
    089
    090        /**
    091         * Creates an instance.
    092         *
    093         * @param theScript to wrap
    094         */
    095        JexlCompiledScript(final JexlScript theScript) {
    096            script = theScript;
    097        }
    098
    099        @Override
    100        public Object eval(final ScriptContext context) throws ScriptException {
    101            // This is mandated by JSR-223 (end of section SCR.4.3.4.1.2 - JexlScript Execution)
    102            context.setAttribute(CONTEXT_KEY, context, ScriptContext.ENGINE_SCOPE);
    103            try {
    104                final JexlContext ctxt = new JexlContextWrapper(context);
    105                return script.execute(ctxt);
    106            } catch (final Exception e) {
    107                throw scriptException(e);
    108            }
    109        }
    110
    111        @Override
    112        public ScriptEngine getEngine() {
    113            return JexlScriptEngine.this;
    114        }
    115
    116        @Override
    117        public String toString() {
    118            return script.getSourceText();
    119        }
    120    }
    121
    122    /**
    123     * Wrapper to help convert a JSR-223 ScriptContext into a JexlContext.
    124     *
    125     * Current implementation only gives access to ENGINE_SCOPE binding.
    126     */
    127    private final class JexlContextWrapper implements JexlContext {
    128
    129        /** The wrapped script context. */
    130        final ScriptContext scriptContext;
    131
    132        /**
    133         * Creates a context wrapper.
    134         *
    135         * @param theContext the engine context.
    136         */
    137        JexlContextWrapper (final ScriptContext theContext){
    138            scriptContext = theContext;
    139        }
    140
    141        @Override
    142        public Object get(final String name) {
    143            final Object o = scriptContext.getAttribute(name);
    144            if (JEXL_OBJECT_KEY.equals(name)) {
    145                if (o != null) {
    146                    LOG.warn("JEXL is a reserved variable name, user-defined value is ignored");
    147                }
    148                return jexlObject;
    149            }
    150            return o;
    151        }
    152
    153        @Override
    154        public boolean has(final String name) {
    155            final Bindings bnd = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);
    156            return bnd.containsKey(name);
    157        }
    158
    159        @Override
    160        public void set(final String name, final Object value) {
    161            int scope = scriptContext.getAttributesScope(name);
    162            if (scope == -1) { // not found, default to engine
    163                scope = ScriptContext.ENGINE_SCOPE;
    164            }
    165            scriptContext.getBindings(scope).put(name , value);
    166        }
    167
    168    }
    169
    170    /**
    171     * Implements engine and engine context properties for use by JEXL scripts.
    172     * Those properties are always bound to the default engine scope context.
    173     *
    174     * <p>The following properties are defined:</p>
    175     *
    176     * <ul>
    177     *   <li>in - refers to the engine scope reader that defaults to reading System.err</li>
    178     *   <li>out - refers the engine scope writer that defaults to writing in System.out</li>
    179     *   <li>err - refers to the engine scope writer that defaults to writing in System.err</li>
    180     *   <li>logger - the JexlScriptEngine logger</li>
    181     *   <li>System - the System.class</li>
    182     * </ul>
    183     *
    184     * @since 2.0
    185     */
    186    public class JexlScriptObject {
    187
    188        /** Default constructor */
    189        public JexlScriptObject() {} // Keep Javadoc happy
    190
    191        /**
    192         * Gives access to the underlying JEXL engine shared between all ScriptEngine instances.
    193         * <p>Although this allows to manipulate various engine flags (lenient, debug, cache...)
    194         * for <strong>all</strong> JexlScriptEngine instances, you probably should only do so
    195         * if you are in strict control and sole user of the JEXL scripting feature.</p>
    196         *
    197         * @return the shared underlying JEXL engine
    198         */
    199        public JexlEngine getEngine() {
    200            return jexlEngine;
    201        }
    202
    203        /**
    204         * Gives access to the engine scope error writer (defaults to System.err).
    205         *
    206         * @return the engine error writer
    207         */
    208        public PrintWriter getErr() {
    209            final Writer error = context.getErrorWriter();
    210            if (error instanceof PrintWriter) {
    211                return (PrintWriter) error;
    212            }
    213            if (error != null) {
    214                return new PrintWriter(error, true);
    215            }
    216            return null;
    217        }
    218
    219        /**
    220         * Gives access to the engine scope input reader (defaults to System.in).
    221         *
    222         * @return the engine input reader
    223         */
    224        public Reader getIn() {
    225            return context.getReader();
    226        }
    227
    228        /**
    229         * Gives access to the engine logger.
    230         *
    231         * @return the JexlScriptEngine logger
    232         */
    233        public Log getLogger() {
    234            return LOG;
    235        }
    236
    237        /**
    238         * Gives access to the engine scope output writer (defaults to System.out).
    239         *
    240         * @return the engine output writer
    241         */
    242        public PrintWriter getOut() {
    243            final Writer out = context.getWriter();
    244            if (out instanceof PrintWriter) {
    245                return (PrintWriter) out;
    246            }
    247            if (out != null) {
    248                return new PrintWriter(out, true);
    249            }
    250            return null;
    251        }
    252
    253        /**
    254         * Gives access to System class.
    255         *
    256         * @return System.class
    257         */
    258        public Class<System> getSystem() {
    259            return System.class;
    260        }
    261    }
    262
    263    /**
    264     * The shared engine instance.
    265     * <p>A single soft-reference JEXL engine and JexlUberspect is shared by all instances of JexlScriptEngine.</p>
    266     */
    267    private static Reference<JexlEngine> ENGINE;
    268
    269    /**
    270     * The permissions used to create the script engine.
    271     */
    272    private static JexlPermissions PERMISSIONS;
    273
    274    /** The logger. */
    275    static final Log LOG = LogFactory.getLog(JexlScriptEngine.class);
    276
    277    /** The shared expression cache size. */
    278    static final int CACHE_SIZE = 512;
    279
    280    /** Reserved key for context (mandated by JSR-223). */
    281    public static final String CONTEXT_KEY = "context";
    282
    283    /** Reserved key for JexlScriptObject. */
    284    public static final String JEXL_OBJECT_KEY = "JEXL";
    285
    286    /**
    287     * @return the shared JexlEngine instance, create it if necessary
    288     */
    289    private static JexlEngine getEngine() {
    290        JexlEngine engine = ENGINE != null ? ENGINE.get() : null;
    291        if (engine == null) {
    292            synchronized (JexlScriptEngineFactory.class) {
    293                engine = ENGINE != null ? ENGINE.get() : null;
    294                if (engine == null) {
    295                    final JexlBuilder builder = new JexlBuilder()
    296                            .strict(true)
    297                            .safe(false)
    298                            .logger(JexlScriptEngine.LOG)
    299                            .cache(JexlScriptEngine.CACHE_SIZE);
    300                    if (PERMISSIONS != null) {
    301                        builder.permissions(PERMISSIONS);
    302                    }
    303                    engine = builder.create();
    304                    ENGINE = new SoftReference<>(engine);
    305                }
    306            }
    307        }
    308        return engine;
    309    }
    310
    311    /**
    312     * Reads from a reader into a local buffer and return a String with
    313     * the contents of the reader.
    314     *
    315     * @param scriptReader to be read.
    316     * @return the contents of the reader as a String.
    317     * @throws ScriptException on any error reading the reader.
    318     */
    319    private static String readerToString(final Reader scriptReader) throws ScriptException {
    320        final StringBuilder buffer = new StringBuilder();
    321        BufferedReader reader;
    322        if (scriptReader instanceof BufferedReader) {
    323            reader = (BufferedReader) scriptReader;
    324        } else {
    325            reader = new BufferedReader(scriptReader);
    326        }
    327        try {
    328            String line;
    329            while ((line = reader.readLine()) != null) {
    330                buffer.append(line).append('\n');
    331            }
    332            return buffer.toString();
    333        } catch (final IOException e) {
    334            throw new ScriptException(e);
    335        }
    336    }
    337
    338    static ScriptException scriptException(final Exception e) {
    339        Exception xany = e;
    340        // unwrap a jexl exception
    341        if (xany instanceof JexlException) {
    342            final Throwable cause = xany.getCause();
    343            if (cause instanceof Exception) {
    344                xany = (Exception) cause;
    345            }
    346        }
    347        return new ScriptException(xany);
    348    }
    349
    350    /**
    351     * Sets the shared instance used for the script engine.
    352     * <p>This should be called early enough to have an effect, ie before any
    353     * {@link javax.script.ScriptEngineManager} features.</p>
    354     * <p>To restore 3.2 script behavior:</p>
    355     * {@code
    356     *         JexlScriptEngine.setInstance(new JexlBuilder()
    357     *                 .cache(512)
    358     *                 .logger(LogFactory.getLog(JexlScriptEngine.class))
    359     *                 .permissions(JexlPermissions.UNRESTRICTED)
    360     *                 .create());
    361     * }
    362     *
    363     * @param engine the JexlEngine instance to use
    364     * @since 3.3
    365     */
    366    public static void setInstance(final JexlEngine engine) {
    367        ENGINE = new SoftReference<>(engine);
    368    }
    369
    370    /**
    371     * Sets the permissions instance used to create the script engine.
    372     * <p>Calling this method will force engine instance re-creation.</p>
    373     * <p>To restore 3.2 script behavior:</p>
    374     * {@code
    375     *         JexlScriptEngine.setPermissions(JexlPermissions.UNRESTRICTED);
    376     * }
    377     *
    378     * @param permissions the permissions instance to use or null to use the {@link JexlBuilder} default
    379     * @since 3.3
    380     */
    381    public static void setPermissions(final JexlPermissions permissions) {
    382        PERMISSIONS = permissions;
    383        ENGINE = null; // will force recreation
    384    }
    385
    386    /** The JexlScriptObject instance. */
    387    final JexlScriptObject jexlObject;
    388
    389    /** The factory which created this instance. */
    390    final ScriptEngineFactory parentFactory;
    391
    392    /** The JEXL EL engine. */
    393    final JexlEngine jexlEngine;
    394
    395    /**
    396     * Default constructor.
    397     *
    398     * <p>Only intended for use when not using a factory.
    399     * Sets the factory to {@link JexlScriptEngineFactory}.</p>
    400     */
    401    public JexlScriptEngine() {
    402        this(FactorySingletonHolder.DEFAULT_FACTORY);
    403    }
    404
    405    /**
    406     * Create a scripting engine using the supplied factory.
    407     *
    408     * @param scriptEngineFactory the factory which created this instance.
    409     * @throws NullPointerException if factory is null
    410     */
    411    public JexlScriptEngine(final ScriptEngineFactory scriptEngineFactory) {
    412        Objects.requireNonNull(scriptEngineFactory, "scriptEngineFactory");
    413        parentFactory = scriptEngineFactory;
    414        jexlEngine = getEngine();
    415        jexlObject = new JexlScriptObject();
    416    }
    417
    418    @Override
    419    public CompiledScript compile(final Reader script) throws ScriptException {
    420        // This is mandated by JSR-223
    421        Objects.requireNonNull(script, "script");
    422        return compile(readerToString(script));
    423    }
    424
    425    @Override
    426    public CompiledScript compile(final String script) throws ScriptException {
    427        // This is mandated by JSR-223
    428        Objects.requireNonNull(script, "script");
    429        try {
    430            final JexlScript jexlScript = jexlEngine.createScript(script);
    431            return new JexlCompiledScript(jexlScript);
    432        } catch (final Exception e) {
    433            throw scriptException(e);
    434        }
    435    }
    436
    437    @Override
    438    public Bindings createBindings() {
    439        return new SimpleBindings();
    440    }
    441
    442    @Override
    443    public Object eval(final Reader reader, final ScriptContext context) throws ScriptException {
    444        // This is mandated by JSR-223 (see SCR.5.5.2   Methods)
    445        Objects.requireNonNull(reader, "reader");
    446        Objects.requireNonNull(context, "context");
    447        return eval(readerToString(reader), context);
    448    }
    449
    450    @Override
    451    public Object eval(final String script, final ScriptContext context) throws ScriptException {
    452        // This is mandated by JSR-223 (see SCR.5.5.2   Methods)
    453        Objects.requireNonNull(script, "context");
    454        Objects.requireNonNull(context, "context");
    455        // This is mandated by JSR-223 (end of section SCR.4.3.4.1.2 - JexlScript Execution)
    456        context.setAttribute(CONTEXT_KEY, context, ScriptContext.ENGINE_SCOPE);
    457        try {
    458            final JexlScript jexlScript = jexlEngine.createScript(script);
    459            final JexlContext ctxt = new JexlContextWrapper(context);
    460            return jexlScript.execute(ctxt);
    461        } catch (final Exception e) {
    462            throw scriptException(e);
    463        }
    464    }
    465
    466    @Override
    467    public ScriptEngineFactory getFactory() {
    468        return parentFactory;
    469    }
    470}