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 * http://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; 027 028import javax.script.AbstractScriptEngine; 029import javax.script.Bindings; 030import javax.script.Compilable; 031import javax.script.CompiledScript; 032import javax.script.ScriptContext; 033import javax.script.ScriptEngine; 034import javax.script.ScriptEngineFactory; 035import javax.script.ScriptException; 036import javax.script.SimpleBindings; 037 038import org.apache.commons.jexl3.JexlBuilder; 039import org.apache.commons.jexl3.JexlContext; 040import org.apache.commons.jexl3.JexlEngine; 041import org.apache.commons.jexl3.JexlException; 042import org.apache.commons.jexl3.JexlScript; 043import org.apache.commons.jexl3.introspection.JexlPermissions; 044import org.apache.commons.logging.Log; 045import org.apache.commons.logging.LogFactory; 046 047/** 048 * Implements the JEXL ScriptEngine for JSF-223. 049 * <p> 050 * This implementation gives access to both ENGINE_SCOPE and GLOBAL_SCOPE bindings. 051 * When a JEXL script accesses a variable for read or write, 052 * this implementation checks first ENGINE and then GLOBAL scope. 053 * The first one found is used. 054 * If no variable is found, and the JEXL script is writing to a variable, 055 * it will be stored in the ENGINE scope. 056 * </p> 057 * <p> 058 * The implementation also creates the "JEXL" script object as an instance of the 059 * class {@link JexlScriptObject} for access to utility methods and variables. 060 * </p> 061 * See 062 * <a href="https://java.sun.com/javase/6/docs/api/javax/script/package-summary.html">Java Scripting API</a> 063 * Javadoc. 064 * 065 * @since 2.0 066 */ 067public class JexlScriptEngine extends AbstractScriptEngine implements Compilable { 068 /** 069 * Holds singleton JexlScriptEngineFactory (IODH). 070 */ 071 private static final class FactorySingletonHolder { 072 /** The engine factory singleton instance. */ 073 static final JexlScriptEngineFactory DEFAULT_FACTORY = new JexlScriptEngineFactory(); 074 075 /** Non instantiable. */ 076 private FactorySingletonHolder() {} 077 } 078 079 /** 080 * Wrapper to help convert a JEXL JexlScript into a JSR-223 CompiledScript. 081 */ 082 private final class JexlCompiledScript extends CompiledScript { 083 /** The underlying JEXL expression instance. */ 084 private final JexlScript script; 085 086 /** 087 * Creates an instance. 088 * 089 * @param theScript to wrap 090 */ 091 JexlCompiledScript(final JexlScript theScript) { 092 script = theScript; 093 } 094 095 @Override 096 public Object eval(final ScriptContext context) throws ScriptException { 097 // This is mandated by JSR-223 (end of section SCR.4.3.4.1.2 - JexlScript Execution) 098 context.setAttribute(CONTEXT_KEY, context, ScriptContext.ENGINE_SCOPE); 099 try { 100 final JexlContext ctxt = new JexlContextWrapper(context); 101 return script.execute(ctxt); 102 } catch (final Exception e) { 103 throw scriptException(e); 104 } 105 } 106 107 @Override 108 public ScriptEngine getEngine() { 109 return JexlScriptEngine.this; 110 } 111 112 @Override 113 public String toString() { 114 return script.getSourceText(); 115 } 116 } 117 /** 118 * Wrapper to help convert a JSR-223 ScriptContext into a JexlContext. 119 * 120 * Current implementation only gives access to ENGINE_SCOPE binding. 121 */ 122 private final class JexlContextWrapper implements JexlContext { 123 /** The wrapped script context. */ 124 final ScriptContext scriptContext; 125 126 /** 127 * Creates a context wrapper. 128 * 129 * @param theContext the engine context. 130 */ 131 JexlContextWrapper (final ScriptContext theContext){ 132 scriptContext = theContext; 133 } 134 135 @Override 136 public Object get(final String name) { 137 final Object o = scriptContext.getAttribute(name); 138 if (JEXL_OBJECT_KEY.equals(name)) { 139 if (o != null) { 140 LOG.warn("JEXL is a reserved variable name, user-defined value is ignored"); 141 } 142 return jexlObject; 143 } 144 return o; 145 } 146 147 @Override 148 public boolean has(final String name) { 149 final Bindings bnd = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE); 150 return bnd.containsKey(name); 151 } 152 153 @Override 154 public void set(final String name, final Object value) { 155 int scope = scriptContext.getAttributesScope(name); 156 if (scope == -1) { // not found, default to engine 157 scope = ScriptContext.ENGINE_SCOPE; 158 } 159 scriptContext.getBindings(scope).put(name , value); 160 } 161 162 } 163 164 /** 165 * Implements engine and engine context properties for use by JEXL scripts. 166 * Those properties are always bound to the default engine scope context. 167 * 168 * <p>The following properties are defined:</p> 169 * 170 * <ul> 171 * <li>in - refers to the engine scope reader that defaults to reading System.err</li> 172 * <li>out - refers the engine scope writer that defaults to writing in System.out</li> 173 * <li>err - refers to the engine scope writer that defaults to writing in System.err</li> 174 * <li>logger - the JexlScriptEngine logger</li> 175 * <li>System - the System.class</li> 176 * </ul> 177 * 178 * @since 2.0 179 */ 180 public class JexlScriptObject { 181 182 /** 183 * Gives access to the underlying JEXL engine shared between all ScriptEngine instances. 184 * <p>Although this allows to manipulate various engine flags (lenient, debug, cache...) 185 * for <strong>all</strong> JexlScriptEngine instances, you probably should only do so 186 * if you are in strict control and sole user of the JEXL scripting feature.</p> 187 * 188 * @return the shared underlying JEXL engine 189 */ 190 public JexlEngine getEngine() { 191 return jexlEngine; 192 } 193 194 /** 195 * Gives access to the engine scope error writer (defaults to System.err). 196 * 197 * @return the engine error writer 198 */ 199 public PrintWriter getErr() { 200 final Writer error = context.getErrorWriter(); 201 if (error instanceof PrintWriter) { 202 return (PrintWriter) error; 203 } 204 if (error != null) { 205 return new PrintWriter(error, true); 206 } 207 return null; 208 } 209 210 /** 211 * Gives access to the engine scope input reader (defaults to System.in). 212 * 213 * @return the engine input reader 214 */ 215 public Reader getIn() { 216 return context.getReader(); 217 } 218 219 /** 220 * Gives access to the engine logger. 221 * 222 * @return the JexlScriptEngine logger 223 */ 224 public Log getLogger() { 225 return LOG; 226 } 227 228 /** 229 * Gives access to the engine scope output writer (defaults to System.out). 230 * 231 * @return the engine output writer 232 */ 233 public PrintWriter getOut() { 234 final Writer out = context.getWriter(); 235 if (out instanceof PrintWriter) { 236 return (PrintWriter) out; 237 } 238 if (out != null) { 239 return new PrintWriter(out, true); 240 } 241 return null; 242 } 243 244 /** 245 * Gives access to System class. 246 * 247 * @return System.class 248 */ 249 public Class<System> getSystem() { 250 return System.class; 251 } 252 } 253 254 /** 255 * The shared engine instance. 256 * <p>A single soft-reference JEXL engine and JexlUberspect is shared by all instances of JexlScriptEngine.</p> 257 */ 258 private static Reference<JexlEngine> ENGINE; 259 260 /** 261 * The permissions used to create the script engine. 262 */ 263 private static JexlPermissions PERMISSIONS; 264 265 /** The logger. */ 266 static final Log LOG = LogFactory.getLog(JexlScriptEngine.class); 267 268 /** The shared expression cache size. */ 269 static final int CACHE_SIZE = 512; 270 271 /** Reserved key for context (mandated by JSR-223). */ 272 public static final String CONTEXT_KEY = "context"; 273 274 /** Reserved key for JexlScriptObject. */ 275 public static final String JEXL_OBJECT_KEY = "JEXL"; 276 277 /** 278 * @return the shared JexlEngine instance, create it if necessary 279 */ 280 private static JexlEngine getEngine() { 281 JexlEngine engine = ENGINE != null ? ENGINE.get() : null; 282 if (engine == null) { 283 synchronized (JexlScriptEngineFactory.class) { 284 engine = ENGINE != null ? ENGINE.get() : null; 285 if (engine == null) { 286 final JexlBuilder builder = new JexlBuilder() 287 .strict(true) 288 .safe(false) 289 .logger(JexlScriptEngine.LOG) 290 .cache(JexlScriptEngine.CACHE_SIZE); 291 if (PERMISSIONS != null ) { 292 builder.permissions(PERMISSIONS); 293 } 294 engine = builder.create(); 295 ENGINE = new SoftReference<>(engine); 296 } 297 } 298 } 299 return engine; 300 } 301 302 /** 303 * Read from a reader into a local buffer and return a String with 304 * the contents of the reader. 305 * 306 * @param scriptReader to be read. 307 * @return the contents of the reader as a String. 308 * @throws ScriptException on any error reading the reader. 309 */ 310 private static String readerToString(final Reader scriptReader) throws ScriptException { 311 final StringBuilder buffer = new StringBuilder(); 312 BufferedReader reader; 313 if (scriptReader instanceof BufferedReader) { 314 reader = (BufferedReader) scriptReader; 315 } else { 316 reader = new BufferedReader(scriptReader); 317 } 318 try { 319 String line; 320 while ((line = reader.readLine()) != null) { 321 buffer.append(line).append('\n'); 322 } 323 return buffer.toString(); 324 } catch (final IOException e) { 325 throw new ScriptException(e); 326 } 327 } 328 329 static ScriptException scriptException(final Exception e) { 330 Exception xany = e; 331 // unwrap a jexl exception 332 if (xany instanceof JexlException) { 333 final Throwable cause = xany.getCause(); 334 if (cause instanceof Exception) { 335 xany = (Exception) cause; 336 } 337 } 338 return new ScriptException(xany); 339 } 340 341 /** 342 * Sets the shared instance used for the script engine. 343 * <p>This should be called early enough to have an effect, ie before any 344 * {@link javax.script.ScriptEngineManager} features.</p> 345 * <p>To restore 3.2 script behavior:</p> 346 * <code> 347 * JexlScriptEngine.setInstance(new JexlBuilder() 348 * .cache(512) 349 * .logger(LogFactory.getLog(JexlScriptEngine.class)) 350 * .permissions(JexlPermissions.UNRESTRICTED) 351 * .create()); 352 * </code> 353 * @param engine the JexlEngine instance to use 354 * @since 3.3 355 */ 356 public static void setInstance(final JexlEngine engine) { 357 ENGINE = new SoftReference<>(engine); 358 } 359 360 /** 361 * Sets the permissions instance used to create the script engine. 362 * <p>Calling this method will force engine instance re-creation.</p> 363 * <p>To restore 3.2 script behavior:</p> 364 * <code> 365 * JexlScriptEngine.setPermissions(JexlPermissions.UNRESTRICTED); 366 * </code> 367 * @param permissions the permissions instance to use or null to use the {@link JexlBuilder} default 368 * @since 3.3 369 */ 370 public static void setPermissions(final JexlPermissions permissions) { 371 PERMISSIONS = permissions; 372 ENGINE = null; // will force recreation 373 } 374 375 /** The JexlScriptObject instance. */ 376 final JexlScriptObject jexlObject; 377 378 /** The factory which created this instance. */ 379 final ScriptEngineFactory parentFactory; 380 381 /** The JEXL EL engine. */ 382 final JexlEngine jexlEngine; 383 384 /** 385 * Default constructor. 386 * 387 * <p>Only intended for use when not using a factory. 388 * Sets the factory to {@link JexlScriptEngineFactory}.</p> 389 */ 390 public JexlScriptEngine() { 391 this(FactorySingletonHolder.DEFAULT_FACTORY); 392 } 393 394 /** 395 * Create a scripting engine using the supplied factory. 396 * 397 * @param factory the factory which created this instance. 398 * @throws NullPointerException if factory is null 399 */ 400 public JexlScriptEngine(final ScriptEngineFactory factory) { 401 if (factory == null) { 402 throw new NullPointerException("ScriptEngineFactory must not be null"); 403 } 404 parentFactory = factory; 405 jexlEngine = getEngine(); 406 jexlObject = new JexlScriptObject(); 407 } 408 409 @Override 410 public CompiledScript compile(final Reader script) throws ScriptException { 411 // This is mandated by JSR-223 412 if (script == null) { 413 throw new NullPointerException("script must be non-null"); 414 } 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 if (script == null) { 422 throw new NullPointerException("script must be non-null"); 423 } 424 try { 425 final JexlScript jexlScript = jexlEngine.createScript(script); 426 return new JexlCompiledScript(jexlScript); 427 } catch (final Exception e) { 428 throw scriptException(e); 429 } 430 } 431 432 @Override 433 public Bindings createBindings() { 434 return new SimpleBindings(); 435 } 436 437 @Override 438 public Object eval(final Reader reader, final ScriptContext context) throws ScriptException { 439 // This is mandated by JSR-223 (see SCR.5.5.2 Methods) 440 if (reader == null || context == null) { 441 throw new NullPointerException("script and context must be non-null"); 442 } 443 return eval(readerToString(reader), context); 444 } 445 446 @Override 447 public Object eval(final String script, final ScriptContext context) throws ScriptException { 448 // This is mandated by JSR-223 (see SCR.5.5.2 Methods) 449 if (script == null || context == null) { 450 throw new NullPointerException("script and context must be non-null"); 451 } 452 // This is mandated by JSR-223 (end of section SCR.4.3.4.1.2 - JexlScript Execution) 453 context.setAttribute(CONTEXT_KEY, context, ScriptContext.ENGINE_SCOPE); 454 try { 455 final JexlScript jexlScript = jexlEngine.createScript(script); 456 final JexlContext ctxt = new JexlContextWrapper(context); 457 return jexlScript.execute(ctxt); 458 } catch (final Exception e) { 459 throw scriptException(e); 460 } 461 } 462 463 @Override 464 public ScriptEngineFactory getFactory() { 465 return parentFactory; 466 } 467}