/** * 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."); } } }