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