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; 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 * Holds singleton JexlScriptEngineFactory (IODH). 071 */ 072 private static final class FactorySingletonHolder { 073 /** The engine factory singleton instance. */ 074 static final JexlScriptEngineFactory DEFAULT_FACTORY = new JexlScriptEngineFactory(); 075 076 /** Non instantiable. */ 077 private FactorySingletonHolder() {} 078 } 079 080 /** 081 * Wrapper to help convert a JEXL JexlScript into a JSR-223 CompiledScript. 082 */ 083 private final class JexlCompiledScript extends CompiledScript { 084 /** The underlying JEXL expression instance. */ 085 private final JexlScript script; 086 087 /** 088 * Creates an instance. 089 * 090 * @param theScript to wrap 091 */ 092 JexlCompiledScript(final JexlScript theScript) { 093 script = theScript; 094 } 095 096 @Override 097 public Object eval(final ScriptContext context) throws ScriptException { 098 // This is mandated by JSR-223 (end of section SCR.4.3.4.1.2 - JexlScript Execution) 099 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 * Read 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}