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