1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.jexl2.scripting;
19
20 import java.io.IOException;
21 import java.io.PrintWriter;
22 import java.io.Reader;
23 import java.io.Writer;
24
25 import javax.script.AbstractScriptEngine;
26 import javax.script.Bindings;
27 import javax.script.Compilable;
28 import javax.script.CompiledScript;
29 import javax.script.ScriptContext;
30 import javax.script.ScriptEngine;
31 import javax.script.ScriptEngineFactory;
32 import javax.script.ScriptException;
33 import javax.script.SimpleBindings;
34
35 import org.apache.commons.jexl2.JexlContext;
36 import org.apache.commons.jexl2.JexlEngine;
37 import org.apache.commons.jexl2.Script;
38
39 import org.apache.commons.logging.Log;
40 import org.apache.commons.logging.LogFactory;
41
42 /**
43 * Implements the Jexl ScriptEngine for JSF-223.
44 * <p>
45 * This implementation gives access to both ENGINE_SCOPE and GLOBAL_SCOPE bindings.
46 * When a JEXL script accesses a variable for read or write,
47 * this implementation checks first ENGINE and then GLOBAL scope.
48 * The first one found is used.
49 * If no variable is found, and the JEXL script is writing to a variable,
50 * it will be stored in the ENGINE scope.
51 * </p>
52 * <p>
53 * The implementation also creates the "JEXL" script object as an instance of the
54 * class {@link JexlScriptObject} for access to utility methods and variables.
55 * </p>
56 * See
57 * <a href="http://java.sun.com/javase/6/docs/api/javax/script/package-summary.html">Java Scripting API</a>
58 * Javadoc.
59 * @since 2.0
60 */
61 public class JexlScriptEngine extends AbstractScriptEngine implements Compilable {
62 /** The logger. */
63 private static final Log LOG = LogFactory.getLog(JexlScriptEngine.class);
64
65 /** The shared expression cache size. */
66 private static final int CACHE_SIZE = 512;
67
68 /** Reserved key for context (mandated by JSR-223). */
69 public static final String CONTEXT_KEY = "context";
70
71 /** Reserved key for JexlScriptObject. */
72 public static final String JEXL_OBJECT_KEY = "JEXL";
73
74 /** The JexlScriptObject instance. */
75 private final JexlScriptObject jexlObject;
76
77 /** The factory which created this instance. */
78 private final ScriptEngineFactory parentFactory;
79
80 /** The JEXL EL engine. */
81 private final JexlEngine jexlEngine;
82
83 /**
84 * Default constructor.
85 * <p>
86 * Only intended for use when not using a factory.
87 * Sets the factory to {@link JexlScriptEngineFactory}.
88 */
89 public JexlScriptEngine() {
90 this(FactorySingletonHolder.DEFAULT_FACTORY);
91 }
92
93 /**
94 * Implements engine and engine context properties for use by JEXL scripts.
95 * Those properties are allways bound to the default engine scope context.
96 * <p>
97 * The following properties are defined:
98 * <ul>
99 * <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 }