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 }