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 java.io.File;
020    import java.io.IOException;
021    import java.io.InputStream;
022    import java.lang.reflect.Constructor;
023    import java.lang.reflect.Field;
024    import java.lang.reflect.Method;
025    import java.net.JarURLConnection;
026    import java.net.URL;
027    import java.net.URLDecoder;
028    import java.util.ArrayList;
029    import java.util.Arrays;
030    import java.util.Collection;
031    import java.util.List;
032    import java.util.jar.JarEntry;
033    import java.util.jar.JarInputStream;
034    
035    /**
036     * ClassFinder searches the classpath of the specified classloader for
037     * packages, classes, constructors, methods, or fields with specific annotations.
038     *
039     * For security reasons ASM is used to find the annotations.  Classes are not
040     * loaded unless they match the requirements of a called findAnnotated* method.
041     * Once loaded, these classes are cached.
042     *
043     * The getClassesNotLoaded() method can be used immediately after any find*
044     * method to get a list of classes which matched the find requirements (i.e.
045     * contained the annotation), but were unable to be loaded.
046     *
047     * @author David Blevins
048     * @version $Rev: 924423 $ $Date: 2010-03-17 20:06:14 +0100 (Wed, 17 Mar 2010) $
049     */
050    public class ClassFinder extends AbstractFinder {
051    
052        private final ClassLoader classLoader;
053    
054        /**
055         * Creates a ClassFinder that will search the urls in the specified classloader
056         * excluding the urls in the classloader's parent.
057         *
058         * To include the parent classloader, use:
059         *
060         *    new ClassFinder(classLoader, false);
061         *
062         * To exclude the parent's parent, use:
063         *
064         *    new ClassFinder(classLoader, classLoader.getParent().getParent());
065         *
066         * @param classLoader source of classes to scan
067         * @throws Exception if something goes wrong
068         */
069        public ClassFinder(ClassLoader classLoader) throws Exception {
070            this(classLoader, true);
071        }
072    
073        /**
074         * Creates a ClassFinder that will search the urls in the specified classloader.
075         *
076         * @param classLoader source of classes to scan
077         * @param excludeParent Allegedly excludes classes from parent classloader, whatever that might mean
078         * @throws Exception if something goes wrong.
079         */
080        public ClassFinder(ClassLoader classLoader, boolean excludeParent) throws Exception {
081            this(classLoader, getUrls(classLoader, excludeParent));
082        }
083    
084        /**
085         * Creates a ClassFinder that will search the urls in the specified classloader excluding
086         * the urls in the 'exclude' classloader.
087         *
088         * @param classLoader source of classes to scan
089         * @param exclude source of classes to exclude from scanning
090         * @throws Exception if something goes wrong
091         */
092        public ClassFinder(ClassLoader classLoader, ClassLoader exclude) throws Exception {
093            this(classLoader, getUrls(classLoader, exclude));
094        }
095    
096        public ClassFinder(ClassLoader classLoader, URL url) {
097            this(classLoader, Arrays.asList(url));
098        }
099    
100        public ClassFinder(ClassLoader classLoader, Collection<URL> urls) {
101            this.classLoader = classLoader;
102    
103            List<String> classNames = new ArrayList<String>();
104            for (URL location : urls) {
105                try {
106                    if (location.getProtocol().equals("jar")) {
107                        classNames.addAll(jar(location));
108                    } else if (location.getProtocol().equals("file")) {
109                        try {
110                            // See if it's actually a jar
111                            URL jarUrl = new URL("jar", "", location.toExternalForm() + "!/");
112                            JarURLConnection juc = (JarURLConnection) jarUrl.openConnection();
113                            juc.getJarFile();
114                            classNames.addAll(jar(jarUrl));
115                        } catch (IOException e) {
116                            classNames.addAll(file(location));
117                        }
118                    }
119                } catch (Exception e) {
120                    e.printStackTrace();
121                }
122            }
123    
124            for (String className : classNames) {
125                readClassDef(className);
126            }
127        }
128    
129        public ClassFinder(Class... classes){
130            this(Arrays.asList(classes));
131        }
132    
133        public ClassFinder(List<Class> classes){
134            this.classLoader = null;
135            List<Info> infos = new ArrayList<Info>();
136            List<Package> packages = new ArrayList<Package>();
137            for (Class clazz : classes) {
138    
139                try {
140                    Package aPackage = clazz.getPackage();
141                    if (aPackage != null && !packages.contains(aPackage)){
142                        infos.add(new PackageInfo(aPackage));
143                        packages.add(aPackage);
144                    }
145    
146                    ClassInfo classInfo = new ClassInfo(clazz);
147                    infos.add(classInfo);
148                    classInfos.add(classInfo);
149                    for (Method method : clazz.getDeclaredMethods()) {
150                        infos.add(new MethodInfo(classInfo, method));
151                    }
152    
153                    for (Constructor constructor : clazz.getConstructors()) {
154                        infos.add(new MethodInfo(classInfo, constructor));
155                    }
156    
157                    for (Field field : clazz.getDeclaredFields()) {
158                        infos.add(new FieldInfo(classInfo, field));
159                    }
160                } catch (NoClassDefFoundError e) {
161                    throw new NoClassDefFoundError("Could not fully load class: " + clazz.getName() + "\n due to:" + e.getMessage() + "\n in classLoader: \n" + clazz.getClassLoader());
162                }
163            }
164    
165            for (Info info : infos) {
166                for (AnnotationInfo annotation : info.getAnnotations()) {
167                    List<Info> annotationInfos = getAnnotationInfos(annotation.getName());
168                    annotationInfos.add(info);
169                }
170            }
171        }
172    
173        private static Collection<URL> getUrls(ClassLoader classLoader, boolean excludeParent) throws IOException {
174            return getUrls(classLoader, excludeParent? classLoader.getParent() : null);
175        }
176    
177        private static Collection<URL> getUrls(ClassLoader classLoader, ClassLoader excludeParent) throws IOException {
178            UrlSet urlSet = new UrlSet(classLoader);
179            if (excludeParent != null){
180                urlSet = urlSet.exclude(excludeParent);
181            }
182            return urlSet.getUrls();
183        }
184    
185        @Override
186        protected URL getResource(String className) {
187            return classLoader.getResource(className);
188        }
189    
190        @Override
191        protected Class<?> loadClass(String fixedName) throws ClassNotFoundException {
192            return classLoader.loadClass(fixedName);
193        }
194    
195    
196    
197        private List<String> file(URL location) {
198            List<String> classNames = new ArrayList<String>();
199            File dir = new File(URLDecoder.decode(location.getPath()));
200            if (dir.getName().equals("META-INF")) {
201                dir = dir.getParentFile(); // Scrape "META-INF" off
202            }
203            if (dir.isDirectory()) {
204                scanDir(dir, classNames, "");
205            }
206            return classNames;
207        }
208    
209        private void scanDir(File dir, List<String> classNames, String packageName) {
210            File[] files = dir.listFiles();
211            for (File file : files) {
212                if (file.isDirectory()) {
213                    scanDir(file, classNames, packageName + file.getName() + ".");
214                } else if (file.getName().endsWith(".class")) {
215                    String name = file.getName();
216                    name = name.replaceFirst(".class$", "");
217                    if (name.contains(".")) continue;
218                    classNames.add(packageName + name);
219                }
220            }
221        }
222    
223        private List<String> jar(URL location) throws IOException {
224            String jarPath = location.getFile();
225            if (jarPath.indexOf("!") > -1){
226                jarPath = jarPath.substring(0, jarPath.indexOf("!"));
227            }
228            URL url = new URL(jarPath);
229            InputStream in = url.openStream();
230            try {
231                JarInputStream jarStream = new JarInputStream(in);
232                return jar(jarStream);
233            } finally {
234                in.close();
235            }
236        }
237    
238        private List<String> jar(JarInputStream jarStream) throws IOException {
239            List<String> classNames = new ArrayList<String>();
240    
241            JarEntry entry;
242            while ((entry = jarStream.getNextJarEntry()) != null) {
243                if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
244                    continue;
245                }
246                String className = entry.getName();
247                className = className.replaceFirst(".class$", "");
248                if (className.contains(".")) continue;
249                className = className.replace('/', '.');
250                classNames.add(className);
251            }
252    
253            return classNames;
254        }
255    
256        protected void readClassDef(String className) {
257            if (!className.endsWith(".class")) {
258                className = className.replace('.', '/') + ".class";
259            }
260            try {
261                URL resource = getResource(className);
262                if (resource != null) {
263                    InputStream in = resource.openStream();
264                    try {
265                        readClassDef(in);
266                    } finally {
267                        in.close();
268                    }
269                } else {
270                    new Exception("Could not load " + className).printStackTrace();
271                }
272            } catch (IOException e) {
273                e.printStackTrace();
274            }
275    
276        }
277    }