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