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