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