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.JarURLConnection;
035    import java.net.URLDecoder;
036    import java.util.ArrayList;
037    import java.util.Arrays;
038    import java.util.Collection;
039    import java.util.Collections;
040    import java.util.Enumeration;
041    import java.util.HashMap;
042    import java.util.List;
043    import java.util.Map;
044    import java.util.jar.JarEntry;
045    import java.util.jar.JarInputStream;
046    
047    /**
048     * ClassFinder searches the classpath of the specified classloader for
049     * packages, classes, constructors, methods, or fields with specific annotations.
050     *
051     * For security reasons ASM is used to find the annotations.  Classes are not
052     * loaded unless they match the requirements of a called findAnnotated* method.
053     * Once loaded, these classes are cached.
054     *
055     * The getClassesNotLoaded() method can be used immediately after any find*
056     * method to get a list of classes which matched the find requirements (i.e.
057     * contained the annotation), but were unable to be loaded.
058     *
059     * @author David Blevins
060     * @version $Rev: 661180 $ $Date: 2008-05-29 04:08:46 +0200 (Thu, 29 May 2008) $
061     */
062    public class ClassFinder {
063        private final Map<String, List<Info>> annotated = new HashMap<String, List<Info>>();
064        private final List<ClassInfo> classInfos = new ArrayList<ClassInfo>();
065    
066        private final ClassLoader classLoader;
067        private final List<String> classesNotLoaded = new ArrayList<String>();
068        private final int ASM_FLAGS = ClassReader.SKIP_CODE + ClassReader.SKIP_DEBUG + ClassReader.SKIP_FRAMES;
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 source of classes to scan
083         * @throws Exception if something goes wrong
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 source of classes to scan
093         * @param excludeParent Allegedly excludes classes from parent classloader, whatever that might mean
094         * @throws Exception if something goes wrong.
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 source of classes to scan
105         * @param exclude source of classes to exclude from scanning
106         * @throws Exception if something goes wrong
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(url));
114        }
115    
116        public ClassFinder(ClassLoader classLoader, Collection<URL> urls) {
117            this.classLoader = classLoader;
118    
119            List<String> classNames = new ArrayList<String>();
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<Info>();
152            List<Package> packages = new ArrayList<Package>();
153            for (Class clazz : classes) {
154    
155                Package aPackage = clazz.getPackage();
156                if (aPackage != null && !packages.contains(aPackage)){
157                    infos.add(new PackageInfo(aPackage));
158                    packages.add(aPackage);
159                }
160    
161                ClassInfo classInfo = new ClassInfo(clazz);
162                infos.add(classInfo);
163                classInfos.add(classInfo);
164                for (Method method : clazz.getDeclaredMethods()) {
165                    infos.add(new MethodInfo(classInfo, method));
166                }
167    
168                for (Constructor constructor : clazz.getConstructors()) {
169                    infos.add(new MethodInfo(classInfo, constructor));
170                }
171    
172                for (Field field : clazz.getDeclaredFields()) {
173                    infos.add(new FieldInfo(classInfo, field));
174                }
175            }
176    
177            for (Info info : infos) {
178                for (AnnotationInfo annotation : info.getAnnotations()) {
179                    List<Info> annotationInfos = getAnnotationInfos(annotation.getName());
180                    annotationInfos.add(info);
181                }
182            }
183        }
184    
185        public boolean isAnnotationPresent(Class<? extends Annotation> annotation) {
186            List<Info> infos = annotated.get(annotation.getName());
187            return infos != null && !infos.isEmpty();
188        }
189    
190        /**
191         * Returns a list of classes that could not be loaded in last invoked findAnnotated* method.
192         * <p/>
193         * The list will only contain entries of classes whose byte code matched the requirements
194         * of last invoked find* method, but were unable to be loaded and included in the results.
195         * <p/>
196         * The list returned is unmodifiable.  Once obtained, the returned list will be a live view of the
197         * results from the last findAnnotated* method call.
198         * <p/>
199         * This method is not thread safe.
200         * @return an unmodifiable live view of classes that could not be loaded in previous findAnnotated* call.
201         */
202        public List<String> getClassesNotLoaded() {
203            return Collections.unmodifiableList(classesNotLoaded);
204        }
205    
206        public List<Package> findAnnotatedPackages(Class<? extends Annotation> annotation) {
207            classesNotLoaded.clear();
208            List<Package> packages = new ArrayList<Package>();
209            List<Info> infos = getAnnotationInfos(annotation.getName());
210            for (Info info : infos) {
211                if (info instanceof PackageInfo) {
212                    PackageInfo packageInfo = (PackageInfo) info;
213                    try {
214                        Package pkg = packageInfo.get();
215                        // double check via proper reflection
216                        if (pkg.isAnnotationPresent(annotation)) {
217                            packages.add(pkg);
218                        }
219                    } catch (ClassNotFoundException e) {
220                        classesNotLoaded.add(packageInfo.getName());
221                    }
222                }
223            }
224            return packages;
225        }
226    
227        public List<Class> findAnnotatedClasses(Class<? extends Annotation> annotation) {
228            classesNotLoaded.clear();
229            List<Class> classes = new ArrayList<Class>();
230            List<Info> infos = getAnnotationInfos(annotation.getName());
231            for (Info info : infos) {
232                if (info instanceof ClassInfo) {
233                    ClassInfo classInfo = (ClassInfo) info;
234                    try {
235                        Class clazz = classInfo.get();
236                        // double check via proper reflection
237                        if (clazz.isAnnotationPresent(annotation)) {
238                            classes.add(clazz);
239                        }
240                    } catch (ClassNotFoundException e) {
241                        classesNotLoaded.add(classInfo.getName());
242                    }
243                }
244            }
245            return classes;
246        }
247    
248        public List<Method> findAnnotatedMethods(Class<? extends Annotation> annotation) {
249            classesNotLoaded.clear();
250            List<ClassInfo> seen = new ArrayList<ClassInfo>();
251            List<Method> methods = new ArrayList<Method>();
252            List<Info> infos = getAnnotationInfos(annotation.getName());
253            for (Info info : infos) {
254                if (info instanceof MethodInfo && !info.getName().equals("<init>")) {
255                    MethodInfo methodInfo = (MethodInfo) info;
256                    ClassInfo classInfo = methodInfo.getDeclaringClass();
257    
258                    if (seen.contains(classInfo)) continue;
259    
260                    seen.add(classInfo);
261    
262                    try {
263                        Class clazz = classInfo.get();
264                        for (Method method : clazz.getDeclaredMethods()) {
265                            if (method.isAnnotationPresent(annotation)) {
266                                methods.add(method);
267                            }
268                        }
269                    } catch (ClassNotFoundException e) {
270                        classesNotLoaded.add(classInfo.getName());
271                    }
272                }
273            }
274            return methods;
275        }
276    
277        public List<Constructor> findAnnotatedConstructors(Class<? extends Annotation> annotation) {
278            classesNotLoaded.clear();
279            List<ClassInfo> seen = new ArrayList<ClassInfo>();
280            List<Constructor> constructors = new ArrayList<Constructor>();
281            List<Info> infos = getAnnotationInfos(annotation.getName());
282            for (Info info : infos) {
283                if (info instanceof MethodInfo && info.getName().equals("<init>")) {
284                    MethodInfo methodInfo = (MethodInfo) info;
285                    ClassInfo classInfo = methodInfo.getDeclaringClass();
286    
287                    if (seen.contains(classInfo)) continue;
288    
289                    seen.add(classInfo);
290    
291                    try {
292                        Class clazz = classInfo.get();
293                        for (Constructor constructor : clazz.getConstructors()) {
294                            if (constructor.isAnnotationPresent(annotation)) {
295                                constructors.add(constructor);
296                            }
297                        }
298                    } catch (ClassNotFoundException e) {
299                        classesNotLoaded.add(classInfo.getName());
300                    }
301                }
302            }
303            return constructors;
304        }
305    
306        public List<Field> findAnnotatedFields(Class<? extends Annotation> annotation) {
307            classesNotLoaded.clear();
308            List<ClassInfo> seen = new ArrayList<ClassInfo>();
309            List<Field> fields = new ArrayList<Field>();
310            List<Info> infos = getAnnotationInfos(annotation.getName());
311            for (Info info : infos) {
312                if (info instanceof FieldInfo) {
313                    FieldInfo fieldInfo = (FieldInfo) info;
314                    ClassInfo classInfo = fieldInfo.getDeclaringClass();
315    
316                    if (seen.contains(classInfo)) continue;
317    
318                    seen.add(classInfo);
319    
320                    try {
321                        Class clazz = classInfo.get();
322                        for (Field field : clazz.getDeclaredFields()) {
323                            if (field.isAnnotationPresent(annotation)) {
324                                fields.add(field);
325                            }
326                        }
327                    } catch (ClassNotFoundException e) {
328                        classesNotLoaded.add(classInfo.getName());
329                    }
330                }
331            }
332            return fields;
333        }
334    
335        public List<Class> findClassesInPackage(String packageName, boolean recursive) {
336            classesNotLoaded.clear();
337            List<Class> classes = new ArrayList<Class>();
338            for (ClassInfo classInfo : classInfos) {
339                try {
340                    if (recursive && classInfo.getPackageName().startsWith(packageName)){
341                        classes.add(classInfo.get());
342                    } else if (classInfo.getPackageName().equals(packageName)){
343                        classes.add(classInfo.get());
344                    }
345                } catch (ClassNotFoundException e) {
346                    classesNotLoaded.add(classInfo.getName());
347                }
348            }
349            return classes;
350        }
351    
352        private static Collection<URL> getUrls(ClassLoader classLoader, boolean excludeParent) throws IOException {
353            return getUrls(classLoader, excludeParent? classLoader.getParent() : null);
354        }
355    
356        private static Collection<URL> getUrls(ClassLoader classLoader, ClassLoader excludeParent) throws IOException {
357            UrlSet urlSet = new UrlSet(classLoader);
358            if (excludeParent != null){
359                urlSet = urlSet.exclude(excludeParent);
360            }
361            return urlSet.getUrls();
362        }
363    
364        private List<String> file(URL location) {
365            List<String> classNames = new ArrayList<String>();
366            File dir = new File(URLDecoder.decode(location.getPath()));
367            if (dir.getName().equals("META-INF")) {
368                dir = dir.getParentFile(); // Scrape "META-INF" off
369            }
370            if (dir.isDirectory()) {
371                scanDir(dir, classNames, "");
372            }
373            return classNames;
374        }
375    
376        private void scanDir(File dir, List<String> classNames, String packageName) {
377            File[] files = dir.listFiles();
378            for (File file : files) {
379                if (file.isDirectory()) {
380                    scanDir(file, classNames, packageName + file.getName() + ".");
381                } else if (file.getName().endsWith(".class")) {
382                    String name = file.getName();
383                    name = name.replaceFirst(".class$", "");
384                    classNames.add(packageName + name);
385                }
386            }
387        }
388    
389        private List<String> jar(URL location) throws IOException {
390            String jarPath = location.getFile();
391            if (jarPath.indexOf("!") > -1){
392                jarPath = jarPath.substring(0, jarPath.indexOf("!"));
393            }
394            URL url = new URL(jarPath);
395            InputStream in = url.openStream();
396            try {
397                JarInputStream jarStream = new JarInputStream(in);
398                return jar(jarStream);
399            } finally {
400                in.close();
401            }
402        }
403    
404        private List<String> jar(JarInputStream jarStream) throws IOException {
405            List<String> classNames = new ArrayList<String>();
406    
407            JarEntry entry;
408            while ((entry = jarStream.getNextJarEntry()) != null) {
409                if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
410                    continue;
411                }
412                String className = entry.getName();
413                className = className.replaceFirst(".class$", "");
414                className = className.replace('/', '.');
415                classNames.add(className);
416            }
417    
418            return classNames;
419        }
420    
421        public class Annotatable {
422            private final List<AnnotationInfo> annotations = new ArrayList<AnnotationInfo>();
423    
424            public Annotatable(AnnotatedElement element) {
425                for (Annotation annotation : element.getAnnotations()) {
426                    annotations.add(new AnnotationInfo(annotation.annotationType().getName()));
427                }
428            }
429    
430            public Annotatable() {
431            }
432    
433            public List<AnnotationInfo> getAnnotations() {
434                return annotations;
435            }
436    
437        }
438    
439        public static interface Info {
440            String getName();
441    
442            List<AnnotationInfo> getAnnotations();
443        }
444    
445        public class PackageInfo extends Annotatable implements Info {
446            private final String name;
447            private final ClassInfo info;
448            private final Package pkg;
449    
450            public PackageInfo(Package pkg){
451                super(pkg);
452                this.pkg = pkg;
453                this.name = pkg.getName();
454                this.info = null;
455            }
456    
457            public PackageInfo(String name) {
458                info = new ClassInfo(name, null);
459                this.name = name;
460                this.pkg = null;
461            }
462    
463            public String getName() {
464                return name;
465            }
466    
467            public Package get() throws ClassNotFoundException {
468                return (pkg != null)?pkg:info.get().getPackage();
469            }
470        }
471    
472        public class ClassInfo extends Annotatable implements Info {
473            private final String name;
474            private final List<MethodInfo> methods = new ArrayList<MethodInfo>();
475            private final List<MethodInfo> constructors = new ArrayList<MethodInfo>();
476            private final String superType;
477            private final List<String> interfaces = new ArrayList<String>();
478            private final List<FieldInfo> fields = new ArrayList<FieldInfo>();
479            private Class<?> clazz;
480            private ClassNotFoundException notFound;
481    
482            public ClassInfo(Class clazz) {
483                super(clazz);
484                this.clazz = clazz;
485                this.name = clazz.getName();
486                Class superclass = clazz.getSuperclass();
487                this.superType = superclass != null ? superclass.getName(): null;
488            }
489    
490            public ClassInfo(String name, String superType) {
491                this.name = name;
492                this.superType = superType;
493            }
494    
495            public String getPackageName(){
496                return name.substring(name.lastIndexOf(".")+1, name.length());
497            }
498    
499            public List<MethodInfo> getConstructors() {
500                return constructors;
501            }
502    
503            public List<String> getInterfaces() {
504                return interfaces;
505            }
506    
507            public List<FieldInfo> getFields() {
508                return fields;
509            }
510    
511            public List<MethodInfo> getMethods() {
512                return methods;
513            }
514    
515            public String getName() {
516                return name;
517            }
518    
519            public String getSuperType() {
520                return superType;
521            }
522    
523            public Class get() throws ClassNotFoundException {
524                if (clazz != null) return clazz;
525                if (notFound != null) throw notFound;
526                try {
527                    this.clazz = classLoader.loadClass(name);
528                    return clazz;
529                } catch (ClassNotFoundException notFound) {
530                    classesNotLoaded.add(name);
531                    this.notFound = notFound;
532                    throw notFound;
533                }
534            }
535    
536            public String toString() {
537                return name;
538            }
539        }
540    
541        public class MethodInfo extends Annotatable implements Info {
542            private final ClassInfo declaringClass;
543            private final String returnType;
544            private final String name;
545            private final List<List<AnnotationInfo>> parameterAnnotations = new ArrayList<List<AnnotationInfo>>();
546    
547            public MethodInfo(ClassInfo info, Constructor constructor){
548                super(constructor);
549                this.declaringClass = info;
550                this.name = "<init>";
551                this.returnType = Void.TYPE.getName();
552            }
553    
554            public MethodInfo(ClassInfo info, Method method){
555                super(method);
556                this.declaringClass = info;
557                this.name = method.getName();
558                this.returnType = method.getReturnType().getName();
559            }
560    
561            public MethodInfo(ClassInfo declarignClass, String name, String returnType) {
562                this.declaringClass = declarignClass;
563                this.name = name;
564                this.returnType = returnType;
565            }
566    
567            public List<List<AnnotationInfo>> getParameterAnnotations() {
568                return parameterAnnotations;
569            }
570    
571            public List<AnnotationInfo> getParameterAnnotations(int index) {
572                if (index >= parameterAnnotations.size()) {
573                    for (int i = parameterAnnotations.size(); i <= index; i++) {
574                        List<AnnotationInfo> annotationInfos = new ArrayList<AnnotationInfo>();
575                        parameterAnnotations.add(i, annotationInfos);
576                    }
577                }
578                return parameterAnnotations.get(index);
579            }
580    
581            public String getName() {
582                return name;
583            }
584    
585            public ClassInfo getDeclaringClass() {
586                return declaringClass;
587            }
588    
589            public String getReturnType() {
590                return returnType;
591            }
592    
593            public String toString() {
594                return declaringClass + "@" + name;
595            }
596        }
597    
598        public class FieldInfo extends Annotatable implements Info {
599            private final String name;
600            private final String type;
601            private final ClassInfo declaringClass;
602    
603            public FieldInfo(ClassInfo info, Field field){
604                super(field);
605                this.declaringClass = info;
606                this.name = field.getName();
607                this.type = field.getType().getName();
608            }
609    
610            public FieldInfo(ClassInfo declaringClass, String name, String type) {
611                this.declaringClass = declaringClass;
612                this.name = name;
613                this.type = type;
614            }
615    
616            public String getName() {
617                return name;
618            }
619    
620            public ClassInfo getDeclaringClass() {
621                return declaringClass;
622            }
623    
624            public String getType() {
625                return type;
626            }
627    
628            public String toString() {
629                return declaringClass + "#" + name;
630            }
631        }
632    
633        public class AnnotationInfo extends Annotatable implements Info {
634            private final String name;
635    
636            public AnnotationInfo(Annotation annotation){
637                this(annotation.getClass().getName());
638            }
639    
640            public AnnotationInfo(Class<? extends Annotation> annotation) {
641                this.name = annotation.getName().intern();
642            }
643    
644            public AnnotationInfo(String name) {
645                name = name.replaceAll("^L|;$", "");
646                name = name.replace('/', '.');
647                this.name = name.intern();
648            }
649    
650            public String getName() {
651                return name;
652            }
653    
654            public String toString() {
655                return name;
656            }
657        }
658    
659        private List<Info> getAnnotationInfos(String name) {
660            List<Info> infos = annotated.get(name);
661            if (infos == null) {
662                infos = new ArrayList<Info>();
663                annotated.put(name, infos);
664            }
665            return infos;
666        }
667    
668        private void readClassDef(String className) {
669            if (!className.endsWith(".class")) {
670                className = className.replace('.', '/') + ".class";
671            }
672            try {
673                URL resource = classLoader.getResource(className);
674                if (resource != null) {
675                    InputStream in = resource.openStream();
676                    try {
677                        ClassReader classReader = new ClassReader(in);
678                        classReader.accept(new InfoBuildingVisitor(), ASM_FLAGS);
679                    } finally {
680                        in.close();
681                    }
682                } else {
683                    new Exception("Could not load " + className).printStackTrace();
684                }
685            } catch (IOException e) {
686                e.printStackTrace();
687            }
688    
689        }
690    
691        public class InfoBuildingVisitor extends EmptyVisitor {
692            private Info info;
693    
694            public InfoBuildingVisitor() {
695            }
696    
697            public InfoBuildingVisitor(Info info) {
698                this.info = info;
699            }
700    
701            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
702                if (name.endsWith("package-info")) {
703                    info = new PackageInfo(javaName(name));
704                } else {
705                    ClassInfo classInfo = new ClassInfo(javaName(name), javaName(superName));
706    
707                    for (String interfce : interfaces) {
708                        classInfo.getInterfaces().add(javaName(interfce));
709                    }
710                    info = classInfo;
711                    classInfos.add(classInfo);
712                }
713            }
714    
715            private String javaName(String name) {
716                return (name == null)? null:name.replace('/', '.');
717            }
718    
719            public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
720                AnnotationInfo annotationInfo = new AnnotationInfo(desc);
721                info.getAnnotations().add(annotationInfo);
722                getAnnotationInfos(annotationInfo.getName()).add(info);
723                return new InfoBuildingVisitor(annotationInfo);
724            }
725    
726            public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
727                ClassInfo classInfo = ((ClassInfo) info);
728                FieldInfo fieldInfo = new FieldInfo(classInfo, name, desc);
729                classInfo.getFields().add(fieldInfo);
730                return new InfoBuildingVisitor(fieldInfo);
731            }
732    
733            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
734                ClassInfo classInfo = ((ClassInfo) info);
735                MethodInfo methodInfo = new MethodInfo(classInfo, name, desc);
736                classInfo.getMethods().add(methodInfo);
737                return new InfoBuildingVisitor(methodInfo);
738            }
739    
740            public AnnotationVisitor visitParameterAnnotation(int param, String desc, boolean visible) {
741                MethodInfo methodInfo = ((MethodInfo) info);
742                List<AnnotationInfo> annotationInfos = methodInfo.getParameterAnnotations(param);
743                AnnotationInfo annotationInfo = new AnnotationInfo(desc);
744                annotationInfos.add(annotationInfo);
745                return new InfoBuildingVisitor(annotationInfo);
746            }
747        }
748    }