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: 634087 $ $Date: 2008-03-06 00:05:23 +0000 (Thu, 06 Mar 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    
069        /**
070         * Creates a ClassFinder that will search the urls in the specified classloader
071         * excluding the urls in the classloader's parent.
072         *
073         * To include the parent classloader, use:
074         *
075         *    new ClassFinder(classLoader, false);
076         *
077         * To exclude the parent's parent, use:
078         *
079         *    new ClassFinder(classLoader, classLoader.getParent().getParent());
080         *
081         * @param classLoader source of classes to scan
082         * @throws Exception if something goes wrong
083         */
084        public ClassFinder(ClassLoader classLoader) throws Exception {
085            this(classLoader, true);
086        }
087    
088        /**
089         * Creates a ClassFinder that will search the urls in the specified classloader.
090         *
091         * @param classLoader source of classes to scan
092         * @param excludeParent Allegedly excludes classes from parent classloader, whatever that might mean
093         * @throws Exception if something goes wrong.
094         */
095        public ClassFinder(ClassLoader classLoader, boolean excludeParent) throws Exception {
096            this(classLoader, getUrls(classLoader, excludeParent));
097        }
098    
099        /**
100         * Creates a ClassFinder that will search the urls in the specified classloader excluding
101         * the urls in the 'exclude' classloader.
102         *
103         * @param classLoader source of classes to scan
104         * @param exclude source of classes to exclude from scanning
105         * @throws Exception if something goes wrong
106         */
107        public ClassFinder(ClassLoader classLoader, ClassLoader exclude) throws Exception {
108            this(classLoader, getUrls(classLoader, exclude));
109        }
110    
111        public ClassFinder(ClassLoader classLoader, URL url) {
112            this(classLoader, Arrays.asList(url));
113        }
114    
115        public ClassFinder(ClassLoader classLoader, Collection<URL> urls) {
116            this.classLoader = classLoader;
117    
118            List<String> classNames = new ArrayList<String>();
119            for (URL location : urls) {
120                try {
121                    if (location.getProtocol().equals("jar")) {
122                        classNames.addAll(jar(location));
123                    } else if (location.getProtocol().equals("file")) {
124                        try {
125                            // See if it's actually a jar
126                            URL jarUrl = new URL("jar", "", location.toExternalForm() + "!/");
127                            JarURLConnection juc = (JarURLConnection) jarUrl.openConnection();
128                            juc.getJarFile();
129                            classNames.addAll(jar(jarUrl));
130                        } catch (IOException e) {
131                            classNames.addAll(file(location));
132                        }
133                    }
134                } catch (Exception e) {
135                    e.printStackTrace();
136                }
137            }
138    
139            for (String className : classNames) {
140                readClassDef(className);
141            }
142        }
143    
144        public ClassFinder(Class... classes){
145            this(Arrays.asList(classes));
146        }
147    
148        public ClassFinder(List<Class> classes){
149            this.classLoader = null;
150            List<Info> infos = new ArrayList<Info>();
151            List<Package> packages = new ArrayList<Package>();
152            for (Class clazz : classes) {
153    
154                Package aPackage = clazz.getPackage();
155                if (aPackage != null && !packages.contains(aPackage)){
156                    infos.add(new PackageInfo(aPackage));
157                    packages.add(aPackage);
158                }
159    
160                ClassInfo classInfo = new ClassInfo(clazz);
161                infos.add(classInfo);
162                classInfos.add(classInfo);
163                for (Method method : clazz.getDeclaredMethods()) {
164                    infos.add(new MethodInfo(classInfo, method));
165                }
166    
167                for (Constructor constructor : clazz.getConstructors()) {
168                    infos.add(new MethodInfo(classInfo, constructor));
169                }
170    
171                for (Field field : clazz.getDeclaredFields()) {
172                    infos.add(new FieldInfo(classInfo, field));
173                }
174            }
175    
176            for (Info info : infos) {
177                for (AnnotationInfo annotation : info.getAnnotations()) {
178                    List<Info> annotationInfos = getAnnotationInfos(annotation.getName());
179                    annotationInfos.add(info);
180                }
181            }
182        }
183    
184        public boolean isAnnotationPresent(Class<? extends Annotation> annotation) {
185            List<Info> infos = annotated.get(annotation.getName());
186            return infos != null && !infos.isEmpty();
187        }
188    
189        /**
190         * Returns a list of classes that could not be loaded in last invoked findAnnotated* method.
191         * <p/>
192         * The list will only contain entries of classes whose byte code matched the requirements
193         * of last invoked find* method, but were unable to be loaded and included in the results.
194         * <p/>
195         * The list returned is unmodifiable.  Once obtained, the returned list will be a live view of the
196         * results from the last findAnnotated* method call.
197         * <p/>
198         * This method is not thread safe.
199         * @return an unmodifiable live view of classes that could not be loaded in previous findAnnotated* call.
200         */
201        public List<String> getClassesNotLoaded() {
202            return Collections.unmodifiableList(classesNotLoaded);
203        }
204    
205        public List<Package> findAnnotatedPackages(Class<? extends Annotation> annotation) {
206            classesNotLoaded.clear();
207            List<Package> packages = new ArrayList<Package>();
208            List<Info> infos = getAnnotationInfos(annotation.getName());
209            for (Info info : infos) {
210                if (info instanceof PackageInfo) {
211                    PackageInfo packageInfo = (PackageInfo) info;
212                    try {
213                        Package pkg = packageInfo.get();
214                        // double check via proper reflection
215                        if (pkg.isAnnotationPresent(annotation)) {
216                            packages.add(pkg);
217                        }
218                    } catch (ClassNotFoundException e) {
219                        classesNotLoaded.add(packageInfo.getName());
220                    }
221                }
222            }
223            return packages;
224        }
225    
226        public List<Class> findAnnotatedClasses(Class<? extends Annotation> annotation) {
227            classesNotLoaded.clear();
228            List<Class> classes = new ArrayList<Class>();
229            List<Info> infos = getAnnotationInfos(annotation.getName());
230            for (Info info : infos) {
231                if (info instanceof ClassInfo) {
232                    ClassInfo classInfo = (ClassInfo) info;
233                    try {
234                        Class clazz = classInfo.get();
235                        // double check via proper reflection
236                        if (clazz.isAnnotationPresent(annotation)) {
237                            classes.add(clazz);
238                        }
239                    } catch (ClassNotFoundException e) {
240                        classesNotLoaded.add(classInfo.getName());
241                    }
242                }
243            }
244            return classes;
245        }
246    
247        public List<Method> findAnnotatedMethods(Class<? extends Annotation> annotation) {
248            classesNotLoaded.clear();
249            List<ClassInfo> seen = new ArrayList<ClassInfo>();
250            List<Method> methods = new ArrayList<Method>();
251            List<Info> infos = getAnnotationInfos(annotation.getName());
252            for (Info info : infos) {
253                if (info instanceof MethodInfo && !info.getName().equals("<init>")) {
254                    MethodInfo methodInfo = (MethodInfo) info;
255                    ClassInfo classInfo = methodInfo.getDeclaringClass();
256    
257                    if (seen.contains(classInfo)) continue;
258    
259                    seen.add(classInfo);
260    
261                    try {
262                        Class clazz = classInfo.get();
263                        for (Method method : clazz.getDeclaredMethods()) {
264                            if (method.isAnnotationPresent(annotation)) {
265                                methods.add(method);
266                            }
267                        }
268                    } catch (ClassNotFoundException e) {
269                        classesNotLoaded.add(classInfo.getName());
270                    }
271                }
272            }
273            return methods;
274        }
275    
276        public List<Constructor> findAnnotatedConstructors(Class<? extends Annotation> annotation) {
277            classesNotLoaded.clear();
278            List<ClassInfo> seen = new ArrayList<ClassInfo>();
279            List<Constructor> constructors = new ArrayList<Constructor>();
280            List<Info> infos = getAnnotationInfos(annotation.getName());
281            for (Info info : infos) {
282                if (info instanceof MethodInfo && info.getName().equals("<init>")) {
283                    MethodInfo methodInfo = (MethodInfo) info;
284                    ClassInfo classInfo = methodInfo.getDeclaringClass();
285    
286                    if (seen.contains(classInfo)) continue;
287    
288                    seen.add(classInfo);
289    
290                    try {
291                        Class clazz = classInfo.get();
292                        for (Constructor constructor : clazz.getConstructors()) {
293                            if (constructor.isAnnotationPresent(annotation)) {
294                                constructors.add(constructor);
295                            }
296                        }
297                    } catch (ClassNotFoundException e) {
298                        classesNotLoaded.add(classInfo.getName());
299                    }
300                }
301            }
302            return constructors;
303        }
304    
305        public List<Field> findAnnotatedFields(Class<? extends Annotation> annotation) {
306            classesNotLoaded.clear();
307            List<ClassInfo> seen = new ArrayList<ClassInfo>();
308            List<Field> fields = new ArrayList<Field>();
309            List<Info> infos = getAnnotationInfos(annotation.getName());
310            for (Info info : infos) {
311                if (info instanceof FieldInfo) {
312                    FieldInfo fieldInfo = (FieldInfo) info;
313                    ClassInfo classInfo = fieldInfo.getDeclaringClass();
314    
315                    if (seen.contains(classInfo)) continue;
316    
317                    seen.add(classInfo);
318    
319                    try {
320                        Class clazz = classInfo.get();
321                        for (Field field : clazz.getDeclaredFields()) {
322                            if (field.isAnnotationPresent(annotation)) {
323                                fields.add(field);
324                            }
325                        }
326                    } catch (ClassNotFoundException e) {
327                        classesNotLoaded.add(classInfo.getName());
328                    }
329                }
330            }
331            return fields;
332        }
333    
334        public List<Class> findClassesInPackage(String packageName, boolean recursive) {
335            classesNotLoaded.clear();
336            List<Class> classes = new ArrayList<Class>();
337            for (ClassInfo classInfo : classInfos) {
338                try {
339                    if (recursive && classInfo.getPackageName().startsWith(packageName)){
340                        classes.add(classInfo.get());
341                    } else if (classInfo.getPackageName().equals(packageName)){
342                        classes.add(classInfo.get());
343                    }
344                } catch (ClassNotFoundException e) {
345                    classesNotLoaded.add(classInfo.getName());
346                }
347            }
348            return classes;
349        }
350    
351        private static Collection<URL> getUrls(ClassLoader classLoader, boolean excludeParent) throws IOException {
352            return getUrls(classLoader, excludeParent? classLoader.getParent() : null);
353        }
354    
355        private static Collection<URL> getUrls(ClassLoader classLoader, ClassLoader excludeParent) throws IOException {
356            UrlSet urlSet = new UrlSet(classLoader);
357            if (excludeParent != null){
358                urlSet = urlSet.exclude(excludeParent);
359            }
360            return urlSet.getUrls();
361        }
362    
363        private List<String> file(URL location) {
364            List<String> classNames = new ArrayList<String>();
365            File dir = new File(URLDecoder.decode(location.getPath()));
366            if (dir.getName().equals("META-INF")) {
367                dir = dir.getParentFile(); // Scrape "META-INF" off
368            }
369            if (dir.isDirectory()) {
370                scanDir(dir, classNames, "");
371            }
372            return classNames;
373        }
374    
375        private void scanDir(File dir, List<String> classNames, String packageName) {
376            File[] files = dir.listFiles();
377            for (File file : files) {
378                if (file.isDirectory()) {
379                    scanDir(file, classNames, packageName + file.getName() + ".");
380                } else if (file.getName().endsWith(".class")) {
381                    String name = file.getName();
382                    name = name.replaceFirst(".class$", "");
383                    classNames.add(packageName + name);
384                }
385            }
386        }
387    
388        private List<String> jar(URL location) throws IOException {
389            String jarPath = location.getFile();
390            if (jarPath.indexOf("!") > -1){
391                jarPath = jarPath.substring(0, jarPath.indexOf("!"));
392            }
393            URL url = new URL(jarPath);
394            InputStream in = url.openStream();
395            try {
396                JarInputStream jarStream = new JarInputStream(in);
397                return jar(jarStream);
398            } finally {
399                in.close();
400            }
401        }
402    
403        private List<String> jar(JarInputStream jarStream) throws IOException {
404            List<String> classNames = new ArrayList<String>();
405    
406            JarEntry entry;
407            while ((entry = jarStream.getNextJarEntry()) != null) {
408                if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
409                    continue;
410                }
411                String className = entry.getName();
412                className = className.replaceFirst(".class$", "");
413                className = className.replace('/', '.');
414                classNames.add(className);
415            }
416    
417            return classNames;
418        }
419    
420        public class Annotatable {
421            private final List<AnnotationInfo> annotations = new ArrayList<AnnotationInfo>();
422    
423            public Annotatable(AnnotatedElement element) {
424                for (Annotation annotation : element.getAnnotations()) {
425                    annotations.add(new AnnotationInfo(annotation.annotationType().getName()));
426                }
427            }
428    
429            public Annotatable() {
430            }
431    
432            public List<AnnotationInfo> getAnnotations() {
433                return annotations;
434            }
435    
436        }
437    
438        public static interface Info {
439            String getName();
440    
441            List<AnnotationInfo> getAnnotations();
442        }
443    
444        public class PackageInfo extends Annotatable implements Info {
445            private final String name;
446            private final ClassInfo info;
447            private final Package pkg;
448    
449            public PackageInfo(Package pkg){
450                super(pkg);
451                this.pkg = pkg;
452                this.name = pkg.getName();
453                this.info = null;
454            }
455    
456            public PackageInfo(String name) {
457                info = new ClassInfo(name, null);
458                this.name = name;
459                this.pkg = null;
460            }
461    
462            public String getName() {
463                return name;
464            }
465    
466            public Package get() throws ClassNotFoundException {
467                return (pkg != null)?pkg:info.get().getPackage();
468            }
469        }
470    
471        public class ClassInfo extends Annotatable implements Info {
472            private final String name;
473            private final List<MethodInfo> methods = new ArrayList<MethodInfo>();
474            private final List<MethodInfo> constructors = new ArrayList<MethodInfo>();
475            private final String superType;
476            private final List<String> interfaces = new ArrayList<String>();
477            private final List<FieldInfo> fields = new ArrayList<FieldInfo>();
478            private Class<?> clazz;
479            private ClassNotFoundException notFound;
480    
481            public ClassInfo(Class clazz) {
482                super(clazz);
483                this.clazz = clazz;
484                this.name = clazz.getName();
485                Class superclass = clazz.getSuperclass();
486                this.superType = superclass != null ? superclass.getName(): null;
487            }
488    
489            public ClassInfo(String name, String superType) {
490                this.name = name;
491                this.superType = superType;
492            }
493    
494            public String getPackageName(){
495                return name.substring(name.lastIndexOf(".")+1, name.length());
496            }
497    
498            public List<MethodInfo> getConstructors() {
499                return constructors;
500            }
501    
502            public List<String> getInterfaces() {
503                return interfaces;
504            }
505    
506            public List<FieldInfo> getFields() {
507                return fields;
508            }
509    
510            public List<MethodInfo> getMethods() {
511                return methods;
512            }
513    
514            public String getName() {
515                return name;
516            }
517    
518            public String getSuperType() {
519                return superType;
520            }
521    
522            public Class get() throws ClassNotFoundException {
523                if (clazz != null) return clazz;
524                if (notFound != null) throw notFound;
525                try {
526                    this.clazz = classLoader.loadClass(name);
527                    return clazz;
528                } catch (ClassNotFoundException notFound) {
529                    classesNotLoaded.add(name);
530                    this.notFound = notFound;
531                    throw notFound;
532                }
533            }
534    
535            public String toString() {
536                return name;
537            }
538        }
539    
540        public class MethodInfo extends Annotatable implements Info {
541            private final ClassInfo declaringClass;
542            private final String returnType;
543            private final String name;
544            private final List<List<AnnotationInfo>> parameterAnnotations = new ArrayList<List<AnnotationInfo>>();
545    
546            public MethodInfo(ClassInfo info, Constructor constructor){
547                super(constructor);
548                this.declaringClass = info;
549                this.name = "<init>";
550                this.returnType = Void.TYPE.getName();
551            }
552    
553            public MethodInfo(ClassInfo info, Method method){
554                super(method);
555                this.declaringClass = info;
556                this.name = method.getName();
557                this.returnType = method.getReturnType().getName();
558            }
559    
560            public MethodInfo(ClassInfo declarignClass, String name, String returnType) {
561                this.declaringClass = declarignClass;
562                this.name = name;
563                this.returnType = returnType;
564            }
565    
566            public List<List<AnnotationInfo>> getParameterAnnotations() {
567                return parameterAnnotations;
568            }
569    
570            public List<AnnotationInfo> getParameterAnnotations(int index) {
571                if (index >= parameterAnnotations.size()) {
572                    for (int i = parameterAnnotations.size(); i <= index; i++) {
573                        List<AnnotationInfo> annotationInfos = new ArrayList<AnnotationInfo>();
574                        parameterAnnotations.add(i, annotationInfos);
575                    }
576                }
577                return parameterAnnotations.get(index);
578            }
579    
580            public String getName() {
581                return name;
582            }
583    
584            public ClassInfo getDeclaringClass() {
585                return declaringClass;
586            }
587    
588            public String getReturnType() {
589                return returnType;
590            }
591    
592            public String toString() {
593                return declaringClass + "@" + name;
594            }
595        }
596    
597        public class FieldInfo extends Annotatable implements Info {
598            private final String name;
599            private final String type;
600            private final ClassInfo declaringClass;
601    
602            public FieldInfo(ClassInfo info, Field field){
603                super(field);
604                this.declaringClass = info;
605                this.name = field.getName();
606                this.type = field.getType().getName();
607            }
608    
609            public FieldInfo(ClassInfo declaringClass, String name, String type) {
610                this.declaringClass = declaringClass;
611                this.name = name;
612                this.type = type;
613            }
614    
615            public String getName() {
616                return name;
617            }
618    
619            public ClassInfo getDeclaringClass() {
620                return declaringClass;
621            }
622    
623            public String getType() {
624                return type;
625            }
626    
627            public String toString() {
628                return declaringClass + "#" + name;
629            }
630        }
631    
632        public class AnnotationInfo extends Annotatable implements Info {
633            private final String name;
634    
635            public AnnotationInfo(Annotation annotation){
636                this(annotation.getClass().getName());
637            }
638    
639            public AnnotationInfo(Class<? extends Annotation> annotation) {
640                this.name = annotation.getName().intern();
641            }
642    
643            public AnnotationInfo(String name) {
644                name = name.replaceAll("^L|;$", "");
645                name = name.replace('/', '.');
646                this.name = name.intern();
647            }
648    
649            public String getName() {
650                return name;
651            }
652    
653            public String toString() {
654                return name;
655            }
656        }
657    
658        private List<Info> getAnnotationInfos(String name) {
659            List<Info> infos = annotated.get(name);
660            if (infos == null) {
661                infos = new ArrayList<Info>();
662                annotated.put(name, infos);
663            }
664            return infos;
665        }
666    
667        private void readClassDef(String className) {
668            if (!className.endsWith(".class")) {
669                className = className.replace('.', '/') + ".class";
670            }
671            try {
672                URL resource = classLoader.getResource(className);
673                if (resource != null) {
674                    InputStream in = resource.openStream();
675                    try {
676                        ClassReader classReader = new ClassReader(in);
677                        classReader.accept(new InfoBuildingVisitor(), true);
678                    } finally {
679                        in.close();
680                    }
681                } else {
682                    new Exception("Could not load " + className).printStackTrace();
683                }
684            } catch (IOException e) {
685                e.printStackTrace();
686            }
687    
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    }