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