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    }