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