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