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