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