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