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