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