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 */
017package org.apache.xbean.finder;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.lang.reflect.Constructor;
023import java.lang.reflect.Field;
024import java.lang.reflect.Method;
025import java.net.JarURLConnection;
026import java.net.URL;
027import java.net.URLDecoder;
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.Collection;
031import java.util.List;
032import java.util.jar.JarEntry;
033import 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 (mer. 17 mars 2010) $
049 */
050public 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}