View Javadoc

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