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 }