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