001: /*
002: * $Id: GroovyScriptEngine.java 3669 2006-02-26 22:11:48Z glaforge $version Jan 9, 2004 12:19:58 PM $user Exp $
003: *
004: * Copyright 2003 (C) Sam Pullara. All Rights Reserved.
005: *
006: * Redistribution and use of this software and associated documentation
007: * ("Software"), with or without modification, are permitted provided that the
008: * following conditions are met: 1. Redistributions of source code must retain
009: * copyright statements and notices. Redistributions must also contain a copy
010: * of this document. 2. Redistributions in binary form must reproduce the above
011: * copyright notice, this list of conditions and the following disclaimer in
012: * the documentation and/or other materials provided with the distribution. 3.
013: * The name "groovy" must not be used to endorse or promote products derived
014: * from this Software without prior written permission of The Codehaus. For
015: * written permission, please contact info@codehaus.org. 4. Products derived
016: * from this Software may not be called "groovy" nor may "groovy" appear in
017: * their names without prior written permission of The Codehaus. "groovy" is a
018: * registered trademark of The Codehaus. 5. Due credit should be given to The
019: * Codehaus - http://groovy.codehaus.org/
020: *
021: * THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS ``AS IS'' AND ANY
022: * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
023: * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
024: * DISCLAIMED. IN NO EVENT SHALL THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR
025: * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
026: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
027: * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
028: * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
029: * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
030: * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
031: * DAMAGE.
032: *
033: */
034: package groovy.util;
035:
036: import groovy.lang.Binding;
037: import groovy.lang.GroovyClassLoader;
038: import groovy.lang.Script;
039:
040: import java.io.BufferedReader;
041: import java.io.File;
042: import java.io.IOException;
043: import java.io.InputStreamReader;
044: import java.net.MalformedURLException;
045: import java.net.URL;
046: import java.net.URLConnection;
047: import java.security.AccessController;
048: import java.security.PrivilegedAction;
049: import java.util.Collections;
050: import java.util.HashMap;
051: import java.util.Iterator;
052: import java.util.Map;
053:
054: import org.codehaus.groovy.control.CompilationFailedException;
055: import org.codehaus.groovy.runtime.InvokerHelper;
056:
057: /**
058: * Specific script engine able to reload modified scripts as well as dealing properly with dependent scripts.
059: *
060: * @author sam
061: * @author Marc Palmer
062: * @author Guillaume Laforge
063: */
064: public class GroovyScriptEngine implements ResourceConnector {
065:
066: /**
067: * Simple testing harness for the GSE. Enter script roots as arguments and
068: * then input script names to run them.
069: *
070: * @param urls
071: * @throws Exception
072: */
073: public static void main(String[] urls) throws Exception {
074: URL[] roots = new URL[urls.length];
075: for (int i = 0; i < roots.length; i++) {
076: roots[i] = new File(urls[i]).toURL();
077: }
078: GroovyScriptEngine gse = new GroovyScriptEngine(roots);
079: BufferedReader br = new BufferedReader(new InputStreamReader(
080: System.in));
081: String line;
082: while (true) {
083: System.out.print("groovy> ");
084: if ((line = br.readLine()) == null || line.equals("quit"))
085: break;
086: try {
087: System.out.println(gse.run(line, new Binding()));
088: } catch (Exception e) {
089: e.printStackTrace();
090: }
091: }
092: }
093:
094: private URL[] roots;
095: private Map scriptCache = Collections
096: .synchronizedMap(new HashMap());
097: private ResourceConnector rc;
098: private ClassLoader parentClassLoader = getClass().getClassLoader();
099:
100: private static class ScriptCacheEntry {
101: private Class scriptClass;
102: private long lastModified;
103: private Map dependencies = new HashMap();
104: }
105:
106: /**
107: * Get a resource connection as a <code>URLConnection</code> to retrieve a script
108: * from the <code>ResourceConnector</code>
109: *
110: * @param resourceName name of the resource to be retrieved
111: * @return a URLConnection to the resource
112: * @throws ResourceException
113: */
114: public URLConnection getResourceConnection(String resourceName)
115: throws ResourceException {
116: // Get the URLConnection
117: URLConnection groovyScriptConn = null;
118:
119: ResourceException se = null;
120: for (int i = 0; i < roots.length; i++) {
121: URL scriptURL = null;
122: try {
123: scriptURL = new URL(roots[i], resourceName);
124:
125: groovyScriptConn = scriptURL.openConnection();
126:
127: // Make sure we can open it, if we can't it doesn't exist.
128: // Could be very slow if there are any non-file:// URLs in there
129: groovyScriptConn.getInputStream();
130:
131: break; // Now this is a bit unusual
132:
133: } catch (MalformedURLException e) {
134: String message = "Malformed URL: " + roots[i] + ", "
135: + resourceName;
136: if (se == null) {
137: se = new ResourceException(message);
138: } else {
139: se = new ResourceException(message, se);
140: }
141: } catch (IOException e1) {
142: String message = "Cannot open URL: " + scriptURL;
143: if (se == null) {
144: se = new ResourceException(message);
145: } else {
146: se = new ResourceException(message, se);
147: }
148: }
149: }
150:
151: // If we didn't find anything, report on all the exceptions that occurred.
152: if (groovyScriptConn == null) {
153: throw se;
154: }
155:
156: return groovyScriptConn;
157: }
158:
159: /**
160: * The groovy script engine will run groovy scripts and reload them and
161: * their dependencies when they are modified. This is useful for embedding
162: * groovy in other containers like games and application servers.
163: *
164: * @param roots This an array of URLs where Groovy scripts will be stored. They should
165: * be layed out using their package structure like Java classes
166: */
167: public GroovyScriptEngine(URL[] roots) {
168: this .roots = roots;
169: this .rc = this ;
170: }
171:
172: public GroovyScriptEngine(URL[] roots, ClassLoader parentClassLoader) {
173: this (roots);
174: this .parentClassLoader = parentClassLoader;
175: }
176:
177: public GroovyScriptEngine(String[] urls) throws IOException {
178: roots = new URL[urls.length];
179: for (int i = 0; i < roots.length; i++) {
180: roots[i] = new File(urls[i]).toURL();
181: }
182: this .rc = this ;
183: }
184:
185: public GroovyScriptEngine(String[] urls,
186: ClassLoader parentClassLoader) throws IOException {
187: this (urls);
188: this .parentClassLoader = parentClassLoader;
189: }
190:
191: public GroovyScriptEngine(String url) throws IOException {
192: roots = new URL[1];
193: roots[0] = new File(url).toURL();
194: this .rc = this ;
195: }
196:
197: public GroovyScriptEngine(String url, ClassLoader parentClassLoader)
198: throws IOException {
199: this (url);
200: this .parentClassLoader = parentClassLoader;
201: }
202:
203: public GroovyScriptEngine(ResourceConnector rc) {
204: this .rc = rc;
205: }
206:
207: public GroovyScriptEngine(ResourceConnector rc,
208: ClassLoader parentClassLoader) {
209: this (rc);
210: this .parentClassLoader = parentClassLoader;
211: }
212:
213: /**
214: * Get the <code>ClassLoader</code> that will serve as the parent ClassLoader of the
215: * {@link GroovyClassLoader} in which scripts will be executed. By default, this is the
216: * ClassLoader that loaded the <code>GroovyScriptEngine</code> class.
217: *
218: * @return parent classloader used to load scripts
219: */
220: public ClassLoader getParentClassLoader() {
221: return parentClassLoader;
222: }
223:
224: /**
225: * @param parentClassLoader ClassLoader to be used as the parent ClassLoader for scripts executed by the engine
226: */
227: public void setParentClassLoader(ClassLoader parentClassLoader) {
228: if (parentClassLoader == null) {
229: throw new IllegalArgumentException(
230: "The parent class loader must not be null.");
231: }
232: this .parentClassLoader = parentClassLoader;
233: }
234:
235: /**
236: * Get the class of the scriptName in question, so that you can instantiate Groovy objects with caching and reloading.
237: *
238: * @param scriptName
239: * @return the loaded scriptName as a compiled class
240: * @throws ResourceException
241: * @throws ScriptException
242: */
243: public Class loadScriptByName(String scriptName)
244: throws ResourceException, ScriptException {
245: return loadScriptByName(scriptName, getClass().getClassLoader());
246: }
247:
248: /**
249: * Get the class of the scriptName in question, so that you can instantiate Groovy objects with caching and reloading.
250: *
251: * @param scriptName
252: * @return the loaded scriptName as a compiled class
253: * @throws ResourceException
254: * @throws ScriptException
255: */
256: public Class loadScriptByName(String scriptName,
257: ClassLoader parentClassLoader) throws ResourceException,
258: ScriptException {
259: scriptName = scriptName.replace('.', File.separatorChar)
260: + ".groovy";
261: ScriptCacheEntry entry = updateCacheEntry(scriptName,
262: parentClassLoader);
263: return entry.scriptClass;
264: }
265:
266: /**
267: * Locate the class and reload it or any of its dependencies
268: *
269: * @param scriptName
270: * @param parentClassLoader
271: * @return the scriptName cache entry
272: * @throws ResourceException
273: * @throws ScriptException
274: */
275: private ScriptCacheEntry updateCacheEntry(String scriptName,
276: final ClassLoader parentClassLoader)
277: throws ResourceException, ScriptException {
278: ScriptCacheEntry entry;
279:
280: scriptName = scriptName.intern();
281: synchronized (scriptName) {
282:
283: URLConnection groovyScriptConn = rc
284: .getResourceConnection(scriptName);
285:
286: // URL last modified
287: long lastModified = groovyScriptConn.getLastModified();
288: // Check the cache for the scriptName
289: entry = (ScriptCacheEntry) scriptCache.get(scriptName);
290: // If the entry isn't null check all the dependencies
291:
292: boolean dependencyOutOfDate = false;
293: if (entry != null) {
294:
295: for (Iterator i = entry.dependencies.keySet()
296: .iterator(); i.hasNext();) {
297: URLConnection urlc = null;
298: URL url = (URL) i.next();
299: try {
300: urlc = url.openConnection();
301: urlc.setDoInput(false);
302: urlc.setDoOutput(false);
303: long dependentLastModified = urlc
304: .getLastModified();
305: if (dependentLastModified > ((Long) entry.dependencies
306: .get(url)).longValue()) {
307: dependencyOutOfDate = true;
308: break;
309: }
310: } catch (IOException ioe) {
311: dependencyOutOfDate = true;
312: break;
313: }
314: }
315: }
316:
317: if (entry == null || entry.lastModified < lastModified
318: || dependencyOutOfDate) {
319: // Make a new entry
320: entry = new ScriptCacheEntry();
321:
322: // Closure variable
323: final ScriptCacheEntry finalEntry = entry;
324:
325: // Compile the scriptName into an object
326: GroovyClassLoader groovyLoader = (GroovyClassLoader) AccessController
327: .doPrivileged(new PrivilegedAction() {
328: public Object run() {
329: return new GroovyClassLoader(
330: parentClassLoader) {
331: protected Class findClass(
332: String className)
333: throws ClassNotFoundException {
334: String filename = className
335: .replace(
336: '.',
337: File.separatorChar)
338: + ".groovy";
339: URLConnection dependentScriptConn = null;
340: try {
341: dependentScriptConn = rc
342: .getResourceConnection(filename);
343: finalEntry.dependencies
344: .put(
345: dependentScriptConn
346: .getURL(),
347: new Long(
348: dependentScriptConn
349: .getLastModified()));
350: } catch (ResourceException e1) {
351: throw new ClassNotFoundException(
352: "Could not read "
353: + className
354: + ": " + e1);
355: }
356: try {
357: return parseClass(
358: dependentScriptConn
359: .getInputStream(),
360: filename);
361: } catch (CompilationFailedException e2) {
362: throw new ClassNotFoundException(
363: "Syntax error in "
364: + className
365: + ": " + e2);
366: } catch (IOException e2) {
367: throw new ClassNotFoundException(
368: "Problem reading "
369: + className
370: + ": " + e2);
371: }
372: }
373: };
374: }
375: });
376:
377: try {
378: entry.scriptClass = groovyLoader.parseClass(
379: groovyScriptConn.getInputStream(),
380: scriptName);
381: } catch (Exception e) {
382: throw new ScriptException(
383: "Could not parse scriptName: " + scriptName,
384: e);
385: }
386: entry.lastModified = lastModified;
387: scriptCache.put(scriptName, entry);
388: }
389: }
390: return entry;
391: }
392:
393: /**
394: * Run a script identified by name.
395: *
396: * @param scriptName name of the script to run
397: * @param argument a single argument passed as a variable named <code>arg</code> in the binding
398: * @return a <code>toString()</code> representation of the result of the execution of the script
399: * @throws ResourceException
400: * @throws ScriptException
401: */
402: public String run(String scriptName, String argument)
403: throws ResourceException, ScriptException {
404: Binding binding = new Binding();
405: binding.setVariable("arg", argument);
406: Object result = run(scriptName, binding);
407: return result == null ? "" : result.toString();
408: }
409:
410: /**
411: * Run a script identified by name.
412: *
413: * @param scriptName name of the script to run
414: * @param binding binding to pass to the script
415: * @return an object
416: * @throws ResourceException
417: * @throws ScriptException
418: */
419: public Object run(String scriptName, Binding binding)
420: throws ResourceException, ScriptException {
421:
422: ScriptCacheEntry entry = updateCacheEntry(scriptName,
423: getParentClassLoader());
424: Script scriptObject = InvokerHelper.createScript(
425: entry.scriptClass, binding);
426: return scriptObject.run();
427: }
428: }
|