View Javadoc

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