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 }