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.net.JarURLConnection;
023    import java.net.URL;
024    import java.net.URLDecoder;
025    import java.util.ArrayList;
026    import java.util.Arrays;
027    import java.util.Collection;
028    import java.util.List;
029    import java.util.jar.JarEntry;
030    import 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: 1064425 $ $Date: 2011-01-28 10:13:40 +0800 (Fri, 28 Jan 2011) $
046     */
047    public 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            for (File file : files) {
180                if (file.isDirectory()) {
181                    scanDir(file, classNames, packageName + file.getName() + ".");
182                } else if (file.getName().endsWith(".class")) {
183                    String name = file.getName();
184                    name = name.replaceFirst(".class$", "");
185                    if (name.contains(".")) continue;
186                    classNames.add(packageName + name);
187                }
188            }
189        }
190    
191        private List<String> jar(URL location) throws IOException {
192            String jarPath = location.getFile();
193            if (jarPath.indexOf("!") > -1){
194                jarPath = jarPath.substring(0, jarPath.indexOf("!"));
195            }
196            URL url = new URL(jarPath);
197            InputStream in = url.openStream();
198            try {
199                JarInputStream jarStream = new JarInputStream(in);
200                return jar(jarStream);
201            } finally {
202                in.close();
203            }
204        }
205    
206        private List<String> jar(JarInputStream jarStream) throws IOException {
207            List<String> classNames = new ArrayList<String>();
208    
209            JarEntry entry;
210            while ((entry = jarStream.getNextJarEntry()) != null) {
211                if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
212                    continue;
213                }
214                String className = entry.getName();
215                className = className.replaceFirst(".class$", "");
216                if (className.contains(".")) continue;
217                className = className.replace('/', '.');
218                classNames.add(className);
219            }
220    
221            return classNames;
222        }
223    
224    }