001 /* 002 $Id: GroovyShell.java,v 1.49 2005/10/03 18:07:35 tug 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.49 $ 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.invokeConstructorOf("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 }