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 * 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 * Reads 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 }