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 }