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