View Javadoc

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