001/*
002 * Copyright 2002-2019 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 javax.script.Invocable;
021import javax.script.ScriptEngine;
022import javax.script.ScriptEngineManager;
023
024import org.springframework.beans.factory.BeanClassLoaderAware;
025import org.springframework.scripting.ScriptCompilationException;
026import org.springframework.scripting.ScriptFactory;
027import org.springframework.scripting.ScriptSource;
028import org.springframework.util.Assert;
029import org.springframework.util.ClassUtils;
030import org.springframework.util.ObjectUtils;
031import org.springframework.util.StringUtils;
032
033/**
034 * {@link org.springframework.scripting.ScriptFactory} implementation based
035 * on the JSR-223 script engine abstraction (as included in Java 6+).
036 * Supports JavaScript, Groovy, JRuby, and other JSR-223 compliant engines.
037 *
038 * <p>Typically used in combination with a
039 * {@link org.springframework.scripting.support.ScriptFactoryPostProcessor};
040 * see the latter's javadoc for a configuration example.
041 *
042 * @author Juergen Hoeller
043 * @since 4.2
044 * @see ScriptFactoryPostProcessor
045 */
046public class StandardScriptFactory implements ScriptFactory, BeanClassLoaderAware {
047
048        private final String scriptEngineName;
049
050        private final String scriptSourceLocator;
051
052        private final Class<?>[] scriptInterfaces;
053
054        private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
055
056        private volatile ScriptEngine scriptEngine;
057
058
059        /**
060         * Create a new StandardScriptFactory for the given script source.
061         * @param scriptSourceLocator a locator that points to the source of the script.
062         * Interpreted by the post-processor that actually creates the script.
063         */
064        public StandardScriptFactory(String scriptSourceLocator) {
065                this(null, scriptSourceLocator, (Class<?>[]) null);
066        }
067
068        /**
069         * Create a new StandardScriptFactory for the given script source.
070         * @param scriptSourceLocator a locator that points to the source of the script.
071         * Interpreted by the post-processor that actually creates the script.
072         * @param scriptInterfaces the Java interfaces that the scripted object
073         * is supposed to implement
074         */
075        public StandardScriptFactory(String scriptSourceLocator, Class<?>... scriptInterfaces) {
076                this(null, scriptSourceLocator, scriptInterfaces);
077        }
078
079        /**
080         * Create a new StandardScriptFactory for the given script source.
081         * @param scriptEngineName the name of the JSR-223 ScriptEngine to use
082         * (explicitly given instead of inferred from the script source)
083         * @param scriptSourceLocator a locator that points to the source of the script.
084         * Interpreted by the post-processor that actually creates the script.
085         */
086        public StandardScriptFactory(String scriptEngineName, String scriptSourceLocator) {
087                this(scriptEngineName, scriptSourceLocator, (Class<?>[]) null);
088        }
089
090        /**
091         * Create a new StandardScriptFactory for the given script source.
092         * @param scriptEngineName the name of the JSR-223 ScriptEngine to use
093         * (explicitly given instead of inferred from the script source)
094         * @param scriptSourceLocator a locator that points to the source of the script.
095         * Interpreted by the post-processor that actually creates the script.
096         * @param scriptInterfaces the Java interfaces that the scripted object
097         * is supposed to implement
098         */
099        public StandardScriptFactory(String scriptEngineName, String scriptSourceLocator, Class<?>... scriptInterfaces) {
100                Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty");
101                this.scriptEngineName = scriptEngineName;
102                this.scriptSourceLocator = scriptSourceLocator;
103                this.scriptInterfaces = scriptInterfaces;
104        }
105
106
107        @Override
108        public void setBeanClassLoader(ClassLoader classLoader) {
109                this.beanClassLoader = classLoader;
110        }
111
112        @Override
113        public String getScriptSourceLocator() {
114                return this.scriptSourceLocator;
115        }
116
117        @Override
118        public Class<?>[] getScriptInterfaces() {
119                return this.scriptInterfaces;
120        }
121
122        @Override
123        public boolean requiresConfigInterface() {
124                return false;
125        }
126
127
128        /**
129         * Load and parse the script via JSR-223's ScriptEngine.
130         */
131        @Override
132        public Object getScriptedObject(ScriptSource scriptSource, Class<?>... actualInterfaces)
133                        throws IOException, ScriptCompilationException {
134
135                Object script = evaluateScript(scriptSource);
136
137                if (!ObjectUtils.isEmpty(actualInterfaces)) {
138                        boolean adaptationRequired = false;
139                        for (Class<?> requestedIfc : actualInterfaces) {
140                                if (script instanceof Class ? !requestedIfc.isAssignableFrom((Class<?>) script) :
141                                                !requestedIfc.isInstance(script)) {
142                                        adaptationRequired = true;
143                                        break;
144                                }
145                        }
146                        if (adaptationRequired) {
147                                script = adaptToInterfaces(script, scriptSource, actualInterfaces);
148                        }
149                }
150
151                if (script instanceof Class) {
152                        Class<?> scriptClass = (Class<?>) script;
153                        try {
154                                return scriptClass.newInstance();
155                        }
156                        catch (InstantiationException ex) {
157                                throw new ScriptCompilationException(
158                                                scriptSource, "Unable to instantiate script class: " + scriptClass.getName(), ex);
159                        }
160                        catch (IllegalAccessException ex) {
161                                throw new ScriptCompilationException(
162                                                scriptSource, "Could not access script constructor: " + scriptClass.getName(), ex);
163                        }
164                }
165
166                return script;
167        }
168
169        protected Object evaluateScript(ScriptSource scriptSource) {
170                try {
171                        if (this.scriptEngine == null) {
172                                this.scriptEngine = retrieveScriptEngine(scriptSource);
173                                if (this.scriptEngine == null) {
174                                        throw new IllegalStateException("Could not determine script engine for " + scriptSource);
175                                }
176                        }
177                        return this.scriptEngine.eval(scriptSource.getScriptAsString());
178                }
179                catch (Exception ex) {
180                        throw new ScriptCompilationException(scriptSource, ex);
181                }
182        }
183
184        protected ScriptEngine retrieveScriptEngine(ScriptSource scriptSource) {
185                ScriptEngineManager scriptEngineManager = new ScriptEngineManager(this.beanClassLoader);
186
187                if (this.scriptEngineName != null) {
188                        return StandardScriptUtils.retrieveEngineByName(scriptEngineManager, this.scriptEngineName);
189                }
190
191                if (scriptSource instanceof ResourceScriptSource) {
192                        String filename = ((ResourceScriptSource) scriptSource).getResource().getFilename();
193                        if (filename != null) {
194                                String extension = StringUtils.getFilenameExtension(filename);
195                                if (extension != null) {
196                                        ScriptEngine engine = scriptEngineManager.getEngineByExtension(extension);
197                                        if (engine != null) {
198                                                return engine;
199                                        }
200                                }
201                        }
202                }
203
204                return null;
205        }
206
207        protected Object adaptToInterfaces(Object script, ScriptSource scriptSource, Class<?>... actualInterfaces) {
208                Class<?> adaptedIfc;
209                if (actualInterfaces.length == 1) {
210                        adaptedIfc = actualInterfaces[0];
211                }
212                else {
213                        adaptedIfc = ClassUtils.createCompositeInterface(actualInterfaces, this.beanClassLoader);
214                }
215
216                if (adaptedIfc != null) {
217                        if (!(this.scriptEngine instanceof Invocable)) {
218                                throw new ScriptCompilationException(scriptSource,
219                                                "ScriptEngine must implement Invocable in order to adapt it to an interface: " +
220                                                                this.scriptEngine);
221                        }
222                        Invocable invocable = (Invocable) this.scriptEngine;
223                        if (script != null) {
224                                script = invocable.getInterface(script, adaptedIfc);
225                        }
226                        if (script == null) {
227                                script = invocable.getInterface(adaptedIfc);
228                                if (script == null) {
229                                        throw new ScriptCompilationException(scriptSource,
230                                                        "Could not adapt script to interface [" + adaptedIfc.getName() + "]");
231                                }
232                        }
233                }
234
235                return script;
236        }
237
238        @Override
239        public Class<?> getScriptedObjectType(ScriptSource scriptSource)
240                        throws IOException, ScriptCompilationException {
241
242                return null;
243        }
244
245        @Override
246        public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) {
247                return scriptSource.isModified();
248        }
249
250
251        @Override
252        public String toString() {
253                return "StandardScriptFactory: script source locator [" + this.scriptSourceLocator + "]";
254        }
255
256}