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(System.in));
080 String line;
081 while (true) {
082 System.out.print("groovy> ");
083 if ((line = br.readLine()) == null || line.equals("quit"))
084 break;
085 try {
086 System.out.println(gse.run(line, new Binding()));
087 } catch (Exception e) {
088 e.printStackTrace();
089 }
090 }
091 }
092
093 private URL[] roots;
094 private Map scriptCache = Collections.synchronizedMap(new HashMap());
095 private ResourceConnector rc;
096 private ClassLoader parentClassLoader = getClass().getClassLoader();
097
098 private static class ScriptCacheEntry {
099 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 }