1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package groovy.util;
35
36 import groovy.lang.Binding;
37 import groovy.lang.GroovyClassLoader;
38 import groovy.lang.Script;
39
40 import java.io.BufferedReader;
41 import java.io.File;
42 import java.io.IOException;
43 import java.io.InputStreamReader;
44 import java.net.MalformedURLException;
45 import java.net.URL;
46 import java.net.URLConnection;
47 import java.security.AccessController;
48 import java.security.PrivilegedAction;
49 import java.util.Collections;
50 import java.util.HashMap;
51 import java.util.Iterator;
52 import java.util.Map;
53
54 import org.codehaus.groovy.control.CompilationFailedException;
55 import org.codehaus.groovy.runtime.InvokerHelper;
56
57 /***
58 * Specific script engine able to reload modified scripts as well as dealing properly with dependent scripts.
59 *
60 * @author sam
61 * @author Marc Palmer
62 * @author Guillaume Laforge
63 */
64 public class GroovyScriptEngine implements ResourceConnector {
65
66 /***
67 * Simple testing harness for the GSE. Enter script roots as arguments and
68 * then input script names to run them.
69 *
70 * @param urls
71 * @throws Exception
72 */
73 public static void main(String[] urls) throws Exception {
74 URL[] roots = new URL[urls.length];
75 for (int i = 0; i < roots.length; i++) {
76 roots[i] = new File(urls[i]).toURL();
77 }
78 GroovyScriptEngine gse = new GroovyScriptEngine(roots);
79 BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
80 String line;
81 while (true) {
82 System.out.print("groovy> ");
83 if ((line = br.readLine()) == null || line.equals("quit"))
84 break;
85 try {
86 System.out.println(gse.run(line, new Binding()));
87 } catch (Exception e) {
88 e.printStackTrace();
89 }
90 }
91 }
92
93 private URL[] roots;
94 private Map scriptCache = Collections.synchronizedMap(new HashMap());
95 private ResourceConnector rc;
96 private ClassLoader parentClassLoader = getClass().getClassLoader();
97
98 private static class ScriptCacheEntry {
99 private Class scriptClass;
100 private long lastModified;
101 private Map dependencies = new HashMap();
102 }
103
104 /***
105 * Get a resource connection as a <code>URLConnection</code> to retrieve a script
106 * from the <code>ResourceConnector</code>
107 *
108 * @param resourceName name of the resource to be retrieved
109 * @return a URLConnection to the resource
110 * @throws ResourceException
111 */
112 public URLConnection getResourceConnection(String resourceName) throws ResourceException {
113
114 URLConnection groovyScriptConn = null;
115
116 ResourceException se = null;
117 for (int i = 0; i < roots.length; i++) {
118 URL scriptURL = null;
119 try {
120 scriptURL = new URL(roots[i], resourceName);
121
122 groovyScriptConn = scriptURL.openConnection();
123
124
125
126 groovyScriptConn.getInputStream();
127
128 break;
129
130 } catch (MalformedURLException e) {
131 String message = "Malformed URL: " + roots[i] + ", " + resourceName;
132 if (se == null) {
133 se = new ResourceException(message);
134 } else {
135 se = new ResourceException(message, se);
136 }
137 } catch (IOException e1) {
138 String message = "Cannot open URL: " + scriptURL;
139 if (se == null) {
140 se = new ResourceException(message);
141 } else {
142 se = new ResourceException(message, se);
143 }
144 }
145 }
146
147
148 if (groovyScriptConn == null) {
149 throw se;
150 }
151
152 return groovyScriptConn;
153 }
154
155 /***
156 * The groovy script engine will run groovy scripts and reload them and
157 * their dependencies when they are modified. This is useful for embedding
158 * groovy in other containers like games and application servers.
159 *
160 * @param roots This an array of URLs where Groovy scripts will be stored. They should
161 * be layed out using their package structure like Java classes
162 */
163 public GroovyScriptEngine(URL[] roots) {
164 this.roots = roots;
165 this.rc = this;
166 }
167
168 public GroovyScriptEngine(URL[] roots, ClassLoader parentClassLoader) {
169 this(roots);
170 this.parentClassLoader = parentClassLoader;
171 }
172
173 public GroovyScriptEngine(String[] urls) throws IOException {
174 roots = new URL[urls.length];
175 for (int i = 0; i < roots.length; i++) {
176 roots[i] = new File(urls[i]).toURL();
177 }
178 this.rc = this;
179 }
180
181 public GroovyScriptEngine(String[] urls, ClassLoader parentClassLoader) throws IOException {
182 this(urls);
183 this.parentClassLoader = parentClassLoader;
184 }
185
186 public GroovyScriptEngine(String url) throws IOException {
187 roots = new URL[1];
188 roots[0] = new File(url).toURL();
189 this.rc = this;
190 }
191
192 public GroovyScriptEngine(String url, ClassLoader parentClassLoader) throws IOException {
193 this(url);
194 this.parentClassLoader = parentClassLoader;
195 }
196
197 public GroovyScriptEngine(ResourceConnector rc) {
198 this.rc = rc;
199 }
200
201 public GroovyScriptEngine(ResourceConnector rc, ClassLoader parentClassLoader) {
202 this(rc);
203 this.parentClassLoader = parentClassLoader;
204 }
205
206 /***
207 * Get the <code>ClassLoader</code> that will serve as the parent ClassLoader of the
208 * {@link GroovyClassLoader} in which scripts will be executed. By default, this is the
209 * ClassLoader that loaded the <code>GroovyScriptEngine</code> class.
210 *
211 * @return parent classloader used to load scripts
212 */
213 public ClassLoader getParentClassLoader() {
214 return parentClassLoader;
215 }
216
217 /***
218 * @param parentClassLoader ClassLoader to be used as the parent ClassLoader for scripts executed by the engine
219 */
220 public void setParentClassLoader(ClassLoader parentClassLoader) {
221 if (parentClassLoader == null) {
222 throw new IllegalArgumentException("The parent class loader must not be null.");
223 }
224 this.parentClassLoader = parentClassLoader;
225 }
226
227 /***
228 * Get the class of the scriptName in question, so that you can instantiate Groovy objects with caching and reloading.
229 *
230 * @param scriptName
231 * @return the loaded scriptName as a compiled class
232 * @throws ResourceException
233 * @throws ScriptException
234 */
235 public Class loadScriptByName(String scriptName) throws ResourceException, ScriptException {
236 return loadScriptByName( scriptName, getClass().getClassLoader());
237 }
238
239
240 /***
241 * Get the class of the scriptName in question, so that you can instantiate Groovy objects with caching and reloading.
242 *
243 * @param scriptName
244 * @return the loaded scriptName as a compiled class
245 * @throws ResourceException
246 * @throws ScriptException
247 */
248 public Class loadScriptByName(String scriptName, ClassLoader parentClassLoader)
249 throws ResourceException, ScriptException {
250 scriptName = scriptName.replace('.', File.separatorChar) + ".groovy";
251 ScriptCacheEntry entry = updateCacheEntry(scriptName, parentClassLoader);
252 return entry.scriptClass;
253 }
254
255 /***
256 * Locate the class and reload it or any of its dependencies
257 *
258 * @param scriptName
259 * @param parentClassLoader
260 * @return the scriptName cache entry
261 * @throws ResourceException
262 * @throws ScriptException
263 */
264 private ScriptCacheEntry updateCacheEntry(String scriptName, final ClassLoader parentClassLoader)
265 throws ResourceException, ScriptException
266 {
267 ScriptCacheEntry entry;
268
269 scriptName = scriptName.intern();
270 synchronized (scriptName) {
271
272 URLConnection groovyScriptConn = rc.getResourceConnection(scriptName);
273
274
275 long lastModified = groovyScriptConn.getLastModified();
276
277 entry = (ScriptCacheEntry) scriptCache.get(scriptName);
278
279
280 boolean dependencyOutOfDate = false;
281 if (entry != null) {
282
283 for (Iterator i = entry.dependencies.keySet().iterator(); i.hasNext();) {
284 URLConnection urlc = null;
285 URL url = (URL) i.next();
286 try {
287 urlc = url.openConnection();
288 urlc.setDoInput(false);
289 urlc.setDoOutput(false);
290 long dependentLastModified = urlc.getLastModified();
291 if (dependentLastModified > ((Long) entry.dependencies.get(url)).longValue()) {
292 dependencyOutOfDate = true;
293 break;
294 }
295 } catch (IOException ioe) {
296 dependencyOutOfDate = true;
297 break;
298 }
299 }
300 }
301
302 if (entry == null || entry.lastModified < lastModified || dependencyOutOfDate) {
303
304 entry = new ScriptCacheEntry();
305
306
307 final ScriptCacheEntry finalEntry = entry;
308
309
310 GroovyClassLoader groovyLoader =
311 (GroovyClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
312 public Object run() {
313 return new GroovyClassLoader(parentClassLoader) {
314 protected Class findClass(String className) throws ClassNotFoundException {
315 String filename = className.replace('.', File.separatorChar) + ".groovy";
316 URLConnection dependentScriptConn = null;
317 try {
318 dependentScriptConn = rc.getResourceConnection(filename);
319 finalEntry.dependencies.put(
320 dependentScriptConn.getURL(),
321 new Long(dependentScriptConn.getLastModified()));
322 } catch (ResourceException e1) {
323 throw new ClassNotFoundException("Could not read " + className + ": " + e1);
324 }
325 try {
326 return parseClass(dependentScriptConn.getInputStream(), filename);
327 } catch (CompilationFailedException e2) {
328 throw new ClassNotFoundException("Syntax error in " + className + ": " + e2);
329 } catch (IOException e2) {
330 throw new ClassNotFoundException("Problem reading " + className + ": " + e2);
331 }
332 }
333 };
334 }
335 });
336
337 try {
338 entry.scriptClass = groovyLoader.parseClass(groovyScriptConn.getInputStream(), scriptName);
339 } catch (Exception e) {
340 throw new ScriptException("Could not parse scriptName: " + scriptName, e);
341 }
342 entry.lastModified = lastModified;
343 scriptCache.put(scriptName, entry);
344 }
345 }
346 return entry;
347 }
348
349 /***
350 * Run a script identified by name.
351 *
352 * @param scriptName name of the script to run
353 * @param argument a single argument passed as a variable named <code>arg</code> in the binding
354 * @return a <code>toString()</code> representation of the result of the execution of the script
355 * @throws ResourceException
356 * @throws ScriptException
357 */
358 public String run(String scriptName, String argument) throws ResourceException, ScriptException {
359 Binding binding = new Binding();
360 binding.setVariable("arg", argument);
361 Object result = run(scriptName, binding);
362 return result == null ? "" : result.toString();
363 }
364
365 /***
366 * Run a script identified by name.
367 *
368 * @param scriptName name of the script to run
369 * @param binding binding to pass to the script
370 * @return an object
371 * @throws ResourceException
372 * @throws ScriptException
373 */
374 public Object run(String scriptName, Binding binding) throws ResourceException, ScriptException {
375
376 ScriptCacheEntry entry = updateCacheEntry(scriptName, getParentClassLoader());
377 Script scriptObject = InvokerHelper.createScript(entry.scriptClass, binding);
378 return scriptObject.run();
379 }
380 }