001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    package org.apache.xbean.finder;
018    
019    import org.objectweb.asm.AnnotationVisitor;
020    import org.objectweb.asm.ClassReader;
021    import org.objectweb.asm.FieldVisitor;
022    import org.objectweb.asm.MethodVisitor;
023    import org.objectweb.asm.commons.EmptyVisitor;
024    
025    import java.io.File;
026    import java.io.IOException;
027    import java.io.InputStream;
028    import java.lang.annotation.Annotation;
029    import java.lang.reflect.Constructor;
030    import java.lang.reflect.Field;
031    import java.lang.reflect.Method;
032    import java.lang.reflect.AnnotatedElement;
033    import java.net.URL;
034    import java.net.MalformedURLException;
035    import java.net.URLConnection;
036    import java.net.JarURLConnection;
037    import java.util.ArrayList;
038    import java.util.Arrays;
039    import java.util.Collection;
040    import java.util.Collections;
041    import java.util.Enumeration;
042    import java.util.HashMap;
043    import java.util.List;
044    import java.util.Map;
045    import java.util.jar.JarEntry;
046    import java.util.jar.JarInputStream;
047    
048    /**
049     * ClassFinder searches the classpath of the specified classloader for
050     * packages, classes, constructors, methods, or fields with specific annotations.
051     *
052     * For security reasons ASM is used to find the annotations.  Classes are not
053     * loaded unless they match the requirements of a called findAnnotated* method.
054     * Once loaded, these classes are cached.
055     *
056     * The getClassesNotLoaded() method can be used immediately after any find*
057     * method to get a list of classes which matched the find requirements (i.e.
058     * contained the annotation), but were unable to be loaded.
059     *
060     * @author David Blevins
061     * @version $Rev: 483277 $ $Date: 2006-12-06 23:53:30 +0100 (mer., 06 déc. 2006) $
062     */
063    public class ClassFinder {
064        private final Map<String, List<Info>> annotated = new HashMap();
065        private final List<ClassInfo> classInfos = new ArrayList();
066    
067        private final ClassLoader classLoader;
068        private final List<String> classesNotLoaded = new ArrayList();
069    
070        /**
071         * Creates a ClassFinder that will search the urls in the specified classloader
072         * excluding the urls in the classloader's parent.
073         *
074         * To include the parent classloader, use:
075         *
076         *    new ClassFinder(classLoader, false);
077         *
078         * To exclude the parent's parent, use:
079         *
080         *    new ClassFinder(classLoader, classLoader.getParent().getParent());
081         *
082         * @param classLoader
083         * @throws Exception
084         */
085        public ClassFinder(ClassLoader classLoader) throws Exception {
086            this(classLoader, true);
087        }
088    
089        /**
090         * Creates a ClassFinder that will search the urls in the specified classloader.
091         *
092         * @param classLoader
093         * @param excludeParent
094         * @throws Exception
095         */
096        public ClassFinder(ClassLoader classLoader, boolean excludeParent) throws Exception {
097            this(classLoader, getUrls(classLoader, excludeParent));
098        }
099    
100        /**
101         * Creates a ClassFinder that will search the urls in the specified classloader excluding
102         * the urls in the 'exclude' classloader.
103         *
104         * @param classLoader
105         * @param exclude
106         * @throws Exception
107         */
108        public ClassFinder(ClassLoader classLoader, ClassLoader exclude) throws Exception {
109            this(classLoader, getUrls(classLoader, exclude));
110        }
111    
112        public ClassFinder(ClassLoader classLoader, URL url) {
113            this(classLoader, Arrays.asList(new URL[]{url}));
114        }
115    
116        public ClassFinder(ClassLoader classLoader, Collection<URL> urls) {
117            this.classLoader = classLoader;
118    
119            List<String> classNames = new ArrayList();
120            for (URL location : urls) {
121                try {
122                    if (location.getProtocol().equals("jar")) {
123                        classNames.addAll(jar(location));
124                    } else if (location.getProtocol().equals("file")) {
125                        try {
126                            // See if it's actually a jar
127                            URL jarUrl = new URL("jar", "", location.toExternalForm() + "!/");
128                            JarURLConnection juc = (JarURLConnection) jarUrl.openConnection();
129                            juc.getJarFile();
130                            classNames.addAll(jar(jarUrl));
131                        } catch (IOException e) {
132                            classNames.addAll(file(location));
133                        }
134                    }
135                } catch (Exception e) {
136                    e.printStackTrace();
137                }
138            }
139    
140            for (String className : classNames) {
141                readClassDef(className);
142            }
143        }
144    
145        public ClassFinder(Class... classes){
146            this(Arrays.asList(classes));
147        }
148    
149        public ClassFinder(List<Class> classes){
150            this.classLoader = null;
151            List<Info> infos = new ArrayList();
152            List<Package> packages = new ArrayList();
153            for (Class clazz : classes) {
154                if (!packages.contains(clazz.getPackage())){
155                    infos.add(new PackageInfo(clazz.getPackage()));
156                    packages.add(clazz.getPackage());
157                }
158    
159                ClassInfo classInfo = new ClassInfo(clazz);
160                infos.add(classInfo);
161                classInfos.add(classInfo);
162                for (Method method : clazz.getDeclaredMethods()) {
163                    infos.add(new MethodInfo(classInfo, method));
164                }
165    
166                for (Constructor constructor : clazz.getConstructors()) {
167                    infos.add(new MethodInfo(classInfo, constructor));
168                }
169    
170                for (Field field : clazz.getDeclaredFields()) {
171                    infos.add(new FieldInfo(classInfo, field));
172                }
173            }
174    
175            for (Info info : infos) {
176                for (AnnotationInfo annotation : info.getAnnotations()) {
177                    List<Info> annotationInfos = getAnnotationInfos(annotation.getName());
178                    annotationInfos.add(info);
179                }
180            }
181        }
182    
183        /**
184         * Returns a list of classes that could not be loaded in last invoked findAnnotated* method.
185         * <p/>
186         * The list will only contain entries of classes whose byte code matched the requirements
187         * of last invoked find* method, but were unable to be loaded and included in the results.
188         * <p/>
189         * The list returned is unmodifiable and the results of this method will change
190         * after each invocation of a findAnnotated* method.
191         * <p/>
192         * This method is not thread safe.
193         */
194        public List<String> getClassesNotLoaded() {
195            return Collections.unmodifiableList(classesNotLoaded);
196        }
197    
198        public List<Package> findAnnotatedPackages(Class<? extends Annotation> annotation) {
199            classesNotLoaded.clear();
200            List<Package> packages = new ArrayList<Package>();
201            List<Info> infos = getAnnotationInfos(annotation.getName());
202            for (Info info : infos) {
203                if (info instanceof PackageInfo) {
204                    PackageInfo packageInfo = (PackageInfo) info;
205                    try {
206                        Package pkg = packageInfo.get();
207                        // double check via proper reflection
208                        if (pkg.isAnnotationPresent(annotation)) {
209                            packages.add(pkg);
210                        }
211                    } catch (ClassNotFoundException e) {
212                        classesNotLoaded.add(packageInfo.getName());
213                    }
214                }
215            }
216            return packages;
217        }
218    
219        public List<Class> findAnnotatedClasses(Class<? extends Annotation> annotation) {
220            classesNotLoaded.clear();
221            List<Class> classes = new ArrayList<Class>();
222            List<Info> infos = getAnnotationInfos(annotation.getName());
223            for (Info info : infos) {
224                if (info instanceof ClassInfo) {
225                    ClassInfo classInfo = (ClassInfo) info;
226                    try {
227                        Class clazz = classInfo.get();
228                        // double check via proper reflection
229                        if (clazz.isAnnotationPresent(annotation)) {
230                            classes.add(clazz);
231                        }
232                    } catch (ClassNotFoundException e) {
233                        classesNotLoaded.add(classInfo.getName());
234                    }
235                }
236            }
237            return classes;
238        }
239    
240        public List<Method> findAnnotatedMethods(Class<? extends Annotation> annotation) {
241            classesNotLoaded.clear();
242            List<ClassInfo> seen = new ArrayList<ClassInfo>();
243            List<Method> methods = new ArrayList<Method>();
244            List<Info> infos = getAnnotationInfos(annotation.getName());
245            for (Info info : infos) {
246                if (info instanceof MethodInfo && !info.getName().equals("<init>")) {
247                    MethodInfo methodInfo = (MethodInfo) info;
248                    ClassInfo classInfo = methodInfo.getDeclaringClass();
249    
250                    if (seen.contains(classInfo)) continue;
251    
252                    seen.add(classInfo);
253    
254                    try {
255                        Class clazz = classInfo.get();
256                        for (Method method : clazz.getDeclaredMethods()) {
257                            if (method.isAnnotationPresent(annotation)) {
258                                methods.add(method);
259                            }
260                        }
261                    } catch (ClassNotFoundException e) {
262                        classesNotLoaded.add(classInfo.getName());
263                    }
264                }
265            }
266            return methods;
267        }
268    
269        public List<Constructor> findAnnotatedConstructors(Class<? extends Annotation> annotation) {
270            classesNotLoaded.clear();
271            List<ClassInfo> seen = new ArrayList<ClassInfo>();
272            List<Constructor> constructors = new ArrayList<Constructor>();
273            List<Info> infos = getAnnotationInfos(annotation.getName());
274            for (Info info : infos) {
275                if (info instanceof MethodInfo && info.getName().equals("<init>")) {
276                    MethodInfo methodInfo = (MethodInfo) info;
277                    ClassInfo classInfo = methodInfo.getDeclaringClass();
278    
279                    if (seen.contains(classInfo)) continue;
280    
281                    seen.add(classInfo);
282    
283                    try {
284                        Class clazz = classInfo.get();
285                        for (Constructor constructor : clazz.getConstructors()) {
286                            if (constructor.isAnnotationPresent(annotation)) {
287                                constructors.add(constructor);
288                            }
289                        }
290                    } catch (ClassNotFoundException e) {
291                        classesNotLoaded.add(classInfo.getName());
292                    }
293                }
294            }
295            return constructors;
296        }
297    
298        public List<Field> findAnnotatedFields(Class<? extends Annotation> annotation) {
299            classesNotLoaded.clear();
300            List<ClassInfo> seen = new ArrayList<ClassInfo>();
301            List<Field> fields = new ArrayList<Field>();
302            List<Info> infos = getAnnotationInfos(annotation.getName());
303            for (Info info : infos) {
304                if (info instanceof FieldInfo) {
305                    FieldInfo fieldInfo = (FieldInfo) info;
306                    ClassInfo classInfo = fieldInfo.getDeclaringClass();
307    
308                    if (seen.contains(classInfo)) continue;
309    
310                    seen.add(classInfo);
311    
312                    try {
313                        Class clazz = classInfo.get();
314                        for (Field field : clazz.getDeclaredFields()) {
315                            if (field.isAnnotationPresent(annotation)) {
316                                fields.add(field);
317                            }
318                        }
319                    } catch (ClassNotFoundException e) {
320                        classesNotLoaded.add(classInfo.getName());
321                    }
322                }
323            }
324            return fields;
325        }
326    
327        public List<Class> findClassesInPackage(String packageName, boolean recursive) {
328            classesNotLoaded.clear();
329            List<Class> classes = new ArrayList();
330            for (ClassInfo classInfo : classInfos) {
331                try {
332                    if (recursive && classInfo.getPackageName().startsWith(packageName)){
333                        classes.add(classInfo.get());
334                    } else if (classInfo.getPackageName().equals(packageName)){
335                        classes.add(classInfo.get());
336                    }
337                } catch (ClassNotFoundException e) {
338                    classesNotLoaded.add(classInfo.getName());
339                }
340            }
341            return classes;
342        }
343    
344        private static Collection<URL> getUrls(ClassLoader classLoader, boolean excludeParent) throws IOException {
345            return getUrls(classLoader, excludeParent? classLoader.getParent() : null);
346        }
347    
348        private static Collection<URL> getUrls(ClassLoader classLoader, ClassLoader excludeParent) throws IOException {
349            Map<String, URL> urls = toMap(classLoader.getResources("META-INF"));
350    
351            if (excludeParent != null) {
352                Map<String, URL> parentUrls = toMap(excludeParent.getResources("META-INF"));
353                for (String url : parentUrls.keySet()) {
354                    urls.remove(url);
355                }
356            }
357    
358            return urls.values();
359        }
360    
361        private static Map<String, URL> toMap(Enumeration<URL> enumeration) {
362            Map<String, URL> urls = new HashMap();
363            while (enumeration.hasMoreElements()) {
364                URL url = enumeration.nextElement();
365                urls.put(url.toExternalForm(), url);
366            }
367            return urls;
368        }
369    
370        private List<String> file(URL location) {
371            List<String> classNames = new ArrayList();
372            File dir = new File(location.getPath());
373            if (dir.getName().equals("META-INF")) {
374                dir = dir.getParentFile(); // Scrape "META-INF" off
375            }
376            if (dir.isDirectory()) {
377                scanDir(dir, classNames, "");
378            }
379            return classNames;
380        }
381    
382        private void scanDir(File dir, List<String> classNames, String packageName) {
383            File[] files = dir.listFiles();
384            for (File file : files) {
385                if (file.isDirectory()) {
386                    scanDir(file, classNames, packageName + file.getName() + ".");
387                } else if (file.getName().endsWith(".class")) {
388                    String name = file.getName();
389                    name = name.replaceFirst(".class$", "");
390                    classNames.add(packageName + name);
391                }
392            }
393        }
394    
395        private List<String> jar(URL location) throws IOException {
396            String jarPath = location.getFile();
397            if (jarPath.indexOf("!") > -1){
398                jarPath = jarPath.substring(0, jarPath.indexOf("!"));
399            }
400            URL url = new URL(jarPath);
401            InputStream in = url.openStream();
402            try {
403                JarInputStream jarStream = new JarInputStream(in);
404                return jar(jarStream);
405            } finally {
406                in.close();
407            }
408        }
409    
410        private List<String> jar(JarInputStream jarStream) throws IOException {
411            List<String> classNames = new ArrayList();
412    
413            JarEntry entry;
414            while ((entry = jarStream.getNextJarEntry()) != null) {
415                if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
416                    continue;
417                }
418                String className = entry.getName();
419                className = className.replaceFirst(".class$", "");
420                className = className.replace('/', '.');
421                classNames.add(className);
422            }
423    
424            return classNames;
425        }
426    
427        public class Annotatable {
428            private final List<AnnotationInfo> annotations = new ArrayList();
429    
430            public Annotatable(AnnotatedElement element) {
431                for (Annotation annotation : element.getAnnotations()) {
432                    annotations.add(new AnnotationInfo(annotation.annotationType().getName()));
433                }
434            }
435    
436            public Annotatable() {
437            }
438    
439            public List<AnnotationInfo> getAnnotations() {
440                return annotations;
441            }
442    
443        }
444    
445        public static interface Info {
446            String getName();
447    
448            List<AnnotationInfo> getAnnotations();
449        }
450    
451        public class PackageInfo extends Annotatable implements Info {
452            private final String name;
453            private final ClassInfo info;
454            private final Package pkg;
455    
456            public PackageInfo(Package pkg){
457                super(pkg);
458                this.pkg = pkg;
459                this.name = pkg.getName();
460                this.info = null;
461            }
462    
463            public PackageInfo(String name) {
464                info = new ClassInfo(name, null);
465                this.name = name;
466                this.pkg = null;
467            }
468    
469            public String getName() {
470                return name;
471            }
472    
473            public Package get() throws ClassNotFoundException {
474                return (pkg != null)?pkg:info.get().getPackage();
475            }
476        }
477    
478        public class ClassInfo extends Annotatable implements Info {
479            private final String name;
480            private final List<MethodInfo> methods = new ArrayList();
481            private final List<MethodInfo> constructors = new ArrayList();
482            private final String superType;
483            private final List<String> interfaces = new ArrayList();
484            private final List<FieldInfo> fields = new ArrayList();
485            private Class<?> clazz;
486            private ClassNotFoundException notFound;
487    
488            public ClassInfo(Class clazz) {
489                super(clazz);
490                this.clazz = clazz;
491                this.name = clazz.getName();
492                Class superclass = clazz.getSuperclass();
493                this.superType = superclass != null ? superclass.getName(): null;
494            }
495    
496            public ClassInfo(String name, String superType) {
497                this.name = name;
498                this.superType = superType;
499            }
500    
501            public String getPackageName(){
502                return name.substring(name.lastIndexOf(".")+1, name.length());
503            }
504    
505            public List<MethodInfo> getConstructors() {
506                return constructors;
507            }
508    
509            public List<String> getInterfaces() {
510                return interfaces;
511            }
512    
513            public List<FieldInfo> getFields() {
514                return fields;
515            }
516    
517            public List<MethodInfo> getMethods() {
518                return methods;
519            }
520    
521            public String getName() {
522                return name;
523            }
524    
525            public String getSuperType() {
526                return superType;
527            }
528    
529            public Class get() throws ClassNotFoundException {
530                if (clazz != null) return clazz;
531                if (notFound != null) throw notFound;
532                try {
533                    this.clazz = classLoader.loadClass(name);
534                    return clazz;
535                } catch (ClassNotFoundException notFound) {
536                    classesNotLoaded.add(name);
537                    this.notFound = notFound;
538                    throw notFound;
539                }
540            }
541    
542            public String toString() {
543                return name;
544            }
545        }
546    
547        public class MethodInfo extends Annotatable implements Info {
548            private final ClassInfo declaringClass;
549            private final String returnType;
550            private final String name;
551            private final List<List<AnnotationInfo>> parameterAnnotations = new ArrayList();
552    
553            public MethodInfo(ClassInfo info, Constructor constructor){
554                super(constructor);
555                this.declaringClass = info;
556                this.name = "<init>";
557                this.returnType = Void.TYPE.getName();
558            }
559    
560            public MethodInfo(ClassInfo info, Method method){
561                super(method);
562                this.declaringClass = info;
563                this.name = method.getName();
564                this.returnType = method.getReturnType().getName();
565            }
566    
567            public MethodInfo(ClassInfo declarignClass, String name, String returnType) {
568                this.declaringClass = declarignClass;
569                this.name = name;
570                this.returnType = returnType;
571            }
572    
573            public List<List<AnnotationInfo>> getParameterAnnotations() {
574                return parameterAnnotations;
575            }
576    
577            public List<AnnotationInfo> getParameterAnnotations(int index) {
578                if (index >= parameterAnnotations.size()) {
579                    for (int i = parameterAnnotations.size(); i <= index; i++) {
580                        List<AnnotationInfo> annotationInfos = new ArrayList<AnnotationInfo>();
581                        parameterAnnotations.add(i, annotationInfos);
582                    }
583                }
584                return parameterAnnotations.get(index);
585            }
586    
587            public String getName() {
588                return name;
589            }
590    
591            public ClassInfo getDeclaringClass() {
592                return declaringClass;
593            }
594    
595            public String getReturnType() {
596                return returnType;
597            }
598    
599            public String toString() {
600                return declaringClass + "@" + name;
601            }
602        }
603    
604        public class FieldInfo extends Annotatable implements Info {
605            private final String name;
606            private final String type;
607            private final ClassInfo declaringClass;
608    
609            public FieldInfo(ClassInfo info, Field field){
610                super(field);
611                this.declaringClass = info;
612                this.name = field.getName();
613                this.type = field.getType().getName();
614            }
615    
616            public FieldInfo(ClassInfo declaringClass, String name, String type) {
617                this.declaringClass = declaringClass;
618                this.name = name;
619                this.type = type;
620            }
621    
622            public String getName() {
623                return name;
624            }
625    
626            public ClassInfo getDeclaringClass() {
627                return declaringClass;
628            }
629    
630            public String getType() {
631                return type;
632            }
633    
634            public String toString() {
635                return declaringClass + "#" + name;
636            }
637        }
638    
639        public class AnnotationInfo extends Annotatable implements Info {
640            private final String name;
641    
642            public AnnotationInfo(Annotation annotation){
643                this(annotation.getClass().getName());
644            }
645    
646            public AnnotationInfo(Class<? extends Annotation> annotation) {
647                this.name = annotation.getName().intern();
648            }
649    
650            public AnnotationInfo(String name) {
651                name = name.replaceAll("^L|;$", "");
652                name = name.replace('/', '.');
653                this.name = name.intern();
654            }
655    
656            public String getName() {
657                return name;
658            }
659    
660            public String toString() {
661                return name.toString();
662            }
663        }
664    
665        private List<Info> getAnnotationInfos(String name) {
666            List<Info> infos = annotated.get(name);
667            if (infos == null) {
668                infos = new ArrayList();
669                annotated.put(name, infos);
670            }
671            return infos;
672        }
673    
674        private void readClassDef(String className) {
675            if (!className.endsWith(".class")) {
676                className = className.replace('.', '/') + ".class";
677            }
678            ClassReader classReader = null;
679            try {
680                URL resource = classLoader.getResource(className);
681                classReader = new ClassReader(resource.openStream());
682            } catch (IOException e) {
683                e.printStackTrace();
684            }
685    
686    //        classReader.accept(new ASMifierClassVisitor(new PrintWriter(System.out)), true);
687            classReader.accept(new InfoBuildingVisitor(), true);
688        }
689    
690        public class InfoBuildingVisitor extends EmptyVisitor {
691            private Info info;
692    
693            public InfoBuildingVisitor() {
694            }
695    
696            public InfoBuildingVisitor(Info info) {
697                this.info = info;
698            }
699    
700            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
701                if (name.endsWith("package-info")) {
702                    info = new PackageInfo(javaName(name));
703                } else {
704                    ClassInfo classInfo = new ClassInfo(javaName(name), javaName(superName));
705    
706                    for (String interfce : interfaces) {
707                        classInfo.getInterfaces().add(javaName(interfce));
708                    }
709                    info = classInfo;
710                    classInfos.add(classInfo);
711                }
712            }
713    
714            private String javaName(String name) {
715                return (name == null)? null:name.replace('/', '.');
716            }
717    
718            public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
719                AnnotationInfo annotationInfo = new AnnotationInfo(desc);
720                info.getAnnotations().add(annotationInfo);
721                getAnnotationInfos(annotationInfo.getName()).add(info);
722                return new InfoBuildingVisitor(annotationInfo);
723            }
724    
725            public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
726                ClassInfo classInfo = ((ClassInfo) info);
727                FieldInfo fieldInfo = new FieldInfo(classInfo, name, desc);
728                classInfo.getFields().add(fieldInfo);
729                return new InfoBuildingVisitor(fieldInfo);
730            }
731    
732            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
733                ClassInfo classInfo = ((ClassInfo) info);
734                MethodInfo methodInfo = new MethodInfo(classInfo, name, desc);
735                classInfo.getMethods().add(methodInfo);
736                return new InfoBuildingVisitor(methodInfo);
737            }
738    
739            public AnnotationVisitor visitParameterAnnotation(int param, String desc, boolean visible) {
740                MethodInfo methodInfo = ((MethodInfo) info);
741                List<AnnotationInfo> annotationInfos = methodInfo.getParameterAnnotations(param);
742                AnnotationInfo annotationInfo = new AnnotationInfo(desc);
743                annotationInfos.add(annotationInfo);
744                return new InfoBuildingVisitor(annotationInfo);
745            }
746        }
747    }