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 }