/**
* Copyright (C) 2010-2016 eBusiness Information, Excilys Group
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed To in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.androidannotations.testutils;
import static java.util.Collections.synchronizedList;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* Based on http://code.google.com/p/acris/wiki/AnnotationProcessing_Testing
*/
public class ClassFinder {
private Map<URL, String> classpathLocations = new HashMap<>();
private Map<Class<?>, URL> results = new HashMap<>();
private List<Throwable> errors = new ArrayList<>();
public ClassFinder() {
refreshLocations();
}
/**
* Rescan the classpath, caching all possible file locations.
*/
public final void refreshLocations() {
synchronized (classpathLocations) {
classpathLocations = getClasspathLocations();
}
}
/**
* Finds all classes in a package.
*
* @param packageName
* Name of superclass/interface on which to search
*
* @return the classes which can be found in the package of the superclass/interface
*/
public final List<Class<?>> findClassesInPackage(String packageName) {
synchronized (classpathLocations) {
synchronized (results) {
errors = new ArrayList<>();
results = new TreeMap<>(CLASS_COMPARATOR);
return findSubclasses(classpathLocations, packageName);
}
}
}
public final List<Throwable> getErrors() {
return new ArrayList<>(errors);
}
/**
* The result of the last search is cached in this object, along with the
* URL that corresponds to each class returned. This method may be called to
* query the cache for the location at which the given class was found.
* <code>null</code> will be returned if the given class was not found
* during the last search, or if the result cache has been cleared.
*
* @param cls the class whose location is queried
*
* @return the location where the class is found
*/
public final URL getLocationOf(Class<?> cls) {
if (results != null) {
return results.get(cls);
} else {
return null;
}
}
/**
* Determine every URL location defined by the current classpath, and it's
* associated package name.
*
* @return the locations of the given classpath
*/
public final Map<URL, String> getClasspathLocations() {
Map<URL, String> map = new TreeMap<>(URL_COMPARATOR);
File file = null;
String pathSep = System.getProperty("path.separator");
String classpath = System.getProperty("java.class.path");
StringTokenizer st = new StringTokenizer(classpath, pathSep);
while (st.hasMoreTokens()) {
String path = st.nextToken();
file = new File(path);
include(null, file, map);
}
return map;
}
private final static FileFilter DIRECTORIES_ONLY = new FileFilter() {
@Override
public boolean accept(File f) {
return f.exists() && f.isDirectory();
}
};
private final static Comparator<URL> URL_COMPARATOR = new Comparator<URL>() {
@Override
public int compare(URL u1, URL u2) {
return String.valueOf(u1).compareTo(String.valueOf(u2));
}
};
private final static Comparator<Class<?>> CLASS_COMPARATOR = new Comparator<Class<?>>() {
@Override
public int compare(Class<?> c1, Class<?> c2) {
return String.valueOf(c1).compareTo(String.valueOf(c2));
}
};
private void include(String name, File file, Map<URL, String> map) {
if (!file.exists()) {
return;
}
if (!file.isDirectory()) {
// could be a JAR file
includeJar(file, map);
return;
}
if (name == null) {
name = "";
} else {
name += ".";
}
// add subpackages
File[] dirs = file.listFiles(DIRECTORIES_ONLY);
for (File dir : dirs) {
try {
// add the present package
map.put(new URL("file://" + dir.getCanonicalPath()), name + dir.getName());
} catch (IOException ioe) {
return;
}
include(name + dir.getName(), dir, map);
}
}
private void includeJar(File file, Map<URL, String> map) {
if (file.isDirectory()) {
return;
}
URL jarURL = null;
JarFile jar = null;
try {
jarURL = new URL("file:/" + file.getCanonicalPath());
jarURL = new URL("jar:" + jarURL.toExternalForm() + "!/");
JarURLConnection conn = (JarURLConnection) jarURL.openConnection();
jar = conn.getJarFile();
} catch (Exception e) {
// not a JAR or disk I/O error
// either way, just skip
return;
}
if (jar == null || jarURL == null) {
return;
}
// include the jar's "default" package (i.e. jar's root)
map.put(jarURL, "");
Enumeration<JarEntry> e = jar.entries();
while (e.hasMoreElements()) {
JarEntry entry = e.nextElement();
if (entry.isDirectory()) {
if (entry.getName().toUpperCase().equals("META-INF/")) {
continue;
}
try {
map.put(new URL(jarURL.toExternalForm() + entry.getName()), packageNameFor(entry));
} catch (MalformedURLException murl) {
// whacky entry?
continue;
}
}
}
}
private static String packageNameFor(JarEntry entry) {
if (entry == null) {
return "";
}
String s = entry.getName();
if (s == null) {
return "";
}
if (s.length() == 0) {
return s;
}
if (s.startsWith("/")) {
s = s.substring(1, s.length());
}
if (s.endsWith("/")) {
s = s.substring(0, s.length() - 1);
}
return s.replace('/', '.');
}
private List<Class<?>> findSubclasses(Map<URL, String> locations, String searchingPackageName) {
List<Class<?>> v = synchronizedList(new ArrayList<Class<?>>());
List<Class<?>> w = null;
for (URL url : locations.keySet()) {
// search just required packages
String packageName = locations.get(url);
if (packageName.startsWith(searchingPackageName)) {
w = findSubclasses(url, packageName, searchingPackageName);
if (w != null && w.size() > 0) {
v.addAll(w);
}
}
}
return v;
}
private List<Class<?>> findSubclasses(URL location, String packageName, String searchingPackageName) {
synchronized (results) {
// hash guarantees unique names...
Map<Class<?>, URL> thisResult = new TreeMap<>(CLASS_COMPARATOR);
List<Class<?>> v = synchronizedList(new ArrayList<Class<?>>());
// ...but return a list
List<URL> knownLocations = new ArrayList<>();
knownLocations.add(location);
// TODO: add getResourceLocations() to this list
// iterate matching package locations...
for (URL url : knownLocations) {
// Get a File object for the package
File directory = new File(url.getFile());
if (directory.exists()) {
// Get the list of the files contained in the package
String[] files = directory.list();
for (String file : files) {
// we are only interested in .class files
if (file.endsWith(".class")) {
// removes the .class extension
String classname = file.substring(0, file.length() - 6);
try {
Class<?> c = Class.forName(packageName + "." + classname);
if (packageName.startsWith(searchingPackageName)) {
thisResult.put(c, url);
}
} catch (Exception ex) {
errors.add(ex);
}
}
}
} else {
try {
// It does not work with the filesystem: we must
// be in the case of a package contained in a jar file.
JarURLConnection conn = (JarURLConnection) url.openConnection();
// String starts = conn.getEntryName();
JarFile jarFile = conn.getJarFile();
Enumeration<JarEntry> e = jarFile.entries();
while (e.hasMoreElements()) {
JarEntry entry = e.nextElement();
String entryname = entry.getName();
if (!entry.isDirectory() && entryname.endsWith(".class")) {
String classname = entryname.substring(0, entryname.length() - 6);
if (classname.startsWith("/")) {
classname = classname.substring(1);
}
classname = classname.replace('/', '.');
try {
// TODO: verify this block
Class<?> c = Class.forName(classname);
if (c.getPackage().getName().startsWith(searchingPackageName)) {
thisResult.put(c, url);
}
} catch (ClassNotFoundException cnfex) {
// that's strange since we're scanning
// the same classpath the classloader's
// using... oh, well
errors.add(cnfex);
} catch (NoClassDefFoundError ncdfe) {
// dependency problem... class is
// unusable anyway, so just ignore it
errors.add(ncdfe);
} catch (UnsatisfiedLinkError ule) {
// another dependency problem... class is
// unusable anyway, so just ignore it
errors.add(ule);
} catch (Exception exception) {
// unexpected problem
// System.err.println (ex);
errors.add(exception);
} catch (Error error) {
// lots of things could go wrong
// that we'll just ignore since
// they're so rare...
errors.add(error);
}
}
}
} catch (IOException ioex) {
errors.add(ioex);
}
}
} // while
results.putAll(thisResult);
for (Class<?> aClass : thisResult.keySet()) {
v.add(aClass);
}
return v;
} // synch results
}
public static void main(String[] args) {
ClassFinder finder = null;
List<Class<?>> v = null;
List<Throwable> errors = null;
if (args.length == 1) {
finder = new ClassFinder();
v = finder.findClassesInPackage(args[0]);
errors = finder.getErrors();
} else {
System.out.println("Usage: java ClassFinder <package.name>");
return;
}
System.out.println("RESULTS:");
if (v != null && v.size() > 0) {
for (Class<?> cls : v) {
System.out.println(cls + " in " + (finder != null ? String.valueOf(finder.getLocationOf(cls)) : "?"));
}
if (errors != null && errors.size() > 0) {
System.out.println("Errors:");
for (Throwable error : errors) {
error.printStackTrace();
}
}
} else {
System.out.println("No subclasses in package " + args[0] + " found.");
}
}
}