001    /*
002     $Id: GroovyShell.java 4346 2006-12-09 03:28:00Z paulk $
003    
004     Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
005    
006     Redistribution and use of this software and associated documentation
007     ("Software"), with or without modification, are permitted provided
008     that the following conditions are met:
009    
010     1. Redistributions of source code must retain copyright
011        statements and notices.  Redistributions must also contain a
012        copy of this document.
013    
014     2. Redistributions in binary form must reproduce the
015        above copyright notice, this list of conditions and the
016        following disclaimer in the documentation and/or other
017        materials provided with the distribution.
018    
019     3. The name "groovy" must not be used to endorse or promote
020        products derived from this Software without prior written
021        permission of The Codehaus.  For written permission,
022        please contact info@codehaus.org.
023    
024     4. Products derived from this Software may not be called "groovy"
025        nor may "groovy" appear in their names without prior written
026        permission of The Codehaus. "groovy" is a registered
027        trademark of The Codehaus.
028    
029     5. Due credit should be given to The Codehaus -
030        http://groovy.codehaus.org/
031    
032     THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS
033     ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
034     NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
035     FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
036     THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
037     INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
038     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
039     SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
040     HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
041     STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
042     ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
043     OF THE POSSIBILITY OF SUCH DAMAGE.
044    
045     */
046    package groovy.lang;
047    
048    import groovy.ui.GroovyMain;
049    
050    import org.codehaus.groovy.control.CompilationFailedException;
051    import org.codehaus.groovy.control.CompilerConfiguration;
052    import org.codehaus.groovy.runtime.InvokerHelper;
053    
054    import java.io.*;
055    import java.lang.reflect.Constructor;
056    import java.security.AccessController;
057    import java.security.PrivilegedAction;
058    import java.security.PrivilegedActionException;
059    import java.security.PrivilegedExceptionAction;
060    import java.util.List;
061    import java.util.Map;
062    
063    /**
064     * Represents a groovy shell capable of running arbitrary groovy scripts
065     *
066     * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
067     * @author Guillaume Laforge
068     * @version $Revision: 4346 $
069     */
070    public class GroovyShell extends GroovyObjectSupport {
071           
072        public static final String[] EMPTY_ARGS = {};
073    
074        
075        private Binding context;
076        private int counter;
077        private CompilerConfiguration config;
078        private GroovyClassLoader loader;
079    
080        public static void main(String[] args) {
081            GroovyMain.main(args);
082        }
083    
084        public GroovyShell() {
085            this(null, new Binding());
086        }
087    
088        public GroovyShell(Binding binding) {
089            this(null, binding);
090        }
091    
092        public GroovyShell(CompilerConfiguration config) {
093            this(new Binding(), config);
094        }
095    
096        public GroovyShell(Binding binding, CompilerConfiguration config) {
097            this(null, binding, config);
098        }
099    
100        public GroovyShell(ClassLoader parent, Binding binding) {
101            this(parent, binding, CompilerConfiguration.DEFAULT);
102        }
103    
104        public GroovyShell(ClassLoader parent) {
105            this(parent, new Binding(), CompilerConfiguration.DEFAULT);
106        }
107        
108        public GroovyShell(ClassLoader parent, Binding binding, final CompilerConfiguration config) {
109            if (binding == null) {
110                throw new IllegalArgumentException("Binding must not be null.");
111            }
112            if (config == null) {
113                throw new IllegalArgumentException("Compiler configuration must not be null.");
114            }
115            final ClassLoader parentLoader = (parent!=null)?parent:GroovyShell.class.getClassLoader();
116            this.loader = (GroovyClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
117                public Object run() {
118                    return new GroovyClassLoader(parentLoader,config);
119                }
120            });
121            this.context = binding;        
122            this.config = config;
123        }
124        
125        public void initializeBinding() {
126            Map map = context.getVariables();
127            if (map.get("shell")==null) map.put("shell",this);
128        }
129        
130        public void resetLoadedClasses() {
131            loader.clearCache();
132        }
133    
134        /**
135         * Creates a child shell using a new ClassLoader which uses the parent shell's
136         * class loader as its parent
137         *
138         * @param shell is the parent shell used for the variable bindings and the parent class loader
139         */
140        public GroovyShell(GroovyShell shell) {
141            this(shell.loader, shell.context);
142        }
143    
144        public Binding getContext() {
145            return context;
146        }
147    
148        public Object getProperty(String property) {
149            Object answer = getVariable(property);
150            if (answer == null) {
151                answer = super.getProperty(property);
152            }
153            return answer;
154        }
155    
156        public void setProperty(String property, Object newValue) {
157            setVariable(property, newValue);
158            try {
159                super.setProperty(property, newValue);
160            } catch (GroovyRuntimeException e) {
161                // ignore, was probably a dynamic property
162            }
163        }
164    
165        /**
166         * A helper method which runs the given script file with the given command line arguments
167         *
168         * @param scriptFile the file of the script to run
169         * @param list       the command line arguments to pass in
170         */
171        public Object run(File scriptFile, List list) throws CompilationFailedException, IOException {
172            String[] args = new String[list.size()];
173            return run(scriptFile, (String[]) list.toArray(args));
174        }
175    
176        /**
177         * A helper method which runs the given cl script with the given command line arguments
178         *
179         * @param scriptText is the text content of the script
180         * @param fileName   is the logical file name of the script (which is used to create the class name of the script)
181         * @param list       the command line arguments to pass in
182         */
183        public Object run(String scriptText, String fileName, List list) throws CompilationFailedException {
184            String[] args = new String[list.size()];
185            list.toArray(args);
186            return run(scriptText, fileName, args);
187        }
188    
189        /**
190         * Runs the given script file name with the given command line arguments
191         *
192         * @param scriptFile the file name of the script to run
193         * @param args       the command line arguments to pass in
194         */
195        public Object run(final File scriptFile, String[] args) throws CompilationFailedException, IOException {
196            String scriptName = scriptFile.getName();
197            int p = scriptName.lastIndexOf(".");
198            if (p++ >= 0) {
199                if (scriptName.substring(p).equals("java")) {
200                    System.err.println("error: cannot compile file with .java extension: " + scriptName);
201                    throw new CompilationFailedException(0, null);
202                }
203            }
204    
205            // Get the current context classloader and save it on the stack
206            final Thread thread = Thread.currentThread();
207            //ClassLoader currentClassLoader = thread.getContextClassLoader();
208    
209            class DoSetContext implements PrivilegedAction {
210                ClassLoader classLoader;
211    
212                public DoSetContext(ClassLoader loader) {
213                    classLoader = loader;
214                }
215    
216                public Object run() {
217                    thread.setContextClassLoader(classLoader);
218                    return null;
219                }
220            }
221    
222            AccessController.doPrivileged(new DoSetContext(loader));
223    
224            // Parse the script, generate the class, and invoke the main method.  This is a little looser than
225            // if you are compiling the script because the JVM isn't executing the main method.
226            Class scriptClass;
227            try {
228                scriptClass = (Class) AccessController.doPrivileged(new PrivilegedExceptionAction() {
229                    public Object run() throws CompilationFailedException, IOException {
230                        return loader.parseClass(scriptFile);
231                    }
232                });
233            } catch (PrivilegedActionException pae) {
234                Exception e = pae.getException();
235                if (e instanceof CompilationFailedException) {
236                    throw (CompilationFailedException) e;
237                } else if (e instanceof IOException) {
238                    throw (IOException) e;
239                } else {
240                    throw (RuntimeException) pae.getException();
241                }
242            }
243    
244            return runMainOrTestOrRunnable(scriptClass, args);
245    
246            // Set the context classloader back to what it was.
247            //AccessController.doPrivileged(new DoSetContext(currentClassLoader));
248        }
249    
250        /**
251         * if (theClass has a main method) {
252         * run the main method
253         * } else if (theClass instanceof GroovyTestCase) {
254         * use the test runner to run it
255         * } else if (theClass implements Runnable) {
256         * if (theClass has a constructor with String[] params)
257         * instanciate theClass with this constructor and run
258         * else if (theClass has a no-args constructor)
259         * instanciate theClass with the no-args constructor and run
260         * }
261         */
262        private Object runMainOrTestOrRunnable(Class scriptClass, String[] args) {
263            if (scriptClass == null) {
264                return null;
265            }
266            try {
267                // let's find a main method
268                scriptClass.getMethod("main", new Class[]{String[].class});
269            } catch (NoSuchMethodException e) {
270                // As no main() method was found, let's see if it's a unit test
271                // if it's a unit test extending GroovyTestCase, run it with JUnit's TextRunner
272                if (isUnitTestCase(scriptClass)) {
273                    return runTest(scriptClass);
274                }
275                // no main() method, not a unit test,
276                // if it implements Runnable, try to instanciate it
277                else if (Runnable.class.isAssignableFrom(scriptClass)) {
278                    Constructor constructor = null;
279                    Runnable runnable = null;
280                    Throwable reason = null;
281                    try {
282                        // first, fetch the constructor taking String[] as parameter
283                        constructor = scriptClass.getConstructor(new Class[]{(new String[]{}).getClass()});
284                        try {
285                            // instanciate a runnable and run it
286                            runnable = (Runnable) constructor.newInstance(new Object[]{args});
287                        } catch (Throwable t) {
288                            reason = t;
289                        }
290                    } catch (NoSuchMethodException e1) {
291                        try {
292                            // otherwise, find the default constructor
293                            constructor = scriptClass.getConstructor(new Class[]{});
294                            try {
295                                // instanciate a runnable and run it
296                                runnable = (Runnable) constructor.newInstance(new Object[]{});
297                            } catch (Throwable t) {
298                                reason = t;
299                            }
300                        } catch (NoSuchMethodException nsme) {
301                            reason = nsme;
302                        }
303                    }
304                    if (constructor != null && runnable != null) {
305                        runnable.run();
306                    } else {
307                        throw new GroovyRuntimeException("This script or class could not be run. ", reason);
308                    }
309                } else {
310                    throw new GroovyRuntimeException("This script or class could not be run. \n" +
311                            "It should either: \n" +
312                            "- have a main method, \n" +
313                            "- be a class extending GroovyTestCase, \n" +
314                            "- or implement the Runnable interface.");
315                }
316                return null;
317            }
318            // if that main method exist, invoke it
319            return InvokerHelper.invokeMethod(scriptClass, "main", new Object[]{args});
320        }
321    
322        /**
323         * Run the specified class extending GroovyTestCase as a unit test.
324         * This is done through reflection, to avoid adding a dependency to the JUnit framework.
325         * Otherwise, developers embedding Groovy and using GroovyShell to load/parse/compile
326         * groovy scripts and classes would have to add another dependency on their classpath.
327         *
328         * @param scriptClass the class to be run as a unit test
329         */
330        private Object runTest(Class scriptClass) {
331            try {
332                Object testSuite = InvokerHelper.invokeConstructorOf("junit.framework.TestSuite",new Object[]{scriptClass});
333                return InvokerHelper.invokeStaticMethod("junit.textui.TestRunner", "run", new Object[]{testSuite});
334            } catch (ClassNotFoundException e) {
335                throw new GroovyRuntimeException("Failed to run the unit test. JUnit is not on the Classpath.");
336            }
337        }
338    
339        /**
340         * Utility method to check through reflection if the parsed class extends GroovyTestCase.
341         *
342         * @param scriptClass the class we want to know if it extends GroovyTestCase
343         * @return true if the class extends groovy.util.GroovyTestCase
344         */
345        private boolean isUnitTestCase(Class scriptClass) {
346            // check if the parsed class is a GroovyTestCase,
347            // so that it is possible to run it as a JUnit test
348            boolean isUnitTestCase = false;
349            try {
350                try {
351                    Class testCaseClass = this.loader.loadClass("groovy.util.GroovyTestCase");
352                    // if scriptClass extends testCaseClass
353                    if (testCaseClass.isAssignableFrom(scriptClass)) {
354                        isUnitTestCase = true;
355                    }
356                } catch (ClassNotFoundException e) {
357                    // fall through
358                }
359            } catch (Throwable e) {
360                // fall through
361            }
362            return isUnitTestCase;
363        }
364    
365        /**
366         * Runs the given script text with command line arguments
367         *
368         * @param scriptText is the text content of the script
369         * @param fileName   is the logical file name of the script (which is used to create the class name of the script)
370         * @param args       the command line arguments to pass in
371         */
372        public Object run(String scriptText, String fileName, String[] args) throws CompilationFailedException {
373            try {
374                return run(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), fileName, args);
375            } catch (UnsupportedEncodingException e) {
376                throw new CompilationFailedException(0, null, e);
377            }
378        }
379    
380        /**
381         * Runs the given script with command line arguments
382         *
383         * @param in       the stream reading the script
384         * @param fileName is the logical file name of the script (which is used to create the class name of the script)
385         * @param args     the command line arguments to pass in
386         */
387        public Object run(final InputStream in, final String fileName, String[] args) throws CompilationFailedException {
388            GroovyCodeSource gcs = (GroovyCodeSource) AccessController.doPrivileged(new PrivilegedAction() {
389                public Object run() {
390                    return new GroovyCodeSource(in, fileName, "/groovy/shell");
391                }
392            });
393            Class scriptClass = parseClass(gcs);
394            return runMainOrTestOrRunnable(scriptClass, args);
395        }
396    
397        public Object getVariable(String name) {
398            return context.getVariables().get(name);
399        }
400    
401        public void setVariable(String name, Object value) {
402            context.setVariable(name, value);
403        }
404    
405        /**
406         * Evaluates some script against the current Binding and returns the result
407         *
408         * @param codeSource
409         * @throws CompilationFailedException
410         * @throws CompilationFailedException
411         */
412        public Object evaluate(GroovyCodeSource codeSource) throws CompilationFailedException {
413            Script script = parse(codeSource);
414            return script.run();
415        }
416    
417        /**
418         * Evaluates some script against the current Binding and returns the result
419         *
420         * @param scriptText the text of the script
421         * @param fileName   is the logical file name of the script (which is used to create the class name of the script)
422         */
423        public Object evaluate(String scriptText, String fileName) throws CompilationFailedException {
424            try {
425                return evaluate(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), fileName);
426            } catch (UnsupportedEncodingException e) {
427                throw new CompilationFailedException(0, null, e);
428            }
429        }
430    
431        /**
432         * Evaluates some script against the current Binding and returns the result.
433         * The .class file created from the script is given the supplied codeBase
434         */
435        public Object evaluate(String scriptText, String fileName, String codeBase) throws CompilationFailedException {
436            try {
437                return evaluate(new GroovyCodeSource(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), fileName, codeBase));
438            } catch (UnsupportedEncodingException e) {
439                throw new CompilationFailedException(0, null, e);
440            }
441        }
442    
443        /**
444         * Evaluates some script against the current Binding and returns the result
445         *
446         * @param file is the file of the script (which is used to create the class name of the script)
447         */
448        public Object evaluate(File file) throws CompilationFailedException, IOException {
449            return evaluate(new GroovyCodeSource(file));
450        }
451    
452        /**
453         * Evaluates some script against the current Binding and returns the result
454         *
455         * @param scriptText the text of the script
456         */
457        public Object evaluate(String scriptText) throws CompilationFailedException {
458            try {
459                return evaluate(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), generateScriptName());
460            } catch (UnsupportedEncodingException e) {
461                throw new CompilationFailedException(0, null, e);
462            }
463        }
464    
465        /**
466         * Evaluates some script against the current Binding and returns the result
467         *
468         * @param in the stream reading the script
469         */
470        public Object evaluate(InputStream in) throws CompilationFailedException {
471            return evaluate(in, generateScriptName());
472        }
473    
474        /**
475         * Evaluates some script against the current Binding and returns the result
476         *
477         * @param in       the stream reading the script
478         * @param fileName is the logical file name of the script (which is used to create the class name of the script)
479         */
480        public Object evaluate(InputStream in, String fileName) throws CompilationFailedException {
481            Script script = null;
482            try {
483                script = parse(in, fileName);
484                return script.run();
485            } finally {
486                if (script != null) {
487                    InvokerHelper.removeClass(script.getClass());
488                }
489            }
490        }
491    
492        /**
493         * Parses the given script and returns it ready to be run
494         *
495         * @param in       the stream reading the script
496         * @param fileName is the logical file name of the script (which is used to create the class name of the script)
497         * @return the parsed script which is ready to be run via @link Script.run()
498         */
499        public Script parse(final InputStream in, final String fileName) throws CompilationFailedException {
500            GroovyCodeSource gcs = (GroovyCodeSource) AccessController.doPrivileged(new PrivilegedAction() {
501                public Object run() {
502                    return new GroovyCodeSource(in, fileName, "/groovy/shell");
503                }
504            });
505            return parse(gcs);
506        }
507    
508        /**
509         * Parses the groovy code contained in codeSource and returns a java class.
510         */
511        private Class parseClass(final GroovyCodeSource codeSource) throws CompilationFailedException {
512            // Don't cache scripts
513            return loader.parseClass(codeSource, false);
514        }
515    
516        /**
517         * Parses the given script and returns it ready to be run.  When running in a secure environment
518         * (-Djava.security.manager) codeSource.getCodeSource() determines what policy grants should be
519         * given to the script.
520         *
521         * @param codeSource
522         * @return ready to run script
523         */
524        public Script parse(final GroovyCodeSource codeSource) throws CompilationFailedException {
525            return InvokerHelper.createScript(parseClass(codeSource), context);
526        }
527    
528        /**
529         * Parses the given script and returns it ready to be run
530         *
531         * @param file is the file of the script (which is used to create the class name of the script)
532         */
533        public Script parse(File file) throws CompilationFailedException, IOException {
534            return parse(new GroovyCodeSource(file));
535        }
536    
537        /**
538         * Parses the given script and returns it ready to be run
539         *
540         * @param scriptText the text of the script
541         */
542        public Script parse(String scriptText) throws CompilationFailedException {
543            try {
544                return parse(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), generateScriptName());
545            } catch (UnsupportedEncodingException e) {
546                throw new CompilationFailedException(0, null, e);
547            }
548        }
549    
550        public Script parse(String scriptText, String fileName) throws CompilationFailedException {
551            try {
552                return parse(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), fileName);
553            } catch (UnsupportedEncodingException e) {
554                throw new CompilationFailedException(0, null, e);
555            }
556        }
557    
558        /**
559         * Parses the given script and returns it ready to be run
560         *
561         * @param in the stream reading the script
562         */
563        public Script parse(InputStream in) throws CompilationFailedException {
564            return parse(in, generateScriptName());
565        }
566    
567        protected synchronized String generateScriptName() {
568            return "Script" + (++counter) + ".groovy";
569        }
570    }