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}