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