View Javadoc

1   /*
2    * $Id: GroovyScriptEngine.java,v 1.14 2006/02/26 22:11:48 glaforge Exp $version Jan 9, 2004 12:19:58 PM $user Exp $
3    * 
4    * Copyright 2003 (C) Sam Pullara. All Rights Reserved.
5    * 
6    * Redistribution and use of this software and associated documentation
7    * ("Software"), with or without modification, are permitted provided that the
8    * following conditions are met: 1. Redistributions of source code must retain
9    * copyright statements and notices. Redistributions must also contain a copy
10   * of this document. 2. Redistributions in binary form must reproduce the above
11   * copyright notice, this list of conditions and the following disclaimer in
12   * the documentation and/or other materials provided with the distribution. 3.
13   * The name "groovy" must not be used to endorse or promote products derived
14   * from this Software without prior written permission of The Codehaus. For
15   * written permission, please contact info@codehaus.org. 4. Products derived
16   * from this Software may not be called "groovy" nor may "groovy" appear in
17   * their names without prior written permission of The Codehaus. "groovy" is a
18   * registered trademark of The Codehaus. 5. Due credit should be given to The
19   * Codehaus - http://groovy.codehaus.org/
20   * 
21   * THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS ``AS IS'' AND ANY
22   * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24   * DISCLAIMED. IN NO EVENT SHALL THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR
25   * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26   * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29   * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
30   * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
31   * DAMAGE.
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         // Get the URLConnection
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                 // Make sure we can open it, if we can't it doesn't exist.
125                 // Could be very slow if there are any non-file:// URLs in there
126                 groovyScriptConn.getInputStream();
127 
128                 break; // Now this is a bit unusual
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         // If we didn't find anything, report on all the exceptions that occurred.
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             // URL last modified
275             long lastModified = groovyScriptConn.getLastModified();
276             // Check the cache for the scriptName
277             entry = (ScriptCacheEntry) scriptCache.get(scriptName);
278             // If the entry isn't null check all the dependencies
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                 // Make a new entry
304                 entry = new ScriptCacheEntry();
305 
306                 // Closure variable
307                 final ScriptCacheEntry finalEntry = entry;
308 
309                 // Compile the scriptName into an object
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 }