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