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