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.jexl2.scripting;
019
020import java.io.IOException;
021import java.io.PrintWriter;
022import java.io.Reader;
023import java.io.Writer;
024
025import javax.script.AbstractScriptEngine;
026import javax.script.Bindings;
027import javax.script.Compilable;
028import javax.script.CompiledScript;
029import javax.script.ScriptContext;
030import javax.script.ScriptEngine;
031import javax.script.ScriptEngineFactory;
032import javax.script.ScriptException;
033import javax.script.SimpleBindings;
034
035import org.apache.commons.jexl2.JexlContext;
036import org.apache.commons.jexl2.JexlEngine;
037import org.apache.commons.jexl2.Script;
038
039import org.apache.commons.logging.Log;
040import 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 */
061public 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     * </p>
099     * <ul>
100     * <li>in - refers to the engine scope reader that defaults to reading System.err</li>
101     * <li>out - refers the engine scope writer that defaults to writing in System.out</li>
102     * <li>err - refers to the engine scope writer that defaults to writing in System.err</li>
103     * <li>logger - the JexlScriptEngine logger</li>
104     * <li>System - the System.class</li>
105     * </ul>
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}