View Javadoc

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