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