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