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