View Javadoc

1   /*
2    $Id: GroovyShell.java,v 1.49 2005/10/03 18:07:35 tug 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.ast.ClassNode;
51  import org.codehaus.groovy.control.CompilationFailedException;
52  import org.codehaus.groovy.control.CompilerConfiguration;
53  import org.codehaus.groovy.runtime.InvokerHelper;
54  
55  import java.io.ByteArrayInputStream;
56  import java.io.File;
57  import java.io.IOException;
58  import java.io.InputStream;
59  import java.lang.reflect.Constructor;
60  import java.security.AccessController;
61  import java.security.PrivilegedAction;
62  import java.security.PrivilegedActionException;
63  import java.security.PrivilegedExceptionAction;
64  import java.util.HashMap;
65  import java.util.List;
66  import java.util.Map;
67  
68  /***
69   * Represents a groovy shell capable of running arbitrary groovy scripts
70   *
71   * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
72   * @author Guillaume Laforge
73   * @version $Revision: 1.49 $
74   */
75  public class GroovyShell extends GroovyObjectSupport {
76      
77      private class ShellLoader extends GroovyClassLoader {
78          public ShellLoader() {
79              super(loader, config);
80          }
81          public Class defineClass(ClassNode classNode, String file, String newCodeBase) {
82              Class c = super.defineClass(classNode,file,newCodeBase);
83              classMap.put(c.getName(),this);
84              return c;
85          }
86      }
87  
88      private static ClassLoader getLoader(ClassLoader cl) {
89          if (cl!=null) return cl;
90          cl = Thread.currentThread().getContextClassLoader();
91          if (cl!=null) return cl;
92          cl = GroovyShell.class.getClassLoader();
93          if (cl!=null) return cl;
94          return null;
95      }
96      
97      private class MainClassLoader extends ClassLoader {
98          public MainClassLoader(ClassLoader parent) {
99              super(getLoader(parent));
100         }
101         protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
102             Object cached = classMap.get(name);
103             if (cached!=null) return (Class) cached;
104             ClassLoader parent = getParent();
105             if (parent!=null) return parent.loadClass(name);
106             return super.loadClass(name,resolve);
107         }
108     }
109     
110     
111     public static final String[] EMPTY_ARGS = {};
112 
113     
114     private HashMap classMap = new HashMap();
115     private MainClassLoader loader;
116     private Binding context;
117     private int counter;
118     private CompilerConfiguration config;
119 
120     public static void main(String[] args) {
121         GroovyMain.main(args);
122     }
123 
124     public GroovyShell() {
125         this(null, new Binding());
126     }
127 
128     public GroovyShell(Binding binding) {
129         this(null, binding);
130     }
131 
132     public GroovyShell(CompilerConfiguration config) {
133         this(new Binding(), config);
134     }
135 
136     public GroovyShell(Binding binding, CompilerConfiguration config) {
137         this(null, binding, config);
138     }
139 
140     public GroovyShell(ClassLoader parent, Binding binding) {
141         this(parent, binding, CompilerConfiguration.DEFAULT);
142     }
143 
144     public GroovyShell(ClassLoader parent) {
145         this(parent, new Binding(), CompilerConfiguration.DEFAULT);
146     }
147     
148     public GroovyShell(ClassLoader parent, Binding binding, CompilerConfiguration config) {
149         if (binding == null) {
150             throw new IllegalArgumentException("Binding must not be null.");
151         }
152         if (config == null) {
153             throw new IllegalArgumentException("Compiler configuration must not be null.");
154         }
155         this.loader = new MainClassLoader(parent);
156         this.context = binding;        
157         this.config = config;
158     }
159     
160     public void initialiseBinding() {
161         Map map = context.getVariables();
162         if (map.get("shell")==null) map.put("shell",this);
163     }
164     
165     public void resetLoadedClasses() {
166         classMap.clear();
167     }
168 
169     /***
170      * Creates a child shell using a new ClassLoader which uses the parent shell's
171      * class loader as its parent
172      *
173      * @param shell is the parent shell used for the variable bindings and the parent class loader
174      */
175     public GroovyShell(GroovyShell shell) {
176         this(shell.loader, shell.context);
177     }
178 
179     public Binding getContext() {
180         return context;
181     }
182 
183     public Object getProperty(String property) {
184         Object answer = getVariable(property);
185         if (answer == null) {
186             answer = super.getProperty(property);
187         }
188         return answer;
189     }
190 
191     public void setProperty(String property, Object newValue) {
192         setVariable(property, newValue);
193         try {
194             super.setProperty(property, newValue);
195         } catch (GroovyRuntimeException e) {
196             // ignore, was probably a dynamic property
197         }
198     }
199 
200     /***
201      * A helper method which runs the given script file with the given command line arguments
202      *
203      * @param scriptFile the file of the script to run
204      * @param list       the command line arguments to pass in
205      */
206     public Object run(File scriptFile, List list) throws CompilationFailedException, IOException {
207         String[] args = new String[list.size()];
208         return run(scriptFile, (String[]) list.toArray(args));
209     }
210 
211     /***
212      * A helper method which runs the given cl script with the given command line arguments
213      *
214      * @param scriptText is the text content of the script
215      * @param fileName   is the logical file name of the script (which is used to create the class name of the script)
216      * @param list       the command line arguments to pass in
217      */
218     public Object run(String scriptText, String fileName, List list) throws CompilationFailedException {
219         String[] args = new String[list.size()];
220         list.toArray(args);
221         return run(scriptText, fileName, args);
222     }
223 
224     /***
225      * Runs the given script file name with the given command line arguments
226      *
227      * @param scriptFile the file name of the script to run
228      * @param args       the command line arguments to pass in
229      */
230     public Object run(final File scriptFile, String[] args) throws CompilationFailedException, IOException {
231         String scriptName = scriptFile.getName();
232         int p = scriptName.lastIndexOf(".");
233         if (p++ >= 0) {
234             if (scriptName.substring(p).equals("java")) {
235                 System.err.println("error: cannot compile file with .java extension: " + scriptName);
236                 throw new CompilationFailedException(0, null);
237             }
238         }
239 
240         // Get the current context classloader and save it on the stack
241         final Thread thread = Thread.currentThread();
242         //ClassLoader currentClassLoader = thread.getContextClassLoader();
243 
244         class DoSetContext implements PrivilegedAction {
245             ClassLoader classLoader;
246 
247             public DoSetContext(ClassLoader loader) {
248                 classLoader = loader;
249             }
250 
251             public Object run() {
252                 thread.setContextClassLoader(classLoader);
253                 return null;
254             }
255         }
256 
257         AccessController.doPrivileged(new DoSetContext(loader));
258 
259         // Parse the script, generate the class, and invoke the main method.  This is a little looser than
260         // if you are compiling the script because the JVM isn't executing the main method.
261         Class scriptClass;
262         final ShellLoader loader = new ShellLoader();
263         try {
264             scriptClass = (Class) AccessController.doPrivileged(new PrivilegedExceptionAction() {
265                 public Object run() throws CompilationFailedException, IOException {
266                     return loader.parseClass(scriptFile);
267                 }
268             });
269         } catch (PrivilegedActionException pae) {
270             Exception e = pae.getException();
271             if (e instanceof CompilationFailedException) {
272                 throw (CompilationFailedException) e;
273             } else if (e instanceof IOException) {
274                 throw (IOException) e;
275             } else {
276                 throw (RuntimeException) pae.getException();
277             }
278         }
279 
280         return runMainOrTestOrRunnable(scriptClass, args);
281 
282         // Set the context classloader back to what it was.
283         //AccessController.doPrivileged(new DoSetContext(currentClassLoader));
284     }
285 
286     /***
287      * if (theClass has a main method) {
288      * run the main method
289      * } else if (theClass instanceof GroovyTestCase) {
290      * use the test runner to run it
291      * } else if (theClass implements Runnable) {
292      * if (theClass has a constructor with String[] params)
293      * instanciate theClass with this constructor and run
294      * else if (theClass has a no-args constructor)
295      * instanciate theClass with the no-args constructor and run
296      * }
297      */
298     private Object runMainOrTestOrRunnable(Class scriptClass, String[] args) {
299         if (scriptClass == null) {
300             return null;
301         }
302         try {
303             // let's find a main method
304             scriptClass.getMethod("main", new Class[]{String[].class});
305         } catch (NoSuchMethodException e) {
306             // As no main() method was found, let's see if it's a unit test
307             // if it's a unit test extending GroovyTestCase, run it with JUnit's TextRunner
308             if (isUnitTestCase(scriptClass)) {
309                 return runTest(scriptClass);
310             }
311             // no main() method, not a unit test,
312             // if it implements Runnable, try to instanciate it
313             else if (Runnable.class.isAssignableFrom(scriptClass)) {
314                 Constructor constructor = null;
315                 Runnable runnable = null;
316                 Throwable reason = null;
317                 try {
318                     // first, fetch the constructor taking String[] as parameter
319                     constructor = scriptClass.getConstructor(new Class[]{(new String[]{}).getClass()});
320                     try {
321                         // instanciate a runnable and run it
322                         runnable = (Runnable) constructor.newInstance(new Object[]{args});
323                     } catch (Throwable t) {
324                         reason = t;
325                     }
326                 } catch (NoSuchMethodException e1) {
327                     try {
328                         // otherwise, find the default constructor
329                         constructor = scriptClass.getConstructor(new Class[]{});
330                         try {
331                             // instanciate a runnable and run it
332                             runnable = (Runnable) constructor.newInstance(new Object[]{});
333                         } catch (Throwable t) {
334                             reason = t;
335                         }
336                     } catch (NoSuchMethodException nsme) {
337                         reason = nsme;
338                     }
339                 }
340                 if (constructor != null && runnable != null) {
341                     runnable.run();
342                 } else {
343                     throw new GroovyRuntimeException("This script or class could not be run. ", reason);
344                 }
345             } else {
346                 throw new GroovyRuntimeException("This script or class could not be run. \n" +
347                         "It should either: \n" +
348                         "- have a main method, \n" +
349                         "- be a class extending GroovyTestCase, \n" +
350                         "- or implement the Runnable interface.");
351             }
352             return null;
353         }
354         // if that main method exist, invoke it
355         return InvokerHelper.invokeMethod(scriptClass, "main", new Object[]{args});
356     }
357 
358     /***
359      * Run the specified class extending GroovyTestCase as a unit test.
360      * This is done through reflection, to avoid adding a dependency to the JUnit framework.
361      * Otherwise, developers embedding Groovy and using GroovyShell to load/parse/compile
362      * groovy scripts and classes would have to add another dependency on their classpath.
363      *
364      * @param scriptClass the class to be run as a unit test
365      */
366     private Object runTest(Class scriptClass) {
367         try {
368             Object testSuite = InvokerHelper.invokeConstructorOf("junit.framework.TestSuite",new Object[]{scriptClass});
369             return InvokerHelper.invokeStaticMethod("junit.textui.TestRunner", "run", new Object[]{testSuite});
370         } catch (Exception e) {
371             throw new GroovyRuntimeException("Failed to run the unit test. JUnit is not on the Classpath.");
372         }
373     }
374 
375     /***
376      * Utility method to check through reflection if the parsed class extends GroovyTestCase.
377      *
378      * @param scriptClass the class we want to know if it extends GroovyTestCase
379      * @return true if the class extends groovy.util.GroovyTestCase
380      */
381     private boolean isUnitTestCase(Class scriptClass) {
382         // check if the parsed class is a GroovyTestCase,
383         // so that it is possible to run it as a JUnit test
384         final ShellLoader loader = new ShellLoader();
385         boolean isUnitTestCase = false;
386         try {
387             try {
388                 Class testCaseClass = this.loader.loadClass("groovy.util.GroovyTestCase");
389                 // if scriptClass extends testCaseClass
390                 if (testCaseClass.isAssignableFrom(scriptClass)) {
391                     isUnitTestCase = true;
392                 }
393             } catch (ClassNotFoundException e) {
394                 // fall through
395             }
396         } catch (Throwable e) {
397             // fall through
398         }
399         return isUnitTestCase;
400     }
401 
402     /***
403      * Runs the given script text with command line arguments
404      *
405      * @param scriptText is the text content of the script
406      * @param fileName   is the logical file name of the script (which is used to create the class name of the script)
407      * @param args       the command line arguments to pass in
408      */
409     public Object run(String scriptText, String fileName, String[] args) throws CompilationFailedException {
410         return run(new ByteArrayInputStream(scriptText.getBytes()), fileName, args);
411     }
412 
413     /***
414      * Runs the given script with command line arguments
415      *
416      * @param in       the stream reading the script
417      * @param fileName is the logical file name of the script (which is used to create the class name of the script)
418      * @param args     the command line arguments to pass in
419      */
420     public Object run(final InputStream in, final String fileName, String[] args) throws CompilationFailedException {
421         GroovyCodeSource gcs = (GroovyCodeSource) AccessController.doPrivileged(new PrivilegedAction() {
422             public Object run() {
423                 return new GroovyCodeSource(in, fileName, "/groovy/shell");
424             }
425         });
426         Class scriptClass = parseClass(gcs);
427         return runMainOrTestOrRunnable(scriptClass, args);
428     }
429 
430     public Object getVariable(String name) {
431         return context.getVariables().get(name);
432     }
433 
434     public void setVariable(String name, Object value) {
435         context.setVariable(name, value);
436     }
437 
438     /***
439      * Evaluates some script against the current Binding and returns the result
440      *
441      * @param codeSource
442      * @return
443      * @throws CompilationFailedException
444      * @throws CompilationFailedException
445      */
446     public Object evaluate(GroovyCodeSource codeSource) throws CompilationFailedException {
447         Script script = parse(codeSource);
448         return script.run();
449     }
450 
451     /***
452      * Evaluates some script against the current Binding and returns the result
453      *
454      * @param scriptText the text of the script
455      * @param fileName   is the logical file name of the script (which is used to create the class name of the script)
456      */
457     public Object evaluate(String scriptText, String fileName) throws CompilationFailedException {
458         return evaluate(new ByteArrayInputStream(scriptText.getBytes()), fileName);
459     }
460 
461     /***
462      * Evaluates some script against the current Binding and returns the result.
463      * The .class file created from the script is given the supplied codeBase
464      */
465     public Object evaluate(String scriptText, String fileName, String codeBase) throws CompilationFailedException {
466         return evaluate(new GroovyCodeSource(new ByteArrayInputStream(scriptText.getBytes()), fileName, codeBase));
467     }
468 
469     /***
470      * Evaluates some script against the current Binding and returns the result
471      *
472      * @param file is the file of the script (which is used to create the class name of the script)
473      */
474     public Object evaluate(File file) throws CompilationFailedException, IOException {
475         return evaluate(new GroovyCodeSource(file));
476     }
477 
478     /***
479      * Evaluates some script against the current Binding and returns the result
480      *
481      * @param scriptText the text of the script
482      */
483     public Object evaluate(String scriptText) throws CompilationFailedException {
484         return evaluate(new ByteArrayInputStream(scriptText.getBytes()), generateScriptName());
485     }
486 
487     /***
488      * Evaluates some script against the current Binding and returns the result
489      *
490      * @param in the stream reading the script
491      */
492     public Object evaluate(InputStream in) throws CompilationFailedException {
493         return evaluate(in, generateScriptName());
494     }
495 
496     /***
497      * Evaluates some script against the current Binding and returns the result
498      *
499      * @param in       the stream reading the script
500      * @param fileName is the logical file name of the script (which is used to create the class name of the script)
501      */
502     public Object evaluate(InputStream in, String fileName) throws CompilationFailedException {
503         Script script = null;
504         try {
505             script = parse(in, fileName);
506             return script.run();
507         } finally {
508             if (script != null) {
509                 InvokerHelper.removeClass(script.getClass());
510             }
511         }
512     }
513 
514     /***
515      * Parses the given script and returns it ready to be run
516      *
517      * @param in       the stream reading the script
518      * @param fileName is the logical file name of the script (which is used to create the class name of the script)
519      * @return the parsed script which is ready to be run via @link Script.run()
520      */
521     public Script parse(final InputStream in, final String fileName) throws CompilationFailedException {
522         GroovyCodeSource gcs = (GroovyCodeSource) AccessController.doPrivileged(new PrivilegedAction() {
523             public Object run() {
524                 return new GroovyCodeSource(in, fileName, "/groovy/shell");
525             }
526         });
527         return parse(gcs);
528     }
529 
530     /***
531      * Parses the groovy code contained in codeSource and returns a java class.
532      */
533     private Class parseClass(final GroovyCodeSource codeSource) throws CompilationFailedException {
534         // Don't cache scripts
535         ShellLoader loader = new ShellLoader();
536         return loader.parseClass(codeSource, false);
537     }
538 
539     /***
540      * Parses the given script and returns it ready to be run.  When running in a secure environment
541      * (-Djava.security.manager) codeSource.getCodeSource() determines what policy grants should be
542      * given to the script.
543      *
544      * @param codeSource
545      * @return ready to run script
546      */
547     public Script parse(final GroovyCodeSource codeSource) throws CompilationFailedException {
548         return InvokerHelper.createScript(parseClass(codeSource), context);
549     }
550 
551     /***
552      * Parses the given script and returns it ready to be run
553      *
554      * @param file is the file of the script (which is used to create the class name of the script)
555      */
556     public Script parse(File file) throws CompilationFailedException, IOException {
557         return parse(new GroovyCodeSource(file));
558     }
559 
560     /***
561      * Parses the given script and returns it ready to be run
562      *
563      * @param scriptText the text of the script
564      */
565     public Script parse(String scriptText) throws CompilationFailedException {
566         return parse(new ByteArrayInputStream(scriptText.getBytes()), generateScriptName());
567     }
568 
569     public Script parse(String scriptText, String fileName) throws CompilationFailedException {
570         return parse(new ByteArrayInputStream(scriptText.getBytes()), fileName);
571     }
572 
573     /***
574      * Parses the given script and returns it ready to be run
575      *
576      * @param in the stream reading the script
577      */
578     public Script parse(InputStream in) throws CompilationFailedException {
579         return parse(in, generateScriptName());
580     }
581 
582     protected synchronized String generateScriptName() {
583         return "Script" + (++counter) + ".groovy";
584     }
585 }