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