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 }