View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.jexl2.scripting;
19  
20  import java.io.IOException;
21  import java.io.PrintWriter;
22  import java.io.Reader;
23  import java.io.Writer;
24  
25  import javax.script.AbstractScriptEngine;
26  import javax.script.Bindings;
27  import javax.script.Compilable;
28  import javax.script.CompiledScript;
29  import javax.script.ScriptContext;
30  import javax.script.ScriptEngine;
31  import javax.script.ScriptEngineFactory;
32  import javax.script.ScriptException;
33  import javax.script.SimpleBindings;
34  
35  import org.apache.commons.jexl2.JexlContext;
36  import org.apache.commons.jexl2.JexlEngine;
37  import org.apache.commons.jexl2.Script;
38  
39  import org.apache.commons.logging.Log;
40  import org.apache.commons.logging.LogFactory;
41  
42  /**
43   * Implements the Jexl ScriptEngine for JSF-223.
44   * <p>
45   * This implementation gives access to both ENGINE_SCOPE and GLOBAL_SCOPE bindings.
46   * When a JEXL script accesses a variable for read or write,
47   * this implementation checks first ENGINE and then GLOBAL scope.
48   * The first one found is used. 
49   * If no variable is found, and the JEXL script is writing to a variable,
50   * it will be stored in the ENGINE scope.
51   * </p>
52   * <p>
53   * The implementation also creates the "JEXL" script object as an instance of the
54   * class {@link JexlScriptObject} for access to utility methods and variables.
55   * </p>
56   * See
57   * <a href="http://java.sun.com/javase/6/docs/api/javax/script/package-summary.html">Java Scripting API</a>
58   * Javadoc.
59   * @since 2.0
60   */
61  public class JexlScriptEngine extends AbstractScriptEngine implements Compilable {
62      /** The logger. */
63      private static final Log LOG = LogFactory.getLog(JexlScriptEngine.class);
64  
65      /** The shared expression cache size. */
66      private static final int CACHE_SIZE = 512;
67  
68      /** Reserved key for context (mandated by JSR-223). */
69      public static final String CONTEXT_KEY = "context";
70  
71      /** Reserved key for JexlScriptObject. */
72      public static final String JEXL_OBJECT_KEY = "JEXL";
73  
74      /** The JexlScriptObject instance. */
75      private final JexlScriptObject jexlObject;
76  
77      /** The factory which created this instance. */
78      private final ScriptEngineFactory parentFactory;
79      
80      /** The JEXL EL engine. */
81      private final JexlEngine jexlEngine;
82      
83      /**
84       * Default constructor.
85       * <p>
86       * Only intended for use when not using a factory.
87       * Sets the factory to {@link JexlScriptEngineFactory}.
88       */
89      public JexlScriptEngine() {
90          this(FactorySingletonHolder.DEFAULT_FACTORY);
91      }
92  
93      /**
94       * Implements engine and engine context properties for use by JEXL scripts.
95       * Those properties are allways bound to the default engine scope context.
96       * <p>
97       * The following properties are defined:
98       * <ul>
99       * <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 }