View Javadoc

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