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