1 /*
2 * Copyright 2003-2007 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package groovy.lang;
17
18 import groovy.ui.GroovyMain;
19
20 import org.codehaus.groovy.control.CompilationFailedException;
21 import org.codehaus.groovy.control.CompilerConfiguration;
22 import org.codehaus.groovy.runtime.InvokerHelper;
23
24 import java.io;
25 import java.lang.reflect.Constructor;
26 import java.security.AccessController;
27 import java.security.PrivilegedAction;
28 import java.security.PrivilegedActionException;
29 import java.security.PrivilegedExceptionAction;
30 import java.util.List;
31 import java.util.Map;
32
33 /**
34 * Represents a groovy shell capable of running arbitrary groovy scripts
35 *
36 * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
37 * @author Guillaume Laforge
38 * @version $Revision: 13745 $
39 */
40 public class GroovyShell extends GroovyObjectSupport {
41
42 public static final String[] EMPTY_ARGS = {};
43
44
45 private Binding context;
46 private int counter;
47 private CompilerConfiguration config;
48 private GroovyClassLoader loader;
49
50 public static void main(String[] args) {
51 GroovyMain.main(args);
52 }
53
54 public GroovyShell() {
55 this(null, new Binding());
56 }
57
58 public GroovyShell(Binding binding) {
59 this(null, binding);
60 }
61
62 public GroovyShell(CompilerConfiguration config) {
63 this(new Binding(), config);
64 }
65
66 public GroovyShell(Binding binding, CompilerConfiguration config) {
67 this(null, binding, config);
68 }
69
70 public GroovyShell(ClassLoader parent, Binding binding) {
71 this(parent, binding, CompilerConfiguration.DEFAULT);
72 }
73
74 public GroovyShell(ClassLoader parent) {
75 this(parent, new Binding(), CompilerConfiguration.DEFAULT);
76 }
77
78 public GroovyShell(ClassLoader parent, Binding binding, final CompilerConfiguration config) {
79 if (binding == null) {
80 throw new IllegalArgumentException("Binding must not be null.");
81 }
82 if (config == null) {
83 throw new IllegalArgumentException("Compiler configuration must not be null.");
84 }
85 final ClassLoader parentLoader = (parent!=null)?parent:GroovyShell.class.getClassLoader();
86 this.loader = (GroovyClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
87 public Object run() {
88 return new GroovyClassLoader(parentLoader,config);
89 }
90 });
91 this.context = binding;
92 this.config = config;
93 }
94
95 public void initializeBinding() {
96 Map map = context.getVariables();
97 if (map.get("shell")==null) map.put("shell",this);
98 }
99
100 public void resetLoadedClasses() {
101 loader.clearCache();
102 }
103
104 /**
105 * Creates a child shell using a new ClassLoader which uses the parent shell's
106 * class loader as its parent
107 *
108 * @param shell is the parent shell used for the variable bindings and the parent class loader
109 */
110 public GroovyShell(GroovyShell shell) {
111 this(shell.loader, shell.context);
112 }
113
114 public Binding getContext() {
115 return context;
116 }
117
118 public GroovyClassLoader getClassLoader() {
119 return loader;
120 }
121
122 public Object getProperty(String property) {
123 Object answer = getVariable(property);
124 if (answer == null) {
125 answer = super.getProperty(property);
126 }
127 return answer;
128 }
129
130 public void setProperty(String property, Object newValue) {
131 setVariable(property, newValue);
132 try {
133 super.setProperty(property, newValue);
134 } catch (GroovyRuntimeException e) {
135 // ignore, was probably a dynamic property
136 }
137 }
138
139 /**
140 * A helper method which runs the given script file with the given command line arguments
141 *
142 * @param scriptFile the file of the script to run
143 * @param list the command line arguments to pass in
144 */
145 public Object run(File scriptFile, List list) throws CompilationFailedException, IOException {
146 String[] args = new String[list.size()];
147 return run(scriptFile, (String[]) list.toArray(args));
148 }
149
150 /**
151 * A helper method which runs the given cl script with the given command line arguments
152 *
153 * @param scriptText is the text content of the script
154 * @param fileName is the logical file name of the script (which is used to create the class name of the script)
155 * @param list the command line arguments to pass in
156 */
157 public Object run(String scriptText, String fileName, List list) throws CompilationFailedException {
158 String[] args = new String[list.size()];
159 list.toArray(args);
160 return run(scriptText, fileName, args);
161 }
162
163 /**
164 * Runs the given script file name with the given command line arguments
165 *
166 * @param scriptFile the file name of the script to run
167 * @param args the command line arguments to pass in
168 */
169 public Object run(final File scriptFile, String[] args) throws CompilationFailedException, IOException {
170 String scriptName = scriptFile.getName();
171 int p = scriptName.lastIndexOf(".");
172 if (p++ >= 0) {
173 if (scriptName.substring(p).equals("java")) {
174 System.err.println("error: cannot compile file with .java extension: " + scriptName);
175 throw new CompilationFailedException(0, null);
176 }
177 }
178
179 // Get the current context classloader and save it on the stack
180 final Thread thread = Thread.currentThread();
181 //ClassLoader currentClassLoader = thread.getContextClassLoader();
182
183 class DoSetContext implements PrivilegedAction {
184 ClassLoader classLoader;
185
186 public DoSetContext(ClassLoader loader) {
187 classLoader = loader;
188 }
189
190 public Object run() {
191 thread.setContextClassLoader(classLoader);
192 return null;
193 }
194 }
195
196 AccessController.doPrivileged(new DoSetContext(loader));
197
198 // Parse the script, generate the class, and invoke the main method. This is a little looser than
199 // if you are compiling the script because the JVM isn't executing the main method.
200 Class scriptClass;
201 try {
202 scriptClass = (Class) AccessController.doPrivileged(new PrivilegedExceptionAction() {
203 public Object run() throws CompilationFailedException, IOException {
204 return loader.parseClass(scriptFile);
205 }
206 });
207 } catch (PrivilegedActionException pae) {
208 Exception e = pae.getException();
209 if (e instanceof CompilationFailedException) {
210 throw (CompilationFailedException) e;
211 } else if (e instanceof IOException) {
212 throw (IOException) e;
213 } else {
214 throw (RuntimeException) pae.getException();
215 }
216 }
217
218 return runScriptOrMainOrTestOrRunnable(scriptClass, args);
219
220 // Set the context classloader back to what it was.
221 //AccessController.doPrivileged(new DoSetContext(currentClassLoader));
222 }
223
224 /**
225 * if (theClass is a Script) {
226 * run it like a script
227 * } else if (theClass has a main method) {
228 * run the main method
229 * } else if (theClass instanceof GroovyTestCase) {
230 * use the test runner to run it
231 * } else if (theClass implements Runnable) {
232 * if (theClass has a constructor with String[] params)
233 * instanciate theClass with this constructor and run
234 * else if (theClass has a no-args constructor)
235 * instanciate theClass with the no-args constructor and run
236 * }
237 */
238 private Object runScriptOrMainOrTestOrRunnable(Class scriptClass, String[] args) {
239 if (scriptClass == null) {
240 return null;
241 }
242 try {
243 if (Script.class.isAssignableFrom(scriptClass)) {
244 // treat it just like a script if it is one
245 Script script = null;
246 try {
247 script = (Script) scriptClass.newInstance();
248 } catch (InstantiationException e) {
249 // ignore instaintiations errors,, try to do main
250 } catch (IllegalAccessException e) {
251 // ignore instaintiations errors, try to do main
252 }
253 if (script != null) {
254 script.setBinding(context);
255 script.setProperty("args", args);
256 return script.run();
257 }
258 }
259 // let's find a main method
260 scriptClass.getMethod("main", new Class[]{String[].class});
261 // if that main method exist, invoke it
262 return InvokerHelper.invokeMethod(scriptClass, "main", new Object[]{args});
263 } catch (NoSuchMethodException e) {
264 // if it implements Runnable, try to instantiate it
265 if (Runnable.class.isAssignableFrom(scriptClass)) {
266 return runRunnable(scriptClass, args);
267 }
268 // if it's a JUnit 3.8.x test, run it with an appropriate runner
269 if (isJUnit3Test(scriptClass)) {
270 return runJUnit3Test(scriptClass);
271 }
272 // if it's a JUnit 4.x test, run it with an appropriate runner
273 if (isJUnit4Test(scriptClass)) {
274 return runJUnit4Test(scriptClass);
275 }
276 // if it's a TestNG tst, run it with an appropriate runner
277 if (isTestNgTest(scriptClass)) {
278 return runTestNgTest(scriptClass);
279 }
280 throw new GroovyRuntimeException("This script or class could not be run.\n" +
281 "It should either: \n" +
282 "- have a main method, \n" +
283 "- be a JUnit test, TestNG test or extend GroovyTestCase, \n" +
284 "- or implement the Runnable interface.");
285 }
286 }
287
288 private Object runRunnable(Class scriptClass, String[] args) {
289 Constructor constructor = null;
290 Runnable runnable = null;
291 Throwable reason = null;
292 try {
293 // first, fetch the constructor taking String[] as parameter
294 constructor = scriptClass.getConstructor(new Class[]{(new String[]{}).getClass()});
295 try {
296 // instantiate a runnable and run it
297 runnable = (Runnable) constructor.newInstance(new Object[]{args});
298 } catch (Throwable t) {
299 reason = t;
300 }
301 } catch (NoSuchMethodException e1) {
302 try {
303 // otherwise, find the default constructor
304 constructor = scriptClass.getConstructor(new Class[]{});
305 try {
306 // instantiate a runnable and run it
307 runnable = (Runnable) constructor.newInstance(new Object[]{});
308 } catch (Throwable t) {
309 reason = t;
310 }
311 } catch (NoSuchMethodException nsme) {
312 reason = nsme;
313 }
314 }
315 if (constructor != null && runnable != null) {
316 runnable.run();
317 } else {
318 throw new GroovyRuntimeException("This script or class was runnable but could not be run. ", reason);
319 }
320 return null;
321 }
322
323 /**
324 * Run the specified class extending TestCase as a unit test.
325 * This is done through reflection, to avoid adding a dependency to the JUnit framework.
326 * Otherwise, developers embedding Groovy and using GroovyShell to load/parse/compile
327 * groovy scripts and classes would have to add another dependency on their classpath.
328 *
329 * @param scriptClass the class to be run as a unit test
330 */
331 private Object runJUnit3Test(Class scriptClass) {
332 try {
333 Object testSuite = InvokerHelper.invokeConstructorOf("junit.framework.TestSuite",new Object[]{scriptClass});
334 return InvokerHelper.invokeStaticMethod("junit.textui.TestRunner", "run", new Object[]{testSuite});
335 } catch (ClassNotFoundException e) {
336 throw new GroovyRuntimeException("Failed to run the unit test. JUnit is not on the Classpath.");
337 }
338 }
339
340 private Object runJUnit4Test(Class scriptClass) {
341 try {
342 return InvokerHelper.invokeStaticMethod("org.codehaus.groovy.vmplugin.v5.JUnit4Utils",
343 "realRunJUnit4Test", new Object[]{scriptClass});
344 } catch (ClassNotFoundException e) {
345 throw new GroovyRuntimeException("Failed to run the JUnit 4 test.");
346 }
347 }
348
349 private Object runTestNgTest(Class scriptClass) {
350 try {
351 return InvokerHelper.invokeStaticMethod("org.codehaus.groovy.vmplugin.v5.TestNgUtils",
352 "realRunTestNgTest", new Object[]{scriptClass});
353 } catch (ClassNotFoundException e) {
354 throw new GroovyRuntimeException("Failed to run the TestNG test.");
355 }
356 }
357
358 /**
359 * Utility method to check through reflection if the class appears to be a
360 * JUnit 3.8.x test, i.e.&nsbp;checks if it extends JUnit 3.8.x's TestCase.
361 *
362 * @param scriptClass the class we want to check
363 * @return true if the class appears to be a test
364 */
365 private boolean isJUnit3Test(Class scriptClass) {
366 // check if the parsed class is a GroovyTestCase,
367 // so that it is possible to run it as a JUnit test
368 boolean isUnitTestCase = false;
369 try {
370 try {
371 Class testCaseClass = this.loader.loadClass("junit.framework.TestCase");
372 // if scriptClass extends testCaseClass
373 if (testCaseClass.isAssignableFrom(scriptClass)) {
374 isUnitTestCase = true;
375 }
376 } catch (ClassNotFoundException e) {
377 // fall through
378 }
379 } catch (Throwable e) {
380 // fall through
381 }
382 return isUnitTestCase;
383 }
384
385 /**
386 * Utility method to check via reflection if the parsed class appears to be a JUnit4
387 * test, i.e.&nsbp;checks whether it appears to be using the relevant JUnit 4 annotations.
388 *
389 * @param scriptClass the class we want to check
390 * @return true if the class appears to be a test
391 */
392 private boolean isJUnit4Test(Class scriptClass) {
393 // if we are running under Java 1.4 don't bother trying to check
394 char version = System.getProperty("java.version").charAt(2);
395 if (version < '5') {
396 return false;
397 }
398
399 // check if there are appropriate class or method annotations
400 // that suggest we have a JUnit 4 test
401 boolean isTest = false;
402
403 try {
404 if (InvokerHelper.invokeStaticMethod("org.codehaus.groovy.vmplugin.v5.JUnit4Utils",
405 "realIsJUnit4Test", new Object[]{scriptClass, this.loader}) == Boolean.TRUE) {
406 isTest = true;
407 };
408 } catch (ClassNotFoundException e) {
409 throw new GroovyRuntimeException("Failed to invoke the JUnit 4 helper class.");
410 }
411 return isTest;
412 }
413
414 /**
415 * Utility method to check via reflection if the parsed class appears to be a TestNG
416 * test, i.e.&nsbp;checks whether it appears to be using the relevant TestNG annotations.
417 *
418 * @param scriptClass the class we want to check
419 * @return true if the class appears to be a test
420 */
421 private boolean isTestNgTest(Class scriptClass) {
422 char version = System.getProperty("java.version").charAt(2);
423 if (version < '5') {
424 return false;
425 }
426
427 // check if there are appropriate class or method annotations
428 // that suggest we have a TestNG test
429 boolean isTest = false;
430
431 try {
432 if (InvokerHelper.invokeStaticMethod("org.codehaus.groovy.vmplugin.v5.TestNgUtils",
433 "realIsTestNgTest", new Object[]{scriptClass, this.loader}) == Boolean.TRUE) {
434 isTest = true;
435 };
436 } catch (ClassNotFoundException e) {
437 throw new GroovyRuntimeException("Failed to invoke the TestNG helper class.");
438 }
439 return isTest;
440 }
441
442 /**
443 * Runs the given script text with command line arguments
444 *
445 * @param scriptText is the text content of the script
446 * @param fileName is the logical file name of the script (which is used to create the class name of the script)
447 * @param args the command line arguments to pass in
448 */
449 public Object run(String scriptText, String fileName, String[] args) throws CompilationFailedException {
450 try {
451 return run(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), fileName, args);
452 } catch (UnsupportedEncodingException e) {
453 throw new CompilationFailedException(0, null, e);
454 }
455 }
456
457 /**
458 * Runs the given script with command line arguments
459 *
460 * @param in the stream reading the script
461 * @param fileName is the logical file name of the script (which is used to create the class name of the script)
462 * @param args the command line arguments to pass in
463 */
464 public Object run(final InputStream in, final String fileName, String[] args) throws CompilationFailedException {
465 GroovyCodeSource gcs = (GroovyCodeSource) AccessController.doPrivileged(new PrivilegedAction() {
466 public Object run() {
467 return new GroovyCodeSource(in, fileName, "/groovy/shell");
468 }
469 });
470 Class scriptClass = parseClass(gcs);
471 return runScriptOrMainOrTestOrRunnable(scriptClass, args);
472 }
473
474 public Object getVariable(String name) {
475 return context.getVariables().get(name);
476 }
477
478 public void setVariable(String name, Object value) {
479 context.setVariable(name, value);
480 }
481
482 /**
483 * Evaluates some script against the current Binding and returns the result
484 *
485 * @param codeSource
486 * @throws CompilationFailedException
487 * @throws CompilationFailedException
488 */
489 public Object evaluate(GroovyCodeSource codeSource) throws CompilationFailedException {
490 Script script = parse(codeSource);
491 return script.run();
492 }
493
494 /**
495 * Evaluates some script against the current Binding and returns the result
496 *
497 * @param scriptText the text of the script
498 * @param fileName is the logical file name of the script (which is used to create the class name of the script)
499 */
500 public Object evaluate(String scriptText, String fileName) throws CompilationFailedException {
501 try {
502 return evaluate(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), fileName);
503 } catch (UnsupportedEncodingException e) {
504 throw new CompilationFailedException(0, null, e);
505 }
506 }
507
508 /**
509 * Evaluates some script against the current Binding and returns the result.
510 * The .class file created from the script is given the supplied codeBase
511 */
512 public Object evaluate(String scriptText, String fileName, String codeBase) throws CompilationFailedException {
513 try {
514 return evaluate(new GroovyCodeSource(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), fileName, codeBase));
515 } catch (UnsupportedEncodingException e) {
516 throw new CompilationFailedException(0, null, e);
517 }
518 }
519
520 /**
521 * Evaluates some script against the current Binding and returns the result
522 *
523 * @param file is the file of the script (which is used to create the class name of the script)
524 */
525 public Object evaluate(File file) throws CompilationFailedException, IOException {
526 return evaluate(new GroovyCodeSource(file));
527 }
528
529 /**
530 * Evaluates some script against the current Binding and returns the result
531 *
532 * @param scriptText the text of the script
533 */
534 public Object evaluate(String scriptText) throws CompilationFailedException {
535 try {
536 return evaluate(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), generateScriptName());
537 } catch (UnsupportedEncodingException e) {
538 throw new CompilationFailedException(0, null, e);
539 }
540 }
541
542 /**
543 * Evaluates some script against the current Binding and returns the result
544 *
545 * @param in the stream reading the script
546 */
547 public Object evaluate(InputStream in) throws CompilationFailedException {
548 return evaluate(in, generateScriptName());
549 }
550
551 /**
552 * Evaluates some script against the current Binding and returns the result
553 *
554 * @param in the stream reading the script
555 * @param fileName is the logical file name of the script (which is used to create the class name of the script)
556 */
557 public Object evaluate(InputStream in, String fileName) throws CompilationFailedException {
558 Script script = null;
559 try {
560 script = parse(in, fileName);
561 return script.run();
562 } finally {
563 if (script != null) {
564 InvokerHelper.removeClass(script.getClass());
565 }
566 }
567 }
568
569 /**
570 * Parses the given script and returns it ready to be run
571 *
572 * @param in the stream reading the script
573 * @param fileName is the logical file name of the script (which is used to create the class name of the script)
574 * @return the parsed script which is ready to be run via @link Script.run()
575 */
576 public Script parse(final InputStream in, final String fileName) throws CompilationFailedException {
577 GroovyCodeSource gcs = (GroovyCodeSource) AccessController.doPrivileged(new PrivilegedAction() {
578 public Object run() {
579 return new GroovyCodeSource(in, fileName, "/groovy/shell");
580 }
581 });
582 return parse(gcs);
583 }
584
585 /**
586 * Parses the groovy code contained in codeSource and returns a java class.
587 */
588 private Class parseClass(final GroovyCodeSource codeSource) throws CompilationFailedException {
589 // Don't cache scripts
590 return loader.parseClass(codeSource, false);
591 }
592
593 /**
594 * Parses the given script and returns it ready to be run. When running in a secure environment
595 * (-Djava.security.manager) codeSource.getCodeSource() determines what policy grants should be
596 * given to the script.
597 *
598 * @param codeSource
599 * @return ready to run script
600 */
601 public Script parse(final GroovyCodeSource codeSource) throws CompilationFailedException {
602 return InvokerHelper.createScript(parseClass(codeSource), context);
603 }
604
605 /**
606 * Parses the given script and returns it ready to be run
607 *
608 * @param file is the file of the script (which is used to create the class name of the script)
609 */
610 public Script parse(File file) throws CompilationFailedException, IOException {
611 return parse(new GroovyCodeSource(file));
612 }
613
614 /**
615 * Parses the given script and returns it ready to be run
616 *
617 * @param scriptText the text of the script
618 */
619 public Script parse(String scriptText) throws CompilationFailedException {
620 try {
621 return parse(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), generateScriptName());
622 } catch (UnsupportedEncodingException e) {
623 throw new CompilationFailedException(0, null, e);
624 }
625 }
626
627 public Script parse(String scriptText, String fileName) throws CompilationFailedException {
628 try {
629 return parse(new ByteArrayInputStream(scriptText.getBytes(config.getSourceEncoding())), fileName);
630 } catch (UnsupportedEncodingException e) {
631 throw new CompilationFailedException(0, null, e);
632 }
633 }
634
635 /**
636 * Parses the given script and returns it ready to be run
637 *
638 * @param in the stream reading the script
639 */
640 public Script parse(InputStream in) throws CompilationFailedException {
641 return parse(in, generateScriptName());
642 }
643
644 protected synchronized String generateScriptName() {
645 return "Script" + (++counter) + ".groovy";
646 }
647 }