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 }