View Javadoc

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