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