001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.scripting.support;
018
019import java.io.IOException;
020import java.util.Map;
021
022import javax.script.Bindings;
023import javax.script.ScriptEngine;
024import javax.script.ScriptEngineManager;
025import javax.script.ScriptException;
026
027import org.springframework.beans.factory.BeanClassLoaderAware;
028import org.springframework.core.io.Resource;
029import org.springframework.lang.Nullable;
030import org.springframework.scripting.ScriptCompilationException;
031import org.springframework.scripting.ScriptEvaluator;
032import org.springframework.scripting.ScriptSource;
033import org.springframework.util.CollectionUtils;
034import org.springframework.util.StringUtils;
035
036/**
037 * {@code javax.script} (JSR-223) based implementation of Spring's {@link ScriptEvaluator}
038 * strategy interface.
039 *
040 * @author Juergen Hoeller
041 * @author Costin Leau
042 * @since 4.0
043 * @see ScriptEngine#eval(String)
044 */
045public class StandardScriptEvaluator implements ScriptEvaluator, BeanClassLoaderAware {
046
047        @Nullable
048        private String engineName;
049
050        @Nullable
051        private volatile Bindings globalBindings;
052
053        @Nullable
054        private volatile ScriptEngineManager scriptEngineManager;
055
056
057        /**
058         * Construct a new {@code StandardScriptEvaluator}.
059         */
060        public StandardScriptEvaluator() {
061        }
062
063        /**
064         * Construct a new {@code StandardScriptEvaluator} for the given class loader.
065         * @param classLoader the class loader to use for script engine detection
066         */
067        public StandardScriptEvaluator(ClassLoader classLoader) {
068                this.scriptEngineManager = new ScriptEngineManager(classLoader);
069        }
070
071        /**
072         * Construct a new {@code StandardScriptEvaluator} for the given JSR-223
073         * {@link ScriptEngineManager} to obtain script engines from.
074         * @param scriptEngineManager the ScriptEngineManager (or subclass thereof) to use
075         * @since 4.2.2
076         */
077        public StandardScriptEvaluator(ScriptEngineManager scriptEngineManager) {
078                this.scriptEngineManager = scriptEngineManager;
079        }
080
081
082        /**
083         * Set the name of the language meant for evaluating the scripts (e.g. "Groovy").
084         * <p>This is effectively an alias for {@link #setEngineName "engineName"},
085         * potentially (but not yet) providing common abbreviations for certain languages
086         * beyond what the JSR-223 script engine factory exposes.
087         * @see #setEngineName
088         */
089        public void setLanguage(String language) {
090                this.engineName = language;
091        }
092
093        /**
094         * Set the name of the script engine for evaluating the scripts (e.g. "Groovy"),
095         * as exposed by the JSR-223 script engine factory.
096         * @since 4.2.2
097         * @see #setLanguage
098         */
099        public void setEngineName(String engineName) {
100                this.engineName = engineName;
101        }
102
103        /**
104         * Set the globally scoped bindings on the underlying script engine manager,
105         * shared by all scripts, as an alternative to script argument bindings.
106         * @since 4.2.2
107         * @see #evaluate(ScriptSource, Map)
108         * @see javax.script.ScriptEngineManager#setBindings(Bindings)
109         * @see javax.script.SimpleBindings
110         */
111        public void setGlobalBindings(Map<String, Object> globalBindings) {
112                Bindings bindings = StandardScriptUtils.getBindings(globalBindings);
113                this.globalBindings = bindings;
114                ScriptEngineManager scriptEngineManager = this.scriptEngineManager;
115                if (scriptEngineManager != null) {
116                        scriptEngineManager.setBindings(bindings);
117                }
118        }
119
120        @Override
121        public void setBeanClassLoader(ClassLoader classLoader) {
122                ScriptEngineManager scriptEngineManager = this.scriptEngineManager;
123                if (scriptEngineManager == null) {
124                        scriptEngineManager = new ScriptEngineManager(classLoader);
125                        this.scriptEngineManager = scriptEngineManager;
126                        Bindings bindings = this.globalBindings;
127                        if (bindings != null) {
128                                scriptEngineManager.setBindings(bindings);
129                        }
130                }
131        }
132
133
134        @Override
135        @Nullable
136        public Object evaluate(ScriptSource script) {
137                return evaluate(script, null);
138        }
139
140        @Override
141        @Nullable
142        public Object evaluate(ScriptSource script, @Nullable Map<String, Object> argumentBindings) {
143                ScriptEngine engine = getScriptEngine(script);
144                try {
145                        if (CollectionUtils.isEmpty(argumentBindings)) {
146                                return engine.eval(script.getScriptAsString());
147                        }
148                        else {
149                                Bindings bindings = StandardScriptUtils.getBindings(argumentBindings);
150                                return engine.eval(script.getScriptAsString(), bindings);
151                        }
152                }
153                catch (IOException ex) {
154                        throw new ScriptCompilationException(script, "Cannot access script for ScriptEngine", ex);
155                }
156                catch (ScriptException ex) {
157                        throw new ScriptCompilationException(script, new StandardScriptEvalException(ex));
158                }
159        }
160
161        /**
162         * Obtain the JSR-223 ScriptEngine to use for the given script.
163         * @param script the script to evaluate
164         * @return the ScriptEngine (never {@code null})
165         */
166        protected ScriptEngine getScriptEngine(ScriptSource script) {
167                ScriptEngineManager scriptEngineManager = this.scriptEngineManager;
168                if (scriptEngineManager == null) {
169                        scriptEngineManager = new ScriptEngineManager();
170                        this.scriptEngineManager = scriptEngineManager;
171                }
172
173                if (StringUtils.hasText(this.engineName)) {
174                        return StandardScriptUtils.retrieveEngineByName(scriptEngineManager, this.engineName);
175                }
176                else if (script instanceof ResourceScriptSource) {
177                        Resource resource = ((ResourceScriptSource) script).getResource();
178                        String extension = StringUtils.getFilenameExtension(resource.getFilename());
179                        if (extension == null) {
180                                throw new IllegalStateException(
181                                                "No script language defined, and no file extension defined for resource: " + resource);
182                        }
183                        ScriptEngine engine = scriptEngineManager.getEngineByExtension(extension);
184                        if (engine == null) {
185                                throw new IllegalStateException("No matching engine found for file extension '" + extension + "'");
186                        }
187                        return engine;
188                }
189                else {
190                        throw new IllegalStateException(
191                                        "No script language defined, and no resource associated with script: " + script);
192                }
193        }
194
195}