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