001 /* 002 * $Id: GroovyClassLoader.java,v 1.52 2005/07/14 13:54:52 blackdrag 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 that the 008 * following conditions are met: 009 * 1. Redistributions of source code must retain copyright statements and 010 * notices. Redistributions must also contain a copy of this document. 011 * 2. Redistributions in binary form must reproduce the above copyright 012 * notice, this list of conditions and the following disclaimer in the 013 * documentation and/or other materials provided with the distribution. 014 * 3. The name "groovy" must not be used to endorse or promote products 015 * derived from this Software without prior written permission of The Codehaus. 016 * For written permission, please contact info@codehaus.org. 017 * 4. Products derived from this Software may not be called "groovy" nor may 018 * "groovy" appear in their names without prior written permission of The 019 * Codehaus. "groovy" is a registered trademark of The Codehaus. 020 * 5. Due credit should be given to The Codehaus - http://groovy.codehaus.org/ 021 * 022 * THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS ``AS IS'' AND ANY 023 * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 024 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 025 * DISCLAIMED. IN NO EVENT SHALL THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR 026 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 027 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 028 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 029 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 030 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 031 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 032 * DAMAGE. 033 * 034 */ 035 package groovy.lang; 036 037 import java.io.BufferedInputStream; 038 import java.io.ByteArrayInputStream; 039 import java.io.ByteArrayOutputStream; 040 import java.io.File; 041 import java.io.IOException; 042 import java.io.InputStream; 043 import java.lang.reflect.Field; 044 import java.net.MalformedURLException; 045 import java.net.URL; 046 import java.security.AccessController; 047 import java.security.CodeSource; 048 import java.security.PrivilegedAction; 049 import java.security.ProtectionDomain; 050 import java.security.SecureClassLoader; 051 import java.util.ArrayList; 052 import java.util.Collection; 053 import java.util.HashMap; 054 import java.util.HashSet; 055 import java.util.Iterator; 056 import java.util.List; 057 import java.util.Map; 058 import java.util.Set; 059 import java.util.jar.Attributes; 060 import java.util.jar.JarEntry; 061 import java.util.jar.JarFile; 062 import java.util.jar.Manifest; 063 064 import org.codehaus.groovy.ast.ClassNode; 065 import org.codehaus.groovy.classgen.Verifier; 066 import org.codehaus.groovy.control.CompilationFailedException; 067 import org.codehaus.groovy.control.CompilationUnit; 068 import org.codehaus.groovy.control.CompilerConfiguration; 069 import org.codehaus.groovy.control.Phases; 070 import org.objectweb.asm.ClassVisitor; 071 import org.objectweb.asm.ClassWriter; 072 073 /** 074 * A ClassLoader which can load Groovy classes 075 * 076 * @author <a href="mailto:james@coredevelopers.net">James Strachan </a> 077 * @author Guillaume Laforge 078 * @author Steve Goetze 079 * @author Bing Ran 080 * @author <a href="mailto:scottstirling@rcn.com">Scott Stirling</a> 081 * @version $Revision: 1.52 $ 082 */ 083 public class GroovyClassLoader extends SecureClassLoader { 084 085 private Map cache = new HashMap(); 086 private Collection loadedClasses = null; 087 088 public void removeFromCache(Class aClass) { 089 cache.remove(aClass); 090 } 091 092 public static class PARSING { 093 } 094 095 private class NOT_RESOLVED { 096 } 097 098 private CompilerConfiguration config; 099 100 private String[] searchPaths; 101 102 private Set additionalPaths = new HashSet(); 103 104 public GroovyClassLoader() { 105 this(Thread.currentThread().getContextClassLoader()); 106 } 107 108 public GroovyClassLoader(ClassLoader loader) { 109 this(loader, null); 110 } 111 112 public GroovyClassLoader(GroovyClassLoader parent) { 113 this(parent, parent.config); 114 } 115 116 public GroovyClassLoader(ClassLoader loader, CompilerConfiguration config) { 117 super(loader); 118 if (config==null) config = CompilerConfiguration.DEFAULT; 119 this.config = config; 120 this.loadedClasses = new ArrayList(); 121 } 122 123 /** 124 * Loads the given class node returning the implementation Class 125 * 126 * @param classNode 127 * @return 128 */ 129 public Class defineClass(ClassNode classNode, String file) { 130 return defineClass(classNode, file, "/groovy/defineClass"); 131 } 132 133 /** 134 * Loads the given class node returning the implementation Class 135 * 136 * @param classNode 137 * @return 138 */ 139 public Class defineClass(ClassNode classNode, String file, String newCodeBase) { 140 CodeSource codeSource = null; 141 try { 142 codeSource = new CodeSource(new URL("file", "", newCodeBase), (java.security.cert.Certificate[]) null); 143 } catch (MalformedURLException e) { 144 //swallow 145 } 146 147 // 148 // BUG: Why is this passing getParent() as the ClassLoader??? 149 150 CompilationUnit unit = new CompilationUnit(config, codeSource, getParent()); 151 try { 152 ClassCollector collector = createCollector(unit); 153 154 unit.addClassNode(classNode); 155 unit.setClassgenCallback(collector); 156 unit.compile(Phases.CLASS_GENERATION); 157 158 return collector.generatedClass; 159 } catch (CompilationFailedException e) { 160 throw new RuntimeException(e); 161 } 162 } 163 164 /** 165 * Parses the given file into a Java class capable of being run 166 * 167 * @param file the file name to parse 168 * @return the main class defined in the given script 169 */ 170 public Class parseClass(File file) throws CompilationFailedException, IOException { 171 return parseClass(new GroovyCodeSource(file)); 172 } 173 174 /** 175 * Parses the given text into a Java class capable of being run 176 * 177 * @param text the text of the script/class to parse 178 * @param fileName the file name to use as the name of the class 179 * @return the main class defined in the given script 180 */ 181 public Class parseClass(String text, String fileName) throws CompilationFailedException { 182 return parseClass(new ByteArrayInputStream(text.getBytes()), fileName); 183 } 184 185 /** 186 * Parses the given text into a Java class capable of being run 187 * 188 * @param text the text of the script/class to parse 189 * @return the main class defined in the given script 190 */ 191 public Class parseClass(String text) throws CompilationFailedException { 192 return parseClass(new ByteArrayInputStream(text.getBytes()), "script" + System.currentTimeMillis() + ".groovy"); 193 } 194 195 /** 196 * Parses the given character stream into a Java class capable of being run 197 * 198 * @param in an InputStream 199 * @return the main class defined in the given script 200 */ 201 public Class parseClass(InputStream in) throws CompilationFailedException { 202 return parseClass(in, "script" + System.currentTimeMillis() + ".groovy"); 203 } 204 205 public Class parseClass(final InputStream in, final String fileName) throws CompilationFailedException { 206 //For generic input streams, provide a catch-all codebase of 207 // GroovyScript 208 //Security for these classes can be administered via policy grants with 209 // a codebase 210 //of file:groovy.script 211 GroovyCodeSource gcs = (GroovyCodeSource) AccessController.doPrivileged(new PrivilegedAction() { 212 public Object run() { 213 return new GroovyCodeSource(in, fileName, "/groovy/script"); 214 } 215 }); 216 return parseClass(gcs); 217 } 218 219 220 public Class parseClass(GroovyCodeSource codeSource) throws CompilationFailedException { 221 return parseClass(codeSource, true); 222 } 223 224 /** 225 * Parses the given code source into a Java class capable of being run 226 * 227 * @return the main class defined in the given script 228 */ 229 public Class parseClass(GroovyCodeSource codeSource, boolean shouldCache) throws CompilationFailedException { 230 String name = codeSource.getName(); 231 Class answer = null; 232 //ASTBuilder.resolveName can call this recursively -- for example when 233 // resolving a Constructor 234 //invocation for a class that is currently being compiled. 235 synchronized (cache) { 236 answer = (Class) cache.get(name); 237 if (answer != null) { 238 return (answer == PARSING.class ? null : answer); 239 } else { 240 cache.put(name, PARSING.class); 241 } 242 } 243 //Was neither already loaded nor compiling, so compile and add to 244 // cache. 245 try { 246 CompilationUnit unit = new CompilationUnit(config, codeSource.getCodeSource(), this); 247 // try { 248 ClassCollector collector = createCollector(unit); 249 250 if (codeSource.getFile()==null) { 251 unit.addSource(name, codeSource.getInputStream()); 252 } else { 253 unit.addSource(codeSource.getFile()); 254 } 255 unit.setClassgenCallback(collector); 256 int goalPhase = Phases.CLASS_GENERATION; 257 if (config != null && config.getTargetDirectory()!=null) goalPhase = Phases.OUTPUT; 258 unit.compile(goalPhase); 259 260 answer = collector.generatedClass; 261 // } 262 // catch( CompilationFailedException e ) { 263 // throw new RuntimeException( e ); 264 // } 265 synchronized (this.loadedClasses) { 266 this.loadedClasses.addAll(collector.getLoadedClasses()); 267 } 268 } finally { 269 synchronized (cache) { 270 if (answer == null || !shouldCache) { 271 cache.remove(name); 272 } else { 273 cache.put(name, answer); 274 } 275 } 276 try { 277 codeSource.getInputStream().close(); 278 } catch (IOException e) { 279 throw new GroovyRuntimeException("unable to close stream",e); 280 } 281 } 282 return answer; 283 } 284 285 /** 286 * Using this classloader you can load groovy classes from the system 287 * classpath as though they were already compiled. Note that .groovy classes 288 * found with this mechanism need to conform to the standard java naming 289 * convention - i.e. the public class inside the file must match the 290 * filename and the file must be located in a directory structure that 291 * matches the package structure. 292 */ 293 /*protected Class findClass(final String name) throws ClassNotFoundException { 294 SecurityManager sm = System.getSecurityManager(); 295 if (sm != null) { 296 String className = name.replace('/', '.'); 297 int i = className.lastIndexOf('.'); 298 if (i != -1) { 299 sm.checkPackageDefinition(className.substring(0, i)); 300 } 301 } 302 try { 303 return (Class) AccessController.doPrivileged(new PrivilegedExceptionAction() { 304 public Object run() throws ClassNotFoundException { 305 return findGroovyClass(name); 306 } 307 }); 308 } catch (PrivilegedActionException pae) { 309 throw (ClassNotFoundException) pae.getException(); 310 } 311 }*/ 312 313 /* protected Class findGroovyClass(String name) throws ClassNotFoundException { 314 //Use a forward slash here for the path separator. It will work as a 315 // separator 316 //for the File class on all platforms, AND it is required as a jar file 317 // entry separator. 318 String filename = name.replace('.', '/') + ".groovy"; 319 String[] paths = getClassPath(); 320 // put the absolute classname in a File object so we can easily 321 // pluck off the class name and the package path 322 File classnameAsFile = new File(filename); 323 // pluck off the classname without the package 324 String classname = classnameAsFile.getName(); 325 String pkg = classnameAsFile.getParent(); 326 String pkgdir; 327 for (int i = 0; i < paths.length; i++) { 328 String pathName = paths[i]; 329 File path = new File(pathName); 330 if (path.exists()) { 331 if (path.isDirectory()) { 332 // patch to fix case preserving but case insensitive file 333 // systems (like macosx) 334 // JIRA issue 414 335 // 336 // first see if the file even exists, no matter what the 337 // case is 338 File nocasefile = new File(path, filename); 339 if (!nocasefile.exists()) 340 continue; 341 342 // now we know the file is there is some form or another, so 343 // let's look up all the files to see if the one we're 344 // really 345 // looking for is there 346 if (pkg == null) 347 pkgdir = pathName; 348 else 349 pkgdir = pathName + "/" + pkg; 350 File pkgdirF = new File(pkgdir); 351 // make sure the resulting path is there and is a dir 352 if (pkgdirF.exists() && pkgdirF.isDirectory()) { 353 File files[] = pkgdirF.listFiles(); 354 for (int j = 0; j < files.length; j++) { 355 // do the case sensitive comparison 356 if (files[j].getName().equals(classname)) { 357 try { 358 return parseClass(files[j]); 359 } catch (CompilationFailedException e) { 360 throw new ClassNotFoundException("Syntax error in groovy file: " + files[j].getAbsolutePath(), e); 361 } catch (IOException e) { 362 throw new ClassNotFoundException("Error reading groovy file: " + files[j].getAbsolutePath(), e); 363 } 364 } 365 } 366 } 367 } else { 368 try { 369 JarFile jarFile = new JarFile(path); 370 JarEntry entry = jarFile.getJarEntry(filename); 371 if (entry != null) { 372 byte[] bytes = extractBytes(jarFile, entry); 373 Certificate[] certs = entry.getCertificates(); 374 try { 375 return parseClass(new GroovyCodeSource(new ByteArrayInputStream(bytes), filename, path, certs)); 376 } catch (CompilationFailedException e1) { 377 throw new ClassNotFoundException("Syntax error in groovy file: " + filename, e1); 378 } 379 } 380 381 } catch (IOException e) { 382 // Bad jar in classpath, ignore 383 } 384 } 385 } 386 } 387 throw new ClassNotFoundException(name); 388 }*/ 389 390 //Read the bytes from a non-null JarEntry. This is done here because the 391 // entry must be read completely 392 //in order to get verified certificates, which can only be obtained after a 393 // full read. 394 private byte[] extractBytes(JarFile jarFile, JarEntry entry) { 395 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 396 int b; 397 try { 398 BufferedInputStream bis = new BufferedInputStream(jarFile.getInputStream(entry)); 399 while ((b = bis.read()) != -1) { 400 baos.write(b); 401 } 402 } catch (IOException ioe) { 403 throw new GroovyRuntimeException("Could not read the jar bytes for " + entry.getName()); 404 } 405 return baos.toByteArray(); 406 } 407 408 /** 409 * Workaround for Groovy-835 410 * 411 * @return the classpath as an array of strings, uses the classpath in the CompilerConfiguration object if possible, 412 * otherwise defaults to the value of the <tt>java.class.path</tt> system property 413 */ 414 protected String[] getClassPath() { 415 if (null == searchPaths) { 416 String classpath; 417 if(null != config && null != config.getClasspath()) { 418 //there's probably a better way to do this knowing the internals of 419 //Groovy, but it works for now 420 StringBuffer sb = new StringBuffer(); 421 for(Iterator iter = config.getClasspath().iterator(); iter.hasNext(); ) { 422 sb.append(iter.next().toString()); 423 sb.append(File.pathSeparatorChar); 424 } 425 //remove extra path separator 426 sb.deleteCharAt(sb.length()-1); 427 classpath = sb.toString(); 428 } else { 429 classpath = System.getProperty("java.class.path", "."); 430 } 431 List pathList = new ArrayList(additionalPaths); 432 expandClassPath(pathList, null, classpath, false); 433 searchPaths = new String[pathList.size()]; 434 searchPaths = (String[]) pathList.toArray(searchPaths); 435 } 436 return searchPaths; 437 } 438 439 /** 440 * @param pathList an empty list that will contain the elements of the classpath 441 * @param classpath the classpath specified as a single string 442 */ 443 protected void expandClassPath(List pathList, String base, String classpath, boolean isManifestClasspath) { 444 445 // checking against null prevents an NPE when recursevely expanding the 446 // classpath 447 // in case the classpath is malformed 448 if (classpath != null) { 449 450 // Sun's convention for the class-path attribute is to seperate each 451 // entry with spaces 452 // but some libraries don't respect that convention and add commas, 453 // colons, semi-colons 454 String[] paths; 455 if (isManifestClasspath) { 456 paths = classpath.split("[\\ ,:;]"); 457 } else { 458 paths = classpath.split(File.pathSeparator); 459 } 460 461 for (int i = 0; i < paths.length; i++) { 462 if (paths.length > 0) { 463 File path = null; 464 465 if ("".equals(base)) { 466 path = new File(paths[i]); 467 } else { 468 path = new File(base, paths[i]); 469 } 470 471 if (path.exists()) { 472 if (!path.isDirectory()) { 473 try { 474 JarFile jar = new JarFile(path); 475 pathList.add(paths[i]); 476 477 Manifest manifest = jar.getManifest(); 478 if (manifest != null) { 479 Attributes classPathAttributes = manifest.getMainAttributes(); 480 String manifestClassPath = classPathAttributes.getValue("Class-Path"); 481 482 if (manifestClassPath != null) 483 expandClassPath(pathList, paths[i], manifestClassPath, true); 484 } 485 } catch (IOException e) { 486 // Bad jar, ignore 487 continue; 488 } 489 } else { 490 pathList.add(paths[i]); 491 } 492 } 493 } 494 } 495 } 496 } 497 498 /** 499 * A helper method to allow bytecode to be loaded. spg changed name to 500 * defineClass to make it more consistent with other ClassLoader methods 501 */ 502 protected Class defineClass(String name, byte[] bytecode, ProtectionDomain domain) { 503 return defineClass(name, bytecode, 0, bytecode.length, domain); 504 } 505 506 protected ClassCollector createCollector(CompilationUnit unit) { 507 return new ClassCollector(this, unit); 508 } 509 510 public static class ClassCollector extends CompilationUnit.ClassgenCallback { 511 private Class generatedClass; 512 513 private GroovyClassLoader cl; 514 515 private CompilationUnit unit; 516 517 private Collection loadedClasses = null; 518 519 protected ClassCollector(GroovyClassLoader cl, CompilationUnit unit) { 520 this.cl = cl; 521 this.unit = unit; 522 this.loadedClasses = new ArrayList(); 523 } 524 525 protected Class onClassNode(ClassWriter classWriter, ClassNode classNode) { 526 byte[] code = classWriter.toByteArray(); 527 528 Class theClass = cl.defineClass(classNode.getName(), code, 0, code.length, unit.getAST().getCodeSource()); 529 this.loadedClasses.add(theClass); 530 531 if (generatedClass == null) { 532 generatedClass = theClass; 533 } 534 535 return theClass; 536 } 537 538 public void call(ClassVisitor classWriter, ClassNode classNode) { 539 onClassNode((ClassWriter) classWriter, classNode); 540 } 541 542 public Collection getLoadedClasses() { 543 return this.loadedClasses; 544 } 545 } 546 547 /** 548 * open up the super class define that takes raw bytes 549 * 550 */ 551 public Class defineClass(String name, byte[] b) { 552 return super.defineClass(name, b, 0, b.length); 553 } 554 555 /* 556 * (non-Javadoc) 557 * 558 * @see java.lang.ClassLoader#loadClass(java.lang.String, boolean) 559 * Implemented here to check package access prior to returning an 560 * already loaded class. todo : br shall we search for the source 561 * groovy here to see if the soource file has been updated first? 562 */ 563 protected synchronized Class loadClass(final String name, boolean resolve) throws ClassNotFoundException { 564 synchronized (cache) { 565 Class cls = (Class) cache.get(name); 566 if (cls == NOT_RESOLVED.class) throw new ClassNotFoundException(name); 567 if (cls!=null) return cls; 568 } 569 570 SecurityManager sm = System.getSecurityManager(); 571 if (sm != null) { 572 String className = name.replace('/', '.'); 573 int i = className.lastIndexOf('.'); 574 if (i != -1) { 575 sm.checkPackageAccess(className.substring(0, i)); 576 } 577 } 578 579 Class cls = null; 580 ClassNotFoundException last = null; 581 try { 582 cls = super.loadClass(name, resolve); 583 584 boolean recompile = false; 585 if (getTimeStamp(cls) < Long.MAX_VALUE) { 586 Class[] inters = cls.getInterfaces(); 587 for (int i = 0; i < inters.length; i++) { 588 if (inters[i].getName().equals(GroovyObject.class.getName())) { 589 recompile=true; 590 break; 591 } 592 } 593 } 594 if (!recompile) return cls; 595 } catch (ClassNotFoundException cnfe) { 596 last = cnfe; 597 } 598 599 // try groovy file 600 try { 601 File source = (File) AccessController.doPrivileged(new PrivilegedAction() { 602 public Object run() { 603 return getSourceFile(name); 604 } 605 }); 606 if (source != null) { 607 if ((cls!=null && isSourceNewer(source, cls)) || (cls==null)) { 608 synchronized (cache) { 609 cache.put(name,PARSING.class); 610 } 611 cls = parseClass(source); 612 } 613 } 614 } catch (Exception e) { 615 cls = null; 616 last = new ClassNotFoundException("Failed to parse groovy file: " + name, e); 617 } 618 if (cls==null) { 619 if (last==null) throw new AssertionError(true); 620 synchronized (cache) { 621 cache.put(name, NOT_RESOLVED.class); 622 } 623 throw last; 624 } 625 synchronized (cache) { 626 cache.put(name, cls); 627 } 628 return cls; 629 } 630 631 private long getTimeStamp(Class cls) { 632 Field field; 633 Long o; 634 try { 635 field = cls.getField(Verifier.__TIMESTAMP); 636 o = (Long) field.get(null); 637 } catch (Exception e) { 638 //throw new RuntimeException(e); 639 return Long.MAX_VALUE; 640 } 641 return o.longValue(); 642 } 643 644 // static class ClassWithTimeTag { 645 // final static ClassWithTimeTag NOT_RESOLVED = new ClassWithTimeTag(null, 646 // 0); 647 // Class cls; 648 // long lastModified; 649 // 650 // public ClassWithTimeTag(Class cls, long lastModified) { 651 // this.cls = cls; 652 // this.lastModified = lastModified; 653 // } 654 // } 655 656 private File getSourceFile(String name) { 657 File source = null; 658 String filename = name.replace('.', '/') + ".groovy"; 659 String[] paths = getClassPath(); 660 for (int i = 0; i < paths.length; i++) { 661 String pathName = paths[i]; 662 File path = new File(pathName); 663 if (path.exists()) { // case sensitivity depending on OS! 664 if (path.isDirectory()) { 665 File file = new File(path, filename); 666 if (file.exists()) { 667 // file.exists() might be case insensitive. Let's do 668 // case sensitive match for the filename 669 boolean fileExists = false; 670 int sepp = filename.lastIndexOf('/'); 671 String fn = filename; 672 if (sepp >= 0) { 673 fn = filename.substring(++sepp); 674 } 675 File parent = file.getParentFile(); 676 String[] files = parent.list(); 677 for (int j = 0; j < files.length; j++) { 678 if (files[j].equals(fn)) { 679 fileExists = true; 680 break; 681 } 682 } 683 684 if (fileExists) { 685 source = file; 686 break; 687 } 688 } 689 } 690 } 691 } 692 return source; 693 } 694 695 private boolean isSourceNewer(File source, Class cls) { 696 return source.lastModified() > getTimeStamp(cls); 697 } 698 699 public void addClasspath(String path) { 700 additionalPaths.add(path); 701 searchPaths = null; 702 } 703 704 /** 705 * <p>Returns all Groovy classes loaded by this class loader. 706 * 707 * @return all classes loaded by this class loader 708 */ 709 public Class[] getLoadedClasses() { 710 Class[] loadedClasses = null; 711 synchronized (this.loadedClasses) { 712 loadedClasses = (Class[])this.loadedClasses.toArray(new Class[this.loadedClasses.size()]); 713 } 714 return loadedClasses; 715 } 716 }