1 /*
2 * Copyright 2003-2009 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 package groovy.util;
17
18 import groovy.lang.Binding;
19 import groovy.lang.GroovyClassLoader;
20 import groovy.lang.Script;
21
22 import java.io;
23 import java.net.MalformedURLException;
24 import java.net.URL;
25 import java.net.URLConnection;
26 import java.security.AccessController;
27 import java.security.PrivilegedAction;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.Map;
31
32 import org.codehaus.groovy.control.CompilationFailedException;
33 import org.codehaus.groovy.runtime.InvokerHelper;
34
35 /**
36 * Specific script engine able to reload modified scripts as well as dealing properly with dependent scripts.
37 *
38 * @author sam
39 * @author Marc Palmer
40 * @author Guillaume Laforge
41 */
42 public class GroovyScriptEngine implements ResourceConnector {
43
44 /**
45 * Simple testing harness for the GSE. Enter script roots as arguments and
46 * then input script names to run them.
47 *
48 * @param urls an array of URLs
49 * @throws Exception if something goes wrong
50 */
51 public static void main(String[] urls) throws Exception {
52 GroovyScriptEngine gse = new GroovyScriptEngine(urls);
53 BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
54 String line;
55 while (true) {
56 System.out.print("groovy> ");
57 if ((line = br.readLine()) == null || line.equals("quit"))
58 break;
59 try {
60 System.out.println(gse.run(line, new Binding()));
61 } catch (Exception e) {
62 e.printStackTrace();
63 }
64 }
65 }
66
67 private URL[] roots;
68 private Map<String, ScriptCacheEntry> scriptCache = Collections.synchronizedMap(new HashMap<String, ScriptCacheEntry>());
69 private ResourceConnector rc;
70
71 private static ThreadLocal<ScriptCacheEntry> currentCacheEntryHolder = new ThreadLocal<ScriptCacheEntry>();
72 private GroovyClassLoader groovyLoader = null;
73
74 private static class ScriptCacheEntry {
75 private Class scriptClass;
76 private long lastModified;
77 private Map<URL, Long> dependencies = new HashMap<URL, Long>();
78 }
79
80 private class ScriptClassLoader extends GroovyClassLoader {
81 public ScriptClassLoader(ClassLoader loader) {
82 super(loader);
83 }
84
85 public ScriptClassLoader(GroovyClassLoader parent) {
86 super(parent);
87 }
88
89 protected Class findClass(String className) throws ClassNotFoundException {
90 String filename = className.replace('.', File.separatorChar) + ".groovy";
91 URLConnection dependentScriptConn = null;
92 try {
93 dependentScriptConn = rc.getResourceConnection(filename);
94 ScriptCacheEntry currentCacheEntry = currentCacheEntryHolder.get();
95 if (currentCacheEntry != null) {
96 currentCacheEntry.dependencies.put(dependentScriptConn.getURL(), dependentScriptConn.getLastModified());
97 }
98 return parseClass(dependentScriptConn.getInputStream(), filename);
99 } catch (ResourceException e1) {
100 throw new ClassNotFoundException("Could not read " + className + ": " + e1);
101 } catch (CompilationFailedException e2) {
102 throw new ClassNotFoundException("Syntax error in " + className + ": " + e2);
103 } catch (IOException e3) {
104 throw new ClassNotFoundException("Problem reading " + className + ": " + e3);
105 } finally {
106 try {
107 if (dependentScriptConn != null && dependentScriptConn.getInputStream() != null) {
108 dependentScriptConn.getInputStream().close();
109 }
110 } catch (IOException e) {
111 // IGNORE
112 }
113 }
114 }
115 }
116
117 /*
118 * Initialize a new GroovyClassLoader with the parentClassLoader passed as a parameter.
119 *
120 * @param parentClassLoader the class loader to use
121 */
122 private void initGroovyLoader(final ClassLoader parentClassLoader) {
123 if (groovyLoader == null || groovyLoader.getParent() != parentClassLoader) {
124 groovyLoader = AccessController.doPrivileged(new PrivilegedAction<GroovyClassLoader>() {
125 public GroovyClassLoader run() {
126 ScriptClassLoader loader;
127 if (parentClassLoader instanceof GroovyClassLoader)
128 loader = new ScriptClassLoader((GroovyClassLoader) parentClassLoader);
129 else
130 loader = new ScriptClassLoader(parentClassLoader);
131 return loader;
132 }
133 });
134 }
135 }
136
137 /**
138 * Get a resource connection as a <code>URLConnection</code> to retrieve a script
139 * from the <code>ResourceConnector</code>.
140 *
141 * @param resourceName name of the resource to be retrieved
142 * @return a URLConnection to the resource
143 * @throws ResourceException
144 */
145 public URLConnection getResourceConnection(String resourceName) throws ResourceException {
146 // Get the URLConnection
147 URLConnection groovyScriptConn = null;
148 ResourceException se = null;
149 for (URL root : roots) {
150 URL scriptURL = null;
151 try {
152 scriptURL = new URL(root, resourceName);
153 groovyScriptConn = scriptURL.openConnection();
154
155 // Make sure we can open it, if we can't it doesn't exist.
156 // Could be very slow if there are any non-file:// URLs in there
157 groovyScriptConn.getInputStream();
158 break; // Now this is a bit unusual
159 } catch (MalformedURLException e) {
160 String message = "Malformed URL: " + root + ", " + resourceName;
161 if (se == null) {
162 se = new ResourceException(message);
163 } else {
164 se = new ResourceException(message, se);
165 }
166 } catch (IOException e1) {
167 String message = "Cannot open URL: " + scriptURL;
168 if (se == null) {
169 se = new ResourceException(message);
170 } else {
171 se = new ResourceException(message, se);
172 }
173 }
174 }
175
176 // If we didn't find anything, report on all the exceptions that occurred.
177 if (groovyScriptConn == null) {
178 throw se;
179 }
180 return groovyScriptConn;
181 }
182
183 /**
184 * The groovy script engine will run groovy scripts and reload them and
185 * their dependencies when they are modified. This is useful for embedding
186 * groovy in other containers like games and application servers.
187 *
188 * @param roots This an array of URLs where Groovy scripts will be stored. They should
189 * be layed out using their package structure like Java classes
190 */
191 public GroovyScriptEngine(URL[] roots) {
192 this.roots = roots;
193 this.rc = this;
194 initGroovyLoader(getClass().getClassLoader());
195 }
196
197 public GroovyScriptEngine(URL[] roots, ClassLoader parentClassLoader) {
198 this(roots);
199 initGroovyLoader(parentClassLoader);
200 }
201
202 public GroovyScriptEngine(String[] urls) throws IOException {
203 roots = new URL[urls.length];
204 for (int i = 0; i < roots.length; i++) {
205 if (urls[i].indexOf("://") != -1) {
206 roots[i] = new URL(urls[i]);
207 } else {
208 roots[i] = new File(urls[i]).toURI().toURL();
209 }
210 }
211 this.rc = this;
212 initGroovyLoader(getClass().getClassLoader());
213 }
214
215 public GroovyScriptEngine(String[] urls, ClassLoader parentClassLoader) throws IOException {
216 this(urls);
217 initGroovyLoader(parentClassLoader);
218 }
219
220 public GroovyScriptEngine(String url) throws IOException {
221 this(new String[]{url});
222 }
223
224 public GroovyScriptEngine(String url, ClassLoader parentClassLoader) throws IOException {
225 this(url);
226 initGroovyLoader(parentClassLoader);
227 }
228
229 public GroovyScriptEngine(ResourceConnector rc) {
230 this.rc = rc;
231 initGroovyLoader(getClass().getClassLoader());
232 }
233
234 public GroovyScriptEngine(ResourceConnector rc, ClassLoader parentClassLoader) {
235 this(rc);
236 initGroovyLoader(parentClassLoader);
237 }
238
239 /**
240 * Get the <code>ClassLoader</code> that will serve as the parent ClassLoader of the
241 * {@link GroovyClassLoader} in which scripts will be executed. By default, this is the
242 * ClassLoader that loaded the <code>GroovyScriptEngine</code> class.
243 *
244 * @return parent classloader used to load scripts
245 */
246 public ClassLoader getParentClassLoader() {
247 return groovyLoader.getParent();
248 }
249
250 /**
251 * @param parentClassLoader ClassLoader to be used as the parent ClassLoader for scripts executed by the engine
252 * @deprecated
253 */
254 public void setParentClassLoader(ClassLoader parentClassLoader) {
255 if (parentClassLoader == null) {
256 throw new IllegalArgumentException("The parent class loader must not be null.");
257 }
258 initGroovyLoader(parentClassLoader);
259 }
260
261 /**
262 * Get the class of the scriptName in question, so that you can instantiate Groovy objects with caching and reloading.
263 *
264 * @param scriptName resource name pointing to the script
265 * @return the loaded scriptName as a compiled class
266 * @throws ResourceException if there is a problem accessing the script
267 * @throws ScriptException if there is a problem parsing the script
268 */
269 public Class loadScriptByName(String scriptName) throws ResourceException, ScriptException {
270 scriptName = scriptName.replace('.', File.separatorChar) + ".groovy";
271 ScriptCacheEntry entry = updateCacheEntry(scriptName);
272 return entry.scriptClass;
273 }
274
275 /**
276 * Get the class of the scriptName in question, so that you can instantiate Groovy objects with caching and reloading.
277 *
278 * @param scriptName resource name pointing to the script
279 * @param parentClassLoader the class loader to use when loading the script
280 * @return the loaded scriptName as a compiled class
281 * @throws ResourceException if there is a problem accessing the script
282 * @throws ScriptException if there is a problem parsing the script
283 * @deprecated
284 */
285 public Class loadScriptByName(String scriptName, ClassLoader parentClassLoader)
286 throws ResourceException, ScriptException {
287 initGroovyLoader(parentClassLoader);
288 return loadScriptByName(scriptName);
289 }
290
291 /**
292 * Locate the class and reload it or any of its dependencies
293 *
294 * @param scriptName resource name pointing to the script
295 * @return the cache entry for scriptName
296 * @throws ResourceException if there is a problem accessing the script
297 * @throws ScriptException if there is a problem parsing the script
298 */
299 private ScriptCacheEntry updateCacheEntry(String scriptName) throws ResourceException, ScriptException {
300 ScriptCacheEntry entry;
301
302 scriptName = scriptName.intern();
303 synchronized (scriptName) {
304
305 URLConnection scriptConn = rc.getResourceConnection(scriptName);
306
307 // URL last modified
308 long lastModified = scriptConn.getLastModified();
309 // Check the cache for the scriptName
310 entry = scriptCache.get(scriptName);
311
312 if (entry == null || entry.lastModified < lastModified || dependencyOutOfDate(entry)) {
313 ScriptCacheEntry cacheEntry = new ScriptCacheEntry();
314 currentCacheEntryHolder.set(cacheEntry);
315 cacheEntry.scriptClass = parseScript(scriptName, scriptConn);
316 cacheEntry.lastModified = lastModified;
317 scriptCache.put(scriptName, cacheEntry);
318 entry = cacheEntry;
319 cacheEntry = null;
320 } else {
321 forceClose(scriptConn);
322 }
323 }
324 return entry;
325 }
326
327 private Class parseScript(String scriptName, URLConnection scriptConn) throws ScriptException {
328 try {
329 InputStream in = scriptConn.getInputStream();
330 return groovyLoader.parseClass(in, scriptName);
331 } catch (Exception e) {
332 throw new ScriptException("Could not parse script: " + scriptName, e);
333 } finally {
334 currentCacheEntryHolder.set(null);
335 forceClose(scriptConn);
336 }
337 }
338
339 private boolean dependencyOutOfDate(ScriptCacheEntry cacheEntry) {
340 if (cacheEntry != null) {
341 for (URL url : cacheEntry.dependencies.keySet()) {
342 URLConnection urlc = null;
343 try {
344 urlc = url.openConnection();
345 urlc.setDoInput(false);
346 urlc.setDoOutput(false);
347 long dependentLastModified = urlc.getLastModified();
348 if (dependentLastModified > cacheEntry.dependencies.get(url)) {
349 return true;
350 }
351 } catch (IOException ioe) {
352 return true;
353 } finally {
354 forceClose(urlc);
355 }
356 }
357 }
358 return false;
359 }
360
361 /**
362 * This method closes a {@link URLConnection} by getting its {@link InputStream} and calling the
363 * {@link InputStream#close()} method on it. The {@link URLConnection} doesn't have a close() method
364 * and relies on garbage collection to close the underlying connection to the file.
365 * Relying on garbage collection could lead to the application exhausting the number of files the
366 * user is allowed to have open at any one point in time and cause the application to crash
367 * ({@link FileNotFoundException} (Too many open files)).
368 * Hence the need for this method to explicitly close the underlying connection to the file.
369 *
370 * @param urlConnection the {@link URLConnection} to be "closed" to close the underlying file descriptors.
371 */
372 private void forceClose(URLConnection urlConnection) {
373 if (urlConnection != null) {
374 // We need to get the input stream and close it to force the open
375 // file descriptor to be released. Otherwise, we will reach the limit
376 // for number of files open at one time.
377
378 InputStream in = null;
379 try {
380 in = urlConnection.getInputStream();
381 } catch (Exception e) {
382 // Do nothing: We were not going to use it anyway.
383 } finally {
384 if (in != null) {
385 try {
386 in.close();
387 } catch (IOException e) {
388 // Do nothing: Just want to make sure it is closed.
389 }
390 }
391 }
392 }
393 }
394
395 /**
396 * Run a script identified by name with a single argument.
397 *
398 * @param scriptName name of the script to run
399 * @param argument a single argument passed as a variable named <code>arg</code> in the binding
400 * @return a <code>toString()</code> representation of the result of the execution of the script
401 * @throws ResourceException if there is a problem accessing the script
402 * @throws ScriptException if there is a problem parsing the script
403 */
404 public String run(String scriptName, String argument) throws ResourceException, ScriptException {
405 Binding binding = new Binding();
406 binding.setVariable("arg", argument);
407 Object result = run(scriptName, binding);
408 return result == null ? "" : result.toString();
409 }
410
411 /**
412 * Run a script identified by name with a given binding.
413 *
414 * @param scriptName name of the script to run
415 * @param binding the binding to pass to the script
416 * @return an object
417 * @throws ResourceException if there is a problem accessing the script
418 * @throws ScriptException if there is a problem parsing the script
419 */
420 public Object run(String scriptName, Binding binding) throws ResourceException, ScriptException {
421 return createScript(scriptName, binding).run();
422 }
423
424 /**
425 * Creates a Script with a given scriptName and binding.
426 *
427 * @param scriptName name of the script to run
428 * @param binding the binding to pass to the script
429 * @return the script object
430 * @throws ResourceException if there is a problem accessing the script
431 * @throws ScriptException if there is a problem parsing the script
432 */
433 public Script createScript(String scriptName, Binding binding) throws ResourceException, ScriptException {
434 ScriptCacheEntry entry = updateCacheEntry(scriptName);
435 scriptName = scriptName.intern();
436 return InvokerHelper.createScript(entry.scriptClass, binding);
437 }
438
439 /**
440 * Returns the GroovyClassLoader associated with this script engine instance.
441 * Useful if you need to pass the class loader to another library.
442 *
443 * @return the GroovyClassLoader
444 */
445 public GroovyClassLoader getGroovyClassLoader() {
446 return groovyLoader;
447 }
448 }