View Javadoc

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