001/*
002 * Copyright 2002-2020 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.groovy;
018
019import java.io.IOException;
020import java.lang.reflect.InvocationTargetException;
021
022import groovy.lang.GroovyClassLoader;
023import groovy.lang.GroovyObject;
024import groovy.lang.MetaClass;
025import groovy.lang.Script;
026import org.codehaus.groovy.control.CompilationFailedException;
027import org.codehaus.groovy.control.CompilerConfiguration;
028import org.codehaus.groovy.control.customizers.CompilationCustomizer;
029
030import org.springframework.beans.factory.BeanClassLoaderAware;
031import org.springframework.beans.factory.BeanFactory;
032import org.springframework.beans.factory.BeanFactoryAware;
033import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
034import org.springframework.lang.Nullable;
035import org.springframework.scripting.ScriptCompilationException;
036import org.springframework.scripting.ScriptFactory;
037import org.springframework.scripting.ScriptSource;
038import org.springframework.util.Assert;
039import org.springframework.util.ClassUtils;
040import org.springframework.util.ObjectUtils;
041import org.springframework.util.ReflectionUtils;
042
043/**
044 * {@link org.springframework.scripting.ScriptFactory} implementation
045 * for a Groovy script.
046 *
047 * <p>Typically used in combination with a
048 * {@link org.springframework.scripting.support.ScriptFactoryPostProcessor};
049 * see the latter's javadoc for a configuration example.
050 *
051 * <p>Note: Spring 4.0 supports Groovy 1.8 and higher.
052 *
053 * @author Juergen Hoeller
054 * @author Rob Harrop
055 * @author Rod Johnson
056 * @since 2.0
057 * @see groovy.lang.GroovyClassLoader
058 * @see org.springframework.scripting.support.ScriptFactoryPostProcessor
059 */
060public class GroovyScriptFactory implements ScriptFactory, BeanFactoryAware, BeanClassLoaderAware {
061
062        private final String scriptSourceLocator;
063
064        @Nullable
065        private GroovyObjectCustomizer groovyObjectCustomizer;
066
067        @Nullable
068        private CompilerConfiguration compilerConfiguration;
069
070        @Nullable
071        private GroovyClassLoader groovyClassLoader;
072
073        @Nullable
074        private Class<?> scriptClass;
075
076        @Nullable
077        private Class<?> scriptResultClass;
078
079        @Nullable
080        private CachedResultHolder cachedResult;
081
082        private final Object scriptClassMonitor = new Object();
083
084        private boolean wasModifiedForTypeCheck = false;
085
086
087        /**
088         * Create a new GroovyScriptFactory for the given script source.
089         * <p>We don't need to specify script interfaces here, since
090         * a Groovy script defines its Java interfaces itself.
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 GroovyScriptFactory(String scriptSourceLocator) {
095                Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty");
096                this.scriptSourceLocator = scriptSourceLocator;
097        }
098
099        /**
100         * Create a new GroovyScriptFactory for the given script source,
101         * specifying a strategy interface that can create a custom MetaClass
102         * to supply missing methods and otherwise change the behavior of the object.
103         * @param scriptSourceLocator a locator that points to the source of the script.
104         * Interpreted by the post-processor that actually creates the script.
105         * @param groovyObjectCustomizer a customizer that can set a custom metaclass
106         * or make other changes to the GroovyObject created by this factory
107         * (may be {@code null})
108         * @see GroovyObjectCustomizer#customize
109         */
110        public GroovyScriptFactory(String scriptSourceLocator, @Nullable GroovyObjectCustomizer groovyObjectCustomizer) {
111                this(scriptSourceLocator);
112                this.groovyObjectCustomizer = groovyObjectCustomizer;
113        }
114
115        /**
116         * Create a new GroovyScriptFactory for the given script source,
117         * specifying a strategy interface that can create a custom MetaClass
118         * to supply missing methods and otherwise change the behavior of the object.
119         * @param scriptSourceLocator a locator that points to the source of the script.
120         * Interpreted by the post-processor that actually creates the script.
121         * @param compilerConfiguration a custom compiler configuration to be applied
122         * to the GroovyClassLoader (may be {@code null})
123         * @since 4.3.3
124         * @see GroovyClassLoader#GroovyClassLoader(ClassLoader, CompilerConfiguration)
125         */
126        public GroovyScriptFactory(String scriptSourceLocator, @Nullable CompilerConfiguration compilerConfiguration) {
127                this(scriptSourceLocator);
128                this.compilerConfiguration = compilerConfiguration;
129        }
130
131        /**
132         * Create a new GroovyScriptFactory for the given script source,
133         * specifying a strategy interface that can customize Groovy's compilation
134         * process within the underlying GroovyClassLoader.
135         * @param scriptSourceLocator a locator that points to the source of the script.
136         * Interpreted by the post-processor that actually creates the script.
137         * @param compilationCustomizers one or more customizers to be applied to the
138         * GroovyClassLoader compiler configuration
139         * @since 4.3.3
140         * @see CompilerConfiguration#addCompilationCustomizers
141         * @see org.codehaus.groovy.control.customizers.ImportCustomizer
142         */
143        public GroovyScriptFactory(String scriptSourceLocator, CompilationCustomizer... compilationCustomizers) {
144                this(scriptSourceLocator);
145                if (!ObjectUtils.isEmpty(compilationCustomizers)) {
146                        this.compilerConfiguration = new CompilerConfiguration();
147                        this.compilerConfiguration.addCompilationCustomizers(compilationCustomizers);
148                }
149        }
150
151
152        @Override
153        public void setBeanFactory(BeanFactory beanFactory) {
154                if (beanFactory instanceof ConfigurableListableBeanFactory) {
155                        ((ConfigurableListableBeanFactory) beanFactory).ignoreDependencyType(MetaClass.class);
156                }
157        }
158
159        @Override
160        public void setBeanClassLoader(ClassLoader classLoader) {
161                if (classLoader instanceof GroovyClassLoader &&
162                                (this.compilerConfiguration == null ||
163                                                ((GroovyClassLoader) classLoader).hasCompatibleConfiguration(this.compilerConfiguration))) {
164                        this.groovyClassLoader = (GroovyClassLoader) classLoader;
165                }
166                else {
167                        this.groovyClassLoader = buildGroovyClassLoader(classLoader);
168                }
169        }
170
171        /**
172         * Return the GroovyClassLoader used by this script factory.
173         */
174        public GroovyClassLoader getGroovyClassLoader() {
175                synchronized (this.scriptClassMonitor) {
176                        if (this.groovyClassLoader == null) {
177                                this.groovyClassLoader = buildGroovyClassLoader(ClassUtils.getDefaultClassLoader());
178                        }
179                        return this.groovyClassLoader;
180                }
181        }
182
183        /**
184         * Build a {@link GroovyClassLoader} for the given {@code ClassLoader}.
185         * @param classLoader the ClassLoader to build a GroovyClassLoader for
186         * @since 4.3.3
187         */
188        protected GroovyClassLoader buildGroovyClassLoader(@Nullable ClassLoader classLoader) {
189                return (this.compilerConfiguration != null ?
190                                new GroovyClassLoader(classLoader, this.compilerConfiguration) : new GroovyClassLoader(classLoader));
191        }
192
193
194        @Override
195        public String getScriptSourceLocator() {
196                return this.scriptSourceLocator;
197        }
198
199        /**
200         * Groovy scripts determine their interfaces themselves,
201         * hence we don't need to explicitly expose interfaces here.
202         * @return {@code null} always
203         */
204        @Override
205        @Nullable
206        public Class<?>[] getScriptInterfaces() {
207                return null;
208        }
209
210        /**
211         * Groovy scripts do not need a config interface,
212         * since they expose their setters as public methods.
213         */
214        @Override
215        public boolean requiresConfigInterface() {
216                return false;
217        }
218
219
220        /**
221         * Loads and parses the Groovy script via the GroovyClassLoader.
222         * @see groovy.lang.GroovyClassLoader
223         */
224        @Override
225        @Nullable
226        public Object getScriptedObject(ScriptSource scriptSource, @Nullable Class<?>... actualInterfaces)
227                        throws IOException, ScriptCompilationException {
228
229                synchronized (this.scriptClassMonitor) {
230                        try {
231                                Class<?> scriptClassToExecute;
232                                this.wasModifiedForTypeCheck = false;
233
234                                if (this.cachedResult != null) {
235                                        Object result = this.cachedResult.object;
236                                        this.cachedResult = null;
237                                        return result;
238                                }
239
240                                if (this.scriptClass == null || scriptSource.isModified()) {
241                                        // New script content...
242                                        this.scriptClass = getGroovyClassLoader().parseClass(
243                                                        scriptSource.getScriptAsString(), scriptSource.suggestedClassName());
244
245                                        if (Script.class.isAssignableFrom(this.scriptClass)) {
246                                                // A Groovy script, probably creating an instance: let's execute it.
247                                                Object result = executeScript(scriptSource, this.scriptClass);
248                                                this.scriptResultClass = (result != null ? result.getClass() : null);
249                                                return result;
250                                        }
251                                        else {
252                                                this.scriptResultClass = this.scriptClass;
253                                        }
254                                }
255                                scriptClassToExecute = this.scriptClass;
256
257                                // Process re-execution outside of the synchronized block.
258                                return executeScript(scriptSource, scriptClassToExecute);
259                        }
260                        catch (CompilationFailedException ex) {
261                                this.scriptClass = null;
262                                this.scriptResultClass = null;
263                                throw new ScriptCompilationException(scriptSource, ex);
264                        }
265                }
266        }
267
268        @Override
269        @Nullable
270        public Class<?> getScriptedObjectType(ScriptSource scriptSource)
271                        throws IOException, ScriptCompilationException {
272
273                synchronized (this.scriptClassMonitor) {
274                        try {
275                                if (this.scriptClass == null || scriptSource.isModified()) {
276                                        // New script content...
277                                        this.wasModifiedForTypeCheck = true;
278                                        this.scriptClass = getGroovyClassLoader().parseClass(
279                                                        scriptSource.getScriptAsString(), scriptSource.suggestedClassName());
280
281                                        if (Script.class.isAssignableFrom(this.scriptClass)) {
282                                                // A Groovy script, probably creating an instance: let's execute it.
283                                                Object result = executeScript(scriptSource, this.scriptClass);
284                                                this.scriptResultClass = (result != null ? result.getClass() : null);
285                                                this.cachedResult = new CachedResultHolder(result);
286                                        }
287                                        else {
288                                                this.scriptResultClass = this.scriptClass;
289                                        }
290                                }
291                                return this.scriptResultClass;
292                        }
293                        catch (CompilationFailedException ex) {
294                                this.scriptClass = null;
295                                this.scriptResultClass = null;
296                                this.cachedResult = null;
297                                throw new ScriptCompilationException(scriptSource, ex);
298                        }
299                }
300        }
301
302        @Override
303        public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) {
304                synchronized (this.scriptClassMonitor) {
305                        return (scriptSource.isModified() || this.wasModifiedForTypeCheck);
306                }
307        }
308
309
310        /**
311         * Instantiate the given Groovy script class and run it if necessary.
312         * @param scriptSource the source for the underlying script
313         * @param scriptClass the Groovy script class
314         * @return the result object (either an instance of the script class
315         * or the result of running the script instance)
316         * @throws ScriptCompilationException in case of instantiation failure
317         */
318        @Nullable
319        protected Object executeScript(ScriptSource scriptSource, Class<?> scriptClass) throws ScriptCompilationException {
320                try {
321                        GroovyObject goo = (GroovyObject) ReflectionUtils.accessibleConstructor(scriptClass).newInstance();
322
323                        if (this.groovyObjectCustomizer != null) {
324                                // Allow metaclass and other customization.
325                                this.groovyObjectCustomizer.customize(goo);
326                        }
327
328                        if (goo instanceof Script) {
329                                // A Groovy script, probably creating an instance: let's execute it.
330                                return ((Script) goo).run();
331                        }
332                        else {
333                                // An instance of the scripted class: let's return it as-is.
334                                return goo;
335                        }
336                }
337                catch (NoSuchMethodException ex) {
338                        throw new ScriptCompilationException(
339                                        "No default constructor on Groovy script class: " + scriptClass.getName(), ex);
340                }
341                catch (InstantiationException ex) {
342                        throw new ScriptCompilationException(
343                                        scriptSource, "Unable to instantiate Groovy script class: " + scriptClass.getName(), ex);
344                }
345                catch (IllegalAccessException ex) {
346                        throw new ScriptCompilationException(
347                                        scriptSource, "Could not access Groovy script constructor: " + scriptClass.getName(), ex);
348                }
349                catch (InvocationTargetException ex) {
350                        throw new ScriptCompilationException(
351                                        "Failed to invoke Groovy script constructor: " + scriptClass.getName(), ex.getTargetException());
352                }
353        }
354
355
356        @Override
357        public String toString() {
358                return "GroovyScriptFactory: script source locator [" + this.scriptSourceLocator + "]";
359        }
360
361
362        /**
363         * Wrapper that holds a temporarily cached result object.
364         */
365        private static class CachedResultHolder {
366
367                @Nullable
368                public final Object object;
369
370                public CachedResultHolder(@Nullable Object object) {
371                        this.object = object;
372                }
373        }
374
375}