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 }