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 }