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