View Javadoc

    1   /*
    2    $Id: GroovyShell.java,v 1.52 2006/05/30 18:14:45 blackdrag Exp $
    3
    4    Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
    5
    6    Redistribution and use of this software and associated documentation
    7    ("Software"), with or without modification, are permitted provided
    8    that the following conditions are met:
    9
    10   1. Redistributions of source code must retain copyright
    11      statements and notices.  Redistributions must also contain a
    12      copy of this document.
    13
    14   2. Redistributions in binary form must reproduce the
    15      above copyright notice, this list of conditions and the
    16      following disclaimer in the documentation and/or other
    17      materials provided with the distribution.
    18
    19   3. The name "groovy" must not be used to endorse or promote
    20      products derived from this Software without prior written
    21      permission of The Codehaus.  For written permission,
    22      please contact info@codehaus.org.
    23
    24   4. Products derived from this Software may not be called "groovy"
    25      nor may "groovy" appear in their names without prior written
    26      permission of The Codehaus. "groovy" is a registered
    27      trademark of The Codehaus.
    28
    29   5. Due credit should be given to The Codehaus -
    30      http://groovy.codehaus.org/
    31
    32   THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS
    33   ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
    34   NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
    35   FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
    36   THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
    37   INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    38   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    39   SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
    40   HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
    41   STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    42   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
    43   OF THE POSSIBILITY OF SUCH DAMAGE.
    44
    45   */
    46  package groovy.lang;
    47
    48  import groovy.ui.GroovyMain;
    49
    50  import org.codehaus.groovy.control.CompilationFailedException;
    51  import org.codehaus.groovy.control.CompilerConfiguration;
    52  import org.codehaus.groovy.runtime.InvokerHelper;
    53
    54  import java.io.*;
    55  import java.lang.reflect.Constructor;
    56  import java.security.AccessController;
    57  import java.security.PrivilegedAction;
    58  import java.security.PrivilegedActionException;
    59  import java.security.PrivilegedExceptionAction;
    60  import java.util.List;
    61  import java.util.Map;
    62
    63  /***
    64   * Represents a groovy shell capable of running arbitrary groovy scripts
    65   *
    66   * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
    67   * @author Guillaume Laforge
    68   * @version $Revision: 1.52 $
    69   */
    70  public class GroovyShell extends GroovyObjectSupport {
    71
    72      public static final String[] EMPTY_ARGS = {};
    73
    74
    75      private Binding context;
    76      private int counter;
    77      private CompilerConfiguration config;
    78      private GroovyClassLoader loader;
    79
    80      public static void main(String[] args) {
    81          GroovyMain.main(args);
    82      }
    83
    84      public GroovyShell() {
    85          this(null, new Binding());
    86      }
    87
    88      public GroovyShell(Binding binding) {
    89          this(null, binding);
    90      }
    91
    92      public GroovyShell(CompilerConfiguration config) {
    93          this(new Binding(), config);
    94      }
    95
    96      public GroovyShell(Binding binding, CompilerConfiguration config) {
    97          this(null, binding, config);
    98      }
    99
    100     public GroovyShell(ClassLoader parent, Binding binding) {
    101         this(parent, binding, CompilerConfiguration.DEFAULT);
    102     }
    103
    104     public GroovyShell(ClassLoader parent) {
    105         this(parent, new Binding(), CompilerConfiguration.DEFAULT);
    106     }
    107
    108     public GroovyShell(ClassLoader parent, Binding binding, final CompilerConfiguration config) {
    109         if (binding == null) {
    110             throw new IllegalArgumentException("Binding must not be null.");
    111         }
    112         if (config == null) {
    113             throw new IllegalArgumentException("Compiler configuration must not be null.");
    114         }
    115         final ClassLoader parentLoader = (parent!=null)?parent:GroovyShell.class.getClassLoader();
    116         this.loader = (GroovyClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
    117             public Object run() {
    118                 return new GroovyClassLoader(parentLoader,config);
    119             }
    120         });
    121         this.context = binding;
    122         this.config = config;
    123     }
    124
    125     public void initialiseBinding() {
    126         Map map = context.getVariables();
    127         if (map.get("shell")==null) map.put("shell",this);
    128     }
    129
    130     public void resetLoadedClasses() {
    131         loader.clearCache();
    132     }
    133
    134     /***
    135      * Creates a child shell using a new ClassLoader which uses the parent shell's
    136      * class loader as its parent
    137      *
    138      * @param shell is the parent shell used for the variable bindings and the parent class loader
    139      */
    140     public GroovyShell(GroovyShell shell) {
    141         this(shell.loader, shell.context);
    142     }
    143
    144     public Binding getContext() {
    145         return context;
    146     }
    147
    148     public Object getProperty(String property) {
    149         Object answer = getVariable(property);
    150         if (answer == null) {
    151             answer = super.getProperty(property);
    152         }
    153         return answer;
    154     }
    155
    156     public void setProperty(String property, Object newValue) {
    157         setVariable(property, newValue);
    158         try {
    159             super.setProperty(property, newValue);
    160         } catch (GroovyRuntimeException e) {
    161             // ignore, was probably a dynamic property
    162         }
    163     }
    164
    165     /***
    166      * A helper method which runs the given script file with the given command line arguments
    167      *
    168      * @param scriptFile the file of the script to run
    169      * @param list       the command line arguments to pass in
    170      */
    171     public Object run(File scriptFile, List list) throws CompilationFailedException, IOException {
    172         String[] args = new String[list.size()];
    173         return run(scriptFile, (String[]) list.toArray(args));
    174     }
    175
    176     /***
    177      * A helper method which runs the given cl script with the given command line arguments
    178      *
    179      * @param scriptText is the text content of the script
    180      * @param fileName   is the logical file name of the script (which is used to create the class name of the script)
    181      * @param list       the command line arguments to pass in
    182      */
    183     public Object run(String scriptText, String fileName, List list) throws CompilationFailedException {
    184         String[] args = new String[list.size()];
    185         list.toArray(args);
    186         return run(scriptText, fileName, args);
    187     }
    188
    189     /***
    190      * Runs the given script file name with the given command line arguments
    191      *
    192      * @param scriptFile the file name of the script to run
    193      * @param args       the command line arguments to pass in
    194      */
    195     public Object run(final File scriptFile, String[] args) throws CompilationFailedException, IOException {
    196         String scriptName = scriptFile.getName();
    197         int p = scriptName.lastIndexOf(".");
    198         if (p++ >= 0) {
    199             if (scriptName.substring(p).equals("java")) {
    200                 System.err.println("error: cannot compile file with .java extension: " + scriptName);
    201                 throw new CompilationFailedException(0, null);
    202             }
    203         }
    204
    205         // Get the current context classloader and save it on the stack
    206         final Thread thread = Thread.currentThread();
    207         //ClassLoader currentClassLoader = thread.getContextClassLoader();
    208
    209         class DoSetContext implements PrivilegedAction {
    210             ClassLoader classLoader;
    211
    212             public DoSetContext(ClassLoader loader) {
    213                 classLoader = loader;
    214             }
    215
    216             public Object run() {
    217                 thread.setContextClassLoader(classLoader);
    218                 return null;
    219             }
    220         }
    221
    222         AccessController.doPrivileged(new DoSetContext(loader));
    223
    224         // Parse the script, generate the class, and invoke the main method.  This is a little looser than
    225         // if you are compiling the script because the JVM isn't executing the main method.
    226         Class scriptClass;
    227         try {
    228             scriptClass = (Class) AccessController.doPrivileged(new PrivilegedExceptionAction() {
    229                 public Object run() throws CompilationFailedException, IOException {
    230                     return loader.parseClass(scriptFile);
    231                 }
    232             });
    233         } catch (PrivilegedActionException pae) {
    234             Exception e = pae.getException();
    235             if (e instanceof CompilationFailedException) {
    236                 throw (CompilationFailedException) e;
    237             } else if (e instanceof IOException) {
    238                 throw (IOException) e;
    239             } else {
    240                 throw (RuntimeException) pae.getException();
    241             }
    242         }
    243
    244         return runMainOrTestOrRunnable(scriptClass, args);
    245
    246         // Set the context classloader back to what it was.
    247         //AccessController.doPrivileged(new DoSetContext(currentClassLoader));
    248     }
    249
    250     /***
    251      * if (theClass has a main method) {
    252      * run the main method
    253      * } else if (theClass instanceof GroovyTestCase) {
    254      * use the test runner to run it
    255      * } else if (theClass implements Runnable) {
    256      * if (theClass has a constructor with String[] params)
    257      * instanciate theClass with this constructor and run
    258      * else if (theClass has a no-args constructor)
    259      * instanciate theClass with the no-args constructor and run
    260      * }
    261      */
    262     private Object runMainOrTestOrRunnable(Class scriptClass, String[] args) {
    263         if (scriptClass == null) {
    264             return null;
    265         }
    266         try {
    267             // let's find a main method
    268             scriptClass.getMethod("main", new Class[]{String[].class});
    269         } catch (NoSuchMethodException e) {
    270             // As no main() method was found, let's see if it's a unit test
    271             // if it's a unit test extending GroovyTestCase, run it with JUnit's TextRunner
    272             if (isUnitTestCase(scriptClass)) {
    273                 return runTest(scriptClass);
    274             }
    275             // no main() method, not a unit test,
    276             // if it implements Runnable, try to instanciate it
    277             else if (Runnable.class.isAssignableFrom(scriptClass)) {
    278                 Constructor constructor = null;
    279                 Runnable runnable = null;
    280                 Throwable reason = null;
    281                 try {
    282                     // first, fetch the constructor taking String[] as parameter
    283                     constructor = scriptClass.getConstructor(new Class[]{(new String[]{}).getClass()});
    284                     try {
    285                         // instanciate a runnable and run it
    286                         runnable = (Runnable) constructor.newInstance(new Object[]{args});
    287                     } catch (Throwable t) {
    288                         reason = t;
    289                     }
    290                 } catch (NoSuchMethodException e1) {
    291                     try {
    292                         // otherwise, find the default constructor
    293                         constructor = scriptClass.getConstructor(new Class[]{});
    294                         try {
    295                             // instanciate a runnable and run it
    296                             runnable = (Runnable) constructor.newInstance(new Object[]{});
    297                         } catch (Throwable t) {
    298                             reason = t;
    299                         }
    300                     } catch (NoSuchMethodException nsme) {
    301                         reason = nsme;
    302                     }
    303                 }
    304                 if (constructor != null && runnable != null) {
    305                     runnable.run();
    306                 } else {
    307                     throw new GroovyRuntimeException("This script or class could not be run. ", reason);
    308                 }
    309             } else {
    310                 throw new GroovyRuntimeException("This script or class could not be run. \n" +
    311                         "It should either: \n" +
    312                         "- have a main method, \n" +
    313                         "- be a class extending GroovyTestCase, \n" +
    314                         "- or implement the Runnable interface.");
    315             }
    316             return null;
    317         }
    318         // if that main method exist, invoke it
    319         return InvokerHelper.invokeMethod(scriptClass, "main", new Object[]{args});
    320     }
    321
    322     /***
    323      * Run the specified class extending GroovyTestCase as a unit test.
    324      * This is done through reflection, to avoid adding a dependency to the JUnit framework.
    325      * Otherwise, developers embedding Groovy and using GroovyShell to load/parse/compile
    326      * groovy scripts and classes would have to add another dependency on their classpath.
    327      *
    328      * @param scriptClass the class to be run as a unit test
    329      */
    330     private Object runTest(Class scriptClass) {
    331         try {
    332             Object testSuite = InvokerHelper.invokeConstructorOf("junit.framework.TestSuite",new Object[]{scriptClass});
    333             return InvokerHelper.invokeStaticMethod("junit.textui.TestRunner", "run", new Object[]{testSuite});
    334         } catch (Exception e) {
    335             throw new GroovyRuntimeException("Failed to run the unit test. JUnit is not on the Classpath.");
    336         }
    337     }
    338
    339     /***
    340      * Utility method to check through reflection if the parsed class extends GroovyTestCase.
    341      *
    342      * @param scriptClass the class we want to know if it extends GroovyTestCase
    343      * @return true if the class extends groovy.util.GroovyTestCase
    344      */
    345     private boolean isUnitTestCase(Class scriptClass) {
    346         // check if the parsed class is a GroovyTestCase,
    347         // so that it is possible to run it as a JUnit test
    348         boolean isUnitTestCase = false;
    349         try {
    350             try {
    351                 Class testCaseClass = this.loader.loadClass("groovy.util.GroovyTestCase");
    352                 // if scriptClass extends testCaseClass
    353                 if (testCaseClass.isAssignableFrom(scriptClass)) {
    354                     isUnitTestCase = true;
    355                 }
    356             } catch (ClassNotFoundException e) {
    357                 // fall through
    358             }
    359         } catch (Throwable e) {
    360             // fall through
    361         }
    362         return isUnitTestCase;
    363     }
    364
    365     /***
    366      * Runs the given script text with command line arguments
    367      *
    368      * @param scriptText is the text content of the script
    369      * @param fileName   is the logical file name of the script (which is used to create the class name of the script)
    370      * @param args       the command line arguments to pass in
    371      */
    372     public Object run(String scriptText, String fileName, String[] args) throws CompilationFailedException {
    373         try {
    374             return run(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), fileName, args);
    375         } catch (UnsupportedEncodingException e) {
    376             throw new CompilationFailedException(0, null, e);
    377         }
    378     }
    379
    380     /***
    381      * Runs the given script with command line arguments
    382      *
    383      * @param in       the stream reading the script
    384      * @param fileName is the logical file name of the script (which is used to create the class name of the script)
    385      * @param args     the command line arguments to pass in
    386      */
    387     public Object run(final InputStream in, final String fileName, String[] args) throws CompilationFailedException {
    388         GroovyCodeSource gcs = (GroovyCodeSource) AccessController.doPrivileged(new PrivilegedAction() {
    389             public Object run() {
    390                 return new GroovyCodeSource(in, fileName, "/groovy/shell");
    391             }
    392         });
    393         Class scriptClass = parseClass(gcs);
    394         return runMainOrTestOrRunnable(scriptClass, args);
    395     }
    396
    397     public Object getVariable(String name) {
    398         return context.getVariables().get(name);
    399     }
    400
    401     public void setVariable(String name, Object value) {
    402         context.setVariable(name, value);
    403     }
    404
    405     /***
    406      * Evaluates some script against the current Binding and returns the result
    407      *
    408      * @param codeSource
    409      * @return
    410      * @throws CompilationFailedException
    411      * @throws CompilationFailedException
    412      */
    413     public Object evaluate(GroovyCodeSource codeSource) throws CompilationFailedException {
    414         Script script = parse(codeSource);
    415         return script.run();
    416     }
    417
    418     /***
    419      * Evaluates some script against the current Binding and returns the result
    420      *
    421      * @param scriptText the text of the script
    422      * @param fileName   is the logical file name of the script (which is used to create the class name of the script)
    423      */
    424     public Object evaluate(String scriptText, String fileName) throws CompilationFailedException {
    425         try {
    426             return evaluate(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), fileName);
    427         } catch (UnsupportedEncodingException e) {
    428             throw new CompilationFailedException(0, null, e);
    429         }
    430     }
    431
    432     /***
    433      * Evaluates some script against the current Binding and returns the result.
    434      * The .class file created from the script is given the supplied codeBase
    435      */
    436     public Object evaluate(String scriptText, String fileName, String codeBase) throws CompilationFailedException {
    437         try {
    438             return evaluate(new GroovyCodeSource(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), fileName, codeBase));
    439         } catch (UnsupportedEncodingException e) {
    440             throw new CompilationFailedException(0, null, e);
    441         }
    442     }
    443
    444     /***
    445      * Evaluates some script against the current Binding and returns the result
    446      *
    447      * @param file is the file of the script (which is used to create the class name of the script)
    448      */
    449     public Object evaluate(File file) throws CompilationFailedException, IOException {
    450         return evaluate(new GroovyCodeSource(file));
    451     }
    452
    453     /***
    454      * Evaluates some script against the current Binding and returns the result
    455      *
    456      * @param scriptText the text of the script
    457      */
    458     public Object evaluate(String scriptText) throws CompilationFailedException {
    459         try {
    460             return evaluate(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), generateScriptName());
    461         } catch (UnsupportedEncodingException e) {
    462             throw new CompilationFailedException(0, null, e);
    463         }
    464     }
    465
    466     /***
    467      * Evaluates some script against the current Binding and returns the result
    468      *
    469      * @param in the stream reading the script
    470      */
    471     public Object evaluate(InputStream in) throws CompilationFailedException {
    472         return evaluate(in, generateScriptName());
    473     }
    474
    475     /***
    476      * Evaluates some script against the current Binding and returns the result
    477      *
    478      * @param in       the stream reading the script
    479      * @param fileName is the logical file name of the script (which is used to create the class name of the script)
    480      */
    481     public Object evaluate(InputStream in, String fileName) throws CompilationFailedException {
    482         Script script = null;
    483         try {
    484             script = parse(in, fileName);
    485             return script.run();
    486         } finally {
    487             if (script != null) {
    488                 InvokerHelper.removeClass(script.getClass());
    489             }
    490         }
    491     }
    492
    493     /***
    494      * Parses the given script and returns it ready to be run
    495      *
    496      * @param in       the stream reading the script
    497      * @param fileName is the logical file name of the script (which is used to create the class name of the script)
    498      * @return the parsed script which is ready to be run via @link Script.run()
    499      */
    500     public Script parse(final InputStream in, final String fileName) throws CompilationFailedException {
    501         GroovyCodeSource gcs = (GroovyCodeSource) AccessController.doPrivileged(new PrivilegedAction() {
    502             public Object run() {
    503                 return new GroovyCodeSource(in, fileName, "/groovy/shell");
    504             }
    505         });
    506         return parse(gcs);
    507     }
    508
    509     /***
    510      * Parses the groovy code contained in codeSource and returns a java class.
    511      */
    512     private Class parseClass(final GroovyCodeSource codeSource) throws CompilationFailedException {
    513         // Don't cache scripts
    514         return loader.parseClass(codeSource, false);
    515     }
    516
    517     /***
    518      * Parses the given script and returns it ready to be run.  When running in a secure environment
    519      * (-Djava.security.manager) codeSource.getCodeSource() determines what policy grants should be
    520      * given to the script.
    521      *
    522      * @param codeSource
    523      * @return ready to run script
    524      */
    525     public Script parse(final GroovyCodeSource codeSource) throws CompilationFailedException {
    526         return InvokerHelper.createScript(parseClass(codeSource), context);
    527     }
    528
    529     /***
    530      * Parses the given script and returns it ready to be run
    531      *
    532      * @param file is the file of the script (which is used to create the class name of the script)
    533      */
    534     public Script parse(File file) throws CompilationFailedException, IOException {
    535         return parse(new GroovyCodeSource(file));
    536     }
    537
    538     /***
    539      * Parses the given script and returns it ready to be run
    540      *
    541      * @param scriptText the text of the script
    542      */
    543     public Script parse(String scriptText) throws CompilationFailedException {
    544         try {
    545             return parse(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), generateScriptName());
    546         } catch (UnsupportedEncodingException e) {
    547             throw new CompilationFailedException(0, null, e);
    548         }
    549     }
    550
    551     public Script parse(String scriptText, String fileName) throws CompilationFailedException {
    552         try {
    553             return parse(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), fileName);
    554         } catch (UnsupportedEncodingException e) {
    555             throw new CompilationFailedException(0, null, e);
    556         }
    557     }
    558
    559     /***
    560      * Parses the given script and returns it ready to be run
    561      *
    562      * @param in the stream reading the script
    563      */
    564     public Script parse(InputStream in) throws CompilationFailedException {
    565         return parse(in, generateScriptName());
    566     }
    567
    568     protected synchronized String generateScriptName() {
    569         return "Script" + (++counter) + ".groovy";
    570     }
    571 }