001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.jexl3.scripting; 019 020import java.io.BufferedReader; 021import java.io.IOException; 022import java.io.PrintWriter; 023import java.io.Reader; 024import java.io.Writer; 025import java.lang.ref.Reference; 026import java.lang.ref.SoftReference; 027import java.util.Objects; 028 029import javax.script.AbstractScriptEngine; 030import javax.script.Bindings; 031import javax.script.Compilable; 032import javax.script.CompiledScript; 033import javax.script.ScriptContext; 034import javax.script.ScriptEngine; 035import javax.script.ScriptEngineFactory; 036import javax.script.ScriptException; 037import javax.script.SimpleBindings; 038 039import org.apache.commons.jexl3.JexlBuilder; 040import org.apache.commons.jexl3.JexlContext; 041import org.apache.commons.jexl3.JexlEngine; 042import org.apache.commons.jexl3.JexlException; 043import org.apache.commons.jexl3.JexlScript; 044import org.apache.commons.jexl3.introspection.JexlPermissions; 045import org.apache.commons.logging.Log; 046import org.apache.commons.logging.LogFactory; 047 048/** 049 * Implements the JEXL ScriptEngine for JSF-223. 050 * <p> 051 * This implementation gives access to both ENGINE_SCOPE and GLOBAL_SCOPE bindings. 052 * When a JEXL script accesses a variable for read or write, 053 * this implementation checks first ENGINE and then GLOBAL scope. 054 * The first one found is used. 055 * If no variable is found, and the JEXL script is writing to a variable, 056 * it will be stored in the ENGINE scope. 057 * </p> 058 * <p> 059 * The implementation also creates the "JEXL" script object as an instance of the 060 * class {@link JexlScriptObject} for access to utility methods and variables. 061 * </p> 062 * See 063 * <a href="https://java.sun.com/javase/6/docs/api/javax/script/package-summary.html">Java Scripting API</a> 064 * Javadoc. 065 * 066 * @since 2.0 067 */ 068public class JexlScriptEngine extends AbstractScriptEngine implements Compilable { 069 070 /** 071 * Holds singleton JexlScriptEngineFactory (IODH). 072 */ 073 private static final class FactorySingletonHolder { 074 075 /** The engine factory singleton instance. */ 076 static final JexlScriptEngineFactory DEFAULT_FACTORY = new JexlScriptEngineFactory(); 077 078 /** Non instantiable. */ 079 private FactorySingletonHolder() {} 080 } 081 082 /** 083 * Wrapper to help convert a JEXL JexlScript into a JSR-223 CompiledScript. 084 */ 085 private final class JexlCompiledScript extends CompiledScript { 086 087 /** The underlying JEXL expression instance. */ 088 private final JexlScript script; 089 090 /** 091 * Creates an instance. 092 * 093 * @param theScript to wrap 094 */ 095 JexlCompiledScript(final JexlScript theScript) { 096 script = theScript; 097 } 098 099 @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}