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.jexl3.scripting;
19  
20  import java.io.BufferedReader;
21  import java.io.IOException;
22  import java.io.PrintWriter;
23  import java.io.Reader;
24  import java.io.Writer;
25  import java.lang.ref.Reference;
26  import java.lang.ref.SoftReference;
27  
28  import javax.script.AbstractScriptEngine;
29  import javax.script.Bindings;
30  import javax.script.Compilable;
31  import javax.script.CompiledScript;
32  import javax.script.ScriptContext;
33  import javax.script.ScriptEngine;
34  import javax.script.ScriptEngineFactory;
35  import javax.script.ScriptException;
36  import javax.script.SimpleBindings;
37  
38  import org.apache.commons.jexl3.JexlBuilder;
39  import org.apache.commons.jexl3.JexlContext;
40  import org.apache.commons.jexl3.JexlEngine;
41  import org.apache.commons.jexl3.JexlException;
42  import org.apache.commons.jexl3.JexlScript;
43  
44  import org.apache.commons.jexl3.introspection.JexlPermissions;
45  import org.apache.commons.logging.Log;
46  import org.apache.commons.logging.LogFactory;
47  
48  /**
49   * Implements the JEXL ScriptEngine for JSF-223.
50   * <p>
51   * This implementation gives access to both ENGINE_SCOPE and GLOBAL_SCOPE bindings.
52   * When a JEXL script accesses a variable for read or write,
53   * this implementation checks first ENGINE and then GLOBAL scope.
54   * The first one found is used.
55   * If no variable is found, and the JEXL script is writing to a variable,
56   * it will be stored in the ENGINE scope.
57   * </p>
58   * <p>
59   * The implementation also creates the "JEXL" script object as an instance of the
60   * class {@link JexlScriptObject} for access to utility methods and variables.
61   * </p>
62   * See
63   * <a href="http://java.sun.com/javase/6/docs/api/javax/script/package-summary.html">Java Scripting API</a>
64   * Javadoc.
65   *
66   * @since 2.0
67   */
68  public class JexlScriptEngine extends AbstractScriptEngine implements Compilable {
69      /**
70       * The shared engine instance.
71       * <p>A single soft-reference JEXL engine and JexlUberspect is shared by all instances of JexlScriptEngine.</p>
72       */
73      private static Reference<JexlEngine> ENGINE = null;
74  
75      /**
76       * The permissions used to create the script engine.
77       */
78      private static JexlPermissions PERMISSIONS = null;
79      /**
80       * Sets the permissions instance used to create the script engine.
81       * <p>Calling this method will force engine instance re-creation.</p>
82       * <p>To restore 3.2 script behavior:</p>
83       * <code>
84       *         JexlScriptEngine.setPermissions(JexlPermissions.UNRESTRICTED);
85       * </code>
86       * @param permissions the permissions instance to use or null to use the {@link JexlBuilder} default
87       * @since 3.3
88       */
89      public static void setPermissions(final JexlPermissions permissions) {
90          PERMISSIONS = permissions;
91          ENGINE = null; // will force recreation
92      }
93  
94      /**
95       * Sets the shared instance used for the script engine.
96       * <p>This should be called early enough to have an effect, ie before any
97       * {@link javax.script.ScriptEngineManager} features.</p>
98       * <p>To restore 3.2 script behavior:</p>
99       * <code>
100      *         JexlScriptEngine.setInstance(new JexlBuilder()
101      *                 .cache(512)
102      *                 .logger(LogFactory.getLog(JexlScriptEngine.class))
103      *                 .permissions(JexlPermissions.UNRESTRICTED)
104      *                 .create());
105      * </code>
106      * @param engine the JexlEngine instance to use
107      * @since 3.3
108      */
109     public static void setInstance(final JexlEngine engine) {
110         ENGINE = new SoftReference<>(engine);
111     }
112 
113     /**
114      * @return the shared JexlEngine instance, create it if necessary
115      */
116     private static JexlEngine getEngine() {
117         JexlEngine engine = ENGINE != null? ENGINE.get() : null;
118         if (engine == null) {
119             synchronized (JexlScriptEngineFactory.class) {
120                 engine = ENGINE != null? ENGINE.get() : null;
121                 if (engine == null) {
122                     final JexlBuilder builder = new JexlBuilder()
123                             .strict(true)
124                             .safe(false)
125                             .logger(JexlScriptEngine.LOG)
126                             .cache(JexlScriptEngine.CACHE_SIZE);
127                     if (PERMISSIONS != null ) {
128                         builder.permissions(PERMISSIONS);
129                     }
130                     engine = builder.create();
131                     ENGINE = new SoftReference<>(engine);
132                 }
133             }
134         }
135         return engine;
136     }
137 
138 
139     /** The logger. */
140     static final Log LOG = LogFactory.getLog(JexlScriptEngine.class);
141 
142     /** The shared expression cache size. */
143     static final int CACHE_SIZE = 512;
144 
145     /** Reserved key for context (mandated by JSR-223). */
146     public static final String CONTEXT_KEY = "context";
147 
148     /** Reserved key for JexlScriptObject. */
149     public static final String JEXL_OBJECT_KEY = "JEXL";
150 
151     /** The JexlScriptObject instance. */
152     final JexlScriptObject jexlObject;
153 
154     /** The factory which created this instance. */
155     final ScriptEngineFactory parentFactory;
156 
157     /** The JEXL EL engine. */
158     final JexlEngine jexlEngine;
159 
160     /**
161      * Default constructor.
162      *
163      * <p>Only intended for use when not using a factory.
164      * Sets the factory to {@link JexlScriptEngineFactory}.</p>
165      */
166     public JexlScriptEngine() {
167         this(FactorySingletonHolder.DEFAULT_FACTORY);
168     }
169 
170     /**
171      * Implements engine and engine context properties for use by JEXL scripts.
172      * Those properties are always bound to the default engine scope context.
173      *
174      * <p>The following properties are defined:</p>
175      *
176      * <ul>
177      *   <li>in - refers to the engine scope reader that defaults to reading System.err</li>
178      *   <li>out - refers the engine scope writer that defaults to writing in System.out</li>
179      *   <li>err - refers to the engine scope writer that defaults to writing in System.err</li>
180      *   <li>logger - the JexlScriptEngine logger</li>
181      *   <li>System - the System.class</li>
182      * </ul>
183      *
184      * @since 2.0
185      */
186     public class JexlScriptObject {
187 
188         /**
189          * Gives access to the underlying JEXL engine shared between all ScriptEngine instances.
190          * <p>Although this allows to manipulate various engine flags (lenient, debug, cache...)
191          * for <strong>all</strong> JexlScriptEngine instances, you probably should only do so
192          * if you are in strict control and sole user of the JEXL scripting feature.</p>
193          *
194          * @return the shared underlying JEXL engine
195          */
196         public JexlEngine getEngine() {
197             return jexlEngine;
198         }
199 
200         /**
201          * Gives access to the engine scope output writer (defaults to System.out).
202          *
203          * @return the engine output writer
204          */
205         public PrintWriter getOut() {
206             final Writer out = context.getWriter();
207             if (out instanceof PrintWriter) {
208                 return (PrintWriter) out;
209             }
210             if (out != null) {
211                 return new PrintWriter(out, true);
212             }
213             return null;
214         }
215 
216         /**
217          * Gives access to the engine scope error writer (defaults to System.err).
218          *
219          * @return the engine error writer
220          */
221         public PrintWriter getErr() {
222             final Writer error = context.getErrorWriter();
223             if (error instanceof PrintWriter) {
224                 return (PrintWriter) error;
225             }
226             if (error != null) {
227                 return new PrintWriter(error, true);
228             }
229             return null;
230         }
231 
232         /**
233          * Gives access to the engine scope input reader (defaults to System.in).
234          *
235          * @return the engine input reader
236          */
237         public Reader getIn() {
238             return context.getReader();
239         }
240 
241         /**
242          * Gives access to System class.
243          *
244          * @return System.class
245          */
246         public Class<System> getSystem() {
247             return System.class;
248         }
249 
250         /**
251          * Gives access to the engine logger.
252          *
253          * @return the JexlScriptEngine logger
254          */
255         public Log getLogger() {
256             return LOG;
257         }
258     }
259 
260 
261     /**
262      * Create a scripting engine using the supplied factory.
263      *
264      * @param factory the factory which created this instance.
265      * @throws NullPointerException if factory is null
266      */
267     public JexlScriptEngine(final ScriptEngineFactory factory) {
268         if (factory == null) {
269             throw new NullPointerException("ScriptEngineFactory must not be null");
270         }
271         parentFactory = factory;
272         jexlEngine = getEngine();
273         jexlObject = new JexlScriptObject();
274     }
275 
276     @Override
277     public Bindings createBindings() {
278         return new SimpleBindings();
279     }
280 
281     @Override
282     public Object eval(final Reader reader, final ScriptContext context) throws ScriptException {
283         // This is mandated by JSR-223 (see SCR.5.5.2   Methods)
284         if (reader == null || context == null) {
285             throw new NullPointerException("script and context must be non-null");
286         }
287         return eval(readerToString(reader), context);
288     }
289 
290     @Override
291     public Object eval(final String script, final ScriptContext context) throws ScriptException {
292         // This is mandated by JSR-223 (see SCR.5.5.2   Methods)
293         if (script == null || context == null) {
294             throw new NullPointerException("script and context must be non-null");
295         }
296         // This is mandated by JSR-223 (end of section SCR.4.3.4.1.2 - JexlScript Execution)
297         context.setAttribute(CONTEXT_KEY, context, ScriptContext.ENGINE_SCOPE);
298         try {
299             final JexlScript jexlScript = jexlEngine.createScript(script);
300             final JexlContext ctxt = new JexlContextWrapper(context);
301             return jexlScript.execute(ctxt);
302         } catch (final Exception e) {
303             throw scriptException(e);
304         }
305     }
306 
307     @Override
308     public ScriptEngineFactory getFactory() {
309         return parentFactory;
310     }
311 
312     @Override
313     public CompiledScript compile(final String script) throws ScriptException {
314         // This is mandated by JSR-223
315         if (script == null) {
316             throw new NullPointerException("script must be non-null");
317         }
318         try {
319             final JexlScript jexlScript = jexlEngine.createScript(script);
320             return new JexlCompiledScript(jexlScript);
321         } catch (final Exception e) {
322             throw scriptException(e);
323         }
324     }
325 
326     static ScriptException scriptException(final Exception e) {
327         Exception xany = e;
328         // unwrap a jexl exception
329         if (xany instanceof JexlException) {
330             final Throwable cause = xany.getCause();
331             if (cause instanceof Exception) {
332                 xany = (Exception) cause;
333             }
334         }
335         return new ScriptException(xany);
336     }
337 
338     @Override
339     public CompiledScript compile(final Reader script) throws ScriptException {
340         // This is mandated by JSR-223
341         if (script == null) {
342             throw new NullPointerException("script must be non-null");
343         }
344         return compile(readerToString(script));
345     }
346 
347     /**
348      * Read from a reader into a local buffer and return a String with
349      * the contents of the reader.
350      *
351      * @param scriptReader to be read.
352      * @return the contents of the reader as a String.
353      * @throws ScriptException on any error reading the reader.
354      */
355     private static String readerToString(final Reader scriptReader) throws ScriptException {
356         final StringBuilder buffer = new StringBuilder();
357         BufferedReader reader;
358         if (scriptReader instanceof BufferedReader) {
359             reader = (BufferedReader) scriptReader;
360         } else {
361             reader = new BufferedReader(scriptReader);
362         }
363         try {
364             String line;
365             while ((line = reader.readLine()) != null) {
366                 buffer.append(line).append('\n');
367             }
368             return buffer.toString();
369         } catch (final IOException e) {
370             throw new ScriptException(e);
371         }
372     }
373 
374     /**
375      * Holds singleton JexlScriptEngineFactory (IODH).
376      */
377     private static class FactorySingletonHolder {
378         /** non instantiable. */
379         private FactorySingletonHolder() {}
380 
381         /** The engine factory singleton instance. */
382         private static final JexlScriptEngineFactory DEFAULT_FACTORY = new JexlScriptEngineFactory();
383     }
384 
385     /**
386      * Wrapper to help convert a JSR-223 ScriptContext into a JexlContext.
387      *
388      * Current implementation only gives access to ENGINE_SCOPE binding.
389      */
390     private final class JexlContextWrapper implements JexlContext {
391         /** The wrapped script context. */
392         final ScriptContext scriptContext;
393 
394         /**
395          * Creates a context wrapper.
396          *
397          * @param theContext the engine context.
398          */
399         JexlContextWrapper (final ScriptContext theContext){
400             scriptContext = theContext;
401         }
402 
403         @Override
404         public Object get(final String name) {
405             final Object o = scriptContext.getAttribute(name);
406             if (JEXL_OBJECT_KEY.equals(name)) {
407                 if (o != null) {
408                     LOG.warn("JEXL is a reserved variable name, user defined value is ignored");
409                 }
410                 return jexlObject;
411             }
412             return o;
413         }
414 
415         @Override
416         public void set(final String name, final Object value) {
417             int scope = scriptContext.getAttributesScope(name);
418             if (scope == -1) { // not found, default to engine
419                 scope = ScriptContext.ENGINE_SCOPE;
420             }
421             scriptContext.getBindings(scope).put(name , value);
422         }
423 
424         @Override
425         public boolean has(final String name) {
426             final Bindings bnd = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);
427             return bnd.containsKey(name);
428         }
429 
430     }
431 
432     /**
433      * Wrapper to help convert a JEXL JexlScript into a JSR-223 CompiledScript.
434      */
435     private final class JexlCompiledScript extends CompiledScript {
436         /** The underlying JEXL expression instance. */
437         private final JexlScript script;
438 
439         /**
440          * Creates an instance.
441          *
442          * @param theScript to wrap
443          */
444         JexlCompiledScript(final JexlScript theScript) {
445             script = theScript;
446         }
447 
448         @Override
449         public String toString() {
450             return script.getSourceText();
451         }
452 
453         @Override
454         public Object eval(final ScriptContext context) throws ScriptException {
455             // This is mandated by JSR-223 (end of section SCR.4.3.4.1.2 - JexlScript Execution)
456             context.setAttribute(CONTEXT_KEY, context, ScriptContext.ENGINE_SCOPE);
457             try {
458                 final JexlContext ctxt = new JexlContextWrapper(context);
459                 return script.execute(ctxt);
460             } catch (final Exception e) {
461                 throw scriptException(e);
462             }
463         }
464 
465         @Override
466         public ScriptEngine getEngine() {
467             return JexlScriptEngine.this;
468         }
469     }
470 }