1 /*
2  * Copyright 2002-2008 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.springframework.scripting.groovy;
18 
19 import java.io.IOException;
20 
21 import groovy.lang.GroovyClassLoader;
22 import groovy.lang.GroovyObject;
23 import groovy.lang.MetaClass;
24 import groovy.lang.Script;
25 import org.codehaus.groovy.control.CompilationFailedException;
26 
27 import org.springframework.beans.BeansException;
28 import org.springframework.beans.factory.BeanClassLoaderAware;
29 import org.springframework.beans.factory.BeanFactory;
30 import org.springframework.beans.factory.BeanFactoryAware;
31 import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
32 import org.springframework.scripting.ScriptCompilationException;
33 import org.springframework.scripting.ScriptFactory;
34 import org.springframework.scripting.ScriptSource;
35 import org.springframework.util.Assert;
36 import org.springframework.util.ClassUtils;
37 
38 /**
39  * {@link org.springframework.scripting.ScriptFactory} implementation
40  * for a Groovy script.
41  *
42  * <p>Typically used in combination with a
43  * {@link org.springframework.scripting.support.ScriptFactoryPostProcessor};
44  * see the latter's javadoc} for a configuration example.
45  *
46  * @author Juergen Hoeller
47  * @author Rob Harrop
48  * @author Rod Johnson
49  * @since 2.0
50  * @see groovy.lang.GroovyClassLoader
51  * @see org.springframework.scripting.support.ScriptFactoryPostProcessor
52  */
53 public class GroovyScriptFactory implements ScriptFactory, BeanFactoryAware, BeanClassLoaderAware {
54 
55 	private final String scriptSourceLocator;
56 
57 	private final GroovyObjectCustomizer groovyObjectCustomizer;
58 
59 	private GroovyClassLoader groovyClassLoader;
60 
61 	private Class scriptClass;
62 
63 	private Class scriptResultClass;
64 
65 	private CachedResultHolder cachedResult;
66 
67 	private final Object scriptClassMonitor = new Object();
68 
69 	private boolean wasModifiedForTypeCheck = false;
70 
71 
72 	/**
73 	 * Create a new GroovyScriptFactory for the given script source.
74 	 * <p>We don't need to specify script interfaces here, since
75 	 * a Groovy script defines its Java interfaces itself.
76 	 * @param scriptSourceLocator a locator that points to the source of the script.
77 	 * Interpreted by the post-processor that actually creates the script.
78 	 */
GroovyScriptFactory(String scriptSourceLocator)79 	public GroovyScriptFactory(String scriptSourceLocator) {
80 		this(scriptSourceLocator, null);
81 	}
82 
83 	/**
84 	 * Create a new GroovyScriptFactory for the given script source,
85 	 * specifying a strategy interface that can create a custom MetaClass
86 	 * to supply missing methods and otherwise change the behavior of the object.
87 	 * <p>We don't need to specify script interfaces here, since
88 	 * a Groovy script defines its Java interfaces itself.
89 	 * @param scriptSourceLocator a locator that points to the source of the script.
90 	 * Interpreted by the post-processor that actually creates the script.
91 	 * @param groovyObjectCustomizer a customizer that can set a custom metaclass
92 	 * or make other changes to the GroovyObject created by this factory
93 	 * (may be <code>null</code>)
94 	 */
GroovyScriptFactory(String scriptSourceLocator, GroovyObjectCustomizer groovyObjectCustomizer)95 	public GroovyScriptFactory(String scriptSourceLocator, GroovyObjectCustomizer groovyObjectCustomizer) {
96 		Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty");
97 		this.scriptSourceLocator = scriptSourceLocator;
98 		this.groovyObjectCustomizer = groovyObjectCustomizer;
99 	}
100 
101 
setBeanFactory(BeanFactory beanFactory)102 	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
103 		if (beanFactory instanceof ConfigurableListableBeanFactory) {
104 			((ConfigurableListableBeanFactory) beanFactory).ignoreDependencyType(MetaClass.class);
105 		}
106 	}
107 
setBeanClassLoader(ClassLoader classLoader)108 	public void setBeanClassLoader(ClassLoader classLoader) {
109 		this.groovyClassLoader = new GroovyClassLoader(classLoader);
110 	}
111 
112 	/**
113 	 * Return the GroovyClassLoader used by this script factory.
114 	 */
getGroovyClassLoader()115 	public GroovyClassLoader getGroovyClassLoader() {
116 		synchronized (this.scriptClassMonitor) {
117 			if (this.groovyClassLoader == null) {
118 				this.groovyClassLoader = new GroovyClassLoader(ClassUtils.getDefaultClassLoader());
119 			}
120 			return this.groovyClassLoader;
121 		}
122 	}
123 
124 
getScriptSourceLocator()125 	public String getScriptSourceLocator() {
126 		return this.scriptSourceLocator;
127 	}
128 
129 	/**
130 	 * Groovy scripts determine their interfaces themselves,
131 	 * hence we don't need to explicitly expose interfaces here.
132 	 * @return <code>null</code> always
133 	 */
getScriptInterfaces()134 	public Class[] getScriptInterfaces() {
135 		return null;
136 	}
137 
138 	/**
139 	 * Groovy scripts do not need a config interface,
140 	 * since they expose their setters as public methods.
141 	 */
requiresConfigInterface()142 	public boolean requiresConfigInterface() {
143 		return false;
144 	}
145 
146 
147 	/**
148 	 * Loads and parses the Groovy script via the GroovyClassLoader.
149 	 * @see groovy.lang.GroovyClassLoader
150 	 */
getScriptedObject(ScriptSource scriptSource, Class[] actualInterfaces)151 	public Object getScriptedObject(ScriptSource scriptSource, Class[] actualInterfaces)
152 			throws IOException, ScriptCompilationException {
153 
154 		try {
155 			Class scriptClassToExecute = null;
156 
157 			synchronized (this.scriptClassMonitor) {
158 				this.wasModifiedForTypeCheck = false;
159 
160 				if (this.cachedResult != null) {
161 					Object result = this.cachedResult.object;
162 					this.cachedResult = null;
163 					return result;
164 				}
165 
166 				if (this.scriptClass == null || scriptSource.isModified()) {
167 					// New script content...
168 					this.scriptClass = getGroovyClassLoader().parseClass(
169 							scriptSource.getScriptAsString(), scriptSource.suggestedClassName());
170 
171 					if (Script.class.isAssignableFrom(this.scriptClass)) {
172 						// A Groovy script, probably creating an instance: let's execute it.
173 						Object result = executeScript(scriptSource, this.scriptClass);
174 						this.scriptResultClass = (result != null ? result.getClass() : null);
175 						return result;
176 					}
177 					else {
178 						this.scriptResultClass = this.scriptClass;
179 					}
180 				}
181 				scriptClassToExecute = this.scriptClass;
182 			}
183 
184 			// Process re-execution outside of the synchronized block.
185 			return executeScript(scriptSource, scriptClassToExecute);
186 		}
187 		catch (CompilationFailedException ex) {
188 			throw new ScriptCompilationException(scriptSource, ex);
189 		}
190 	}
191 
getScriptedObjectType(ScriptSource scriptSource)192 	public Class getScriptedObjectType(ScriptSource scriptSource)
193 			throws IOException, ScriptCompilationException {
194 
195 		try {
196 			synchronized (this.scriptClassMonitor) {
197 				if (this.scriptClass == null || scriptSource.isModified()) {
198 					// New script content...
199 					this.wasModifiedForTypeCheck = true;
200 					this.scriptClass = getGroovyClassLoader().parseClass(
201 							scriptSource.getScriptAsString(), scriptSource.suggestedClassName());
202 
203 					if (Script.class.isAssignableFrom(this.scriptClass)) {
204 						// A Groovy script, probably creating an instance: let's execute it.
205 						Object result = executeScript(scriptSource, this.scriptClass);
206 						this.scriptResultClass = (result != null ? result.getClass() : null);
207 						this.cachedResult = new CachedResultHolder(result);
208 					}
209 					else {
210 						this.scriptResultClass = this.scriptClass;
211 					}
212 				}
213 				return this.scriptResultClass;
214 			}
215 		}
216 		catch (CompilationFailedException ex) {
217 			throw new ScriptCompilationException(scriptSource, ex);
218 		}
219 	}
220 
requiresScriptedObjectRefresh(ScriptSource scriptSource)221 	public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) {
222 		synchronized (this.scriptClassMonitor) {
223 			return (scriptSource.isModified() || this.wasModifiedForTypeCheck);
224 		}
225 	}
226 
227 
228 	/**
229 	 * Instantiate the given Groovy script class and run it if necessary.
230 	 * @param scriptSource the source for the underlying script
231 	 * @param scriptClass the Groovy script class
232 	 * @return the result object (either an instance of the script class
233 	 * or the result of running the script instance)
234 	 * @throws ScriptCompilationException in case of instantiation failure
235 	 */
executeScript(ScriptSource scriptSource, Class scriptClass)236 	protected Object executeScript(ScriptSource scriptSource, Class scriptClass) throws ScriptCompilationException {
237 		try {
238 			GroovyObject goo = (GroovyObject) scriptClass.newInstance();
239 
240 			if (this.groovyObjectCustomizer != null) {
241 				// Allow metaclass and other customization.
242 				this.groovyObjectCustomizer.customize(goo);
243 			}
244 
245 			if (goo instanceof Script) {
246 				// A Groovy script, probably creating an instance: let's execute it.
247 				return ((Script) goo).run();
248 			}
249 			else {
250 				// An instance of the scripted class: let's return it as-is.
251 				return goo;
252 			}
253 		}
254 		catch (InstantiationException ex) {
255 			throw new ScriptCompilationException(
256 					scriptSource, "Could not instantiate Groovy script class: " + scriptClass.getName(), ex);
257 		}
258 		catch (IllegalAccessException ex) {
259 			throw new ScriptCompilationException(
260 					scriptSource, "Could not access Groovy script constructor: " + scriptClass.getName(), ex);
261 		}
262 	}
263 
264 
265 	@Override
toString()266 	public String toString() {
267 		return "GroovyScriptFactory: script source locator [" + this.scriptSourceLocator + "]";
268 	}
269 
270 
271 	/**
272 	 * Wrapper that holds a temporarily cached result object.
273 	 */
274 	private static class CachedResultHolder {
275 
276 		public final Object object;
277 
CachedResultHolder(Object object)278 		public CachedResultHolder(Object object) {
279 			this.object = object;
280 		}
281 	}
282 
283 }
284