View Javadoc

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