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 }