Save This Page
Home » groovy-src-1.6.5 » groovy » util » [javadoc | source]
    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   }

Save This Page
Home » groovy-src-1.6.5 » groovy » util » [javadoc | source]