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 }