ClassList.java

232 lines | 8.972 kB Blame History Raw Download
package com.ning.billing.beatrix.lifecycle;


import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarFile;

/**
 * This abstract class can be used to obtain a list of all classes in a classpath.
 *
 * <em>Caveat:</em> When used in environments which utilize multiple class loaders--such as
 * a J2EE Container like Tomcat--it is important to select the correct classloader
 * otherwise the classes returned, if any, will be incompatible with those declared
 * in the code employing this class lister.
 * to get a reference to your classloader within an instance method use:
 *  <code>this.getClass().getClassLoader()</code> or
 *  <code>Thread.currentThread().getContextClassLoader()</code> anywhere else
 * <p>
 * @author Kris Dover <krisdover@hotmail.com>
 * @version 0.2.0
 * @since   0.1.0
 */
public abstract class ClassList {

	/**
	 * Searches the classpath for all classes matching a specified search criteria,
	 * returning them in a map keyed with the interfaces they implement or null if they
	 * have no interfaces. The search criteria can be specified via interface, package
	 * and jar name filter arguments
	 * <p>
	 * @param classLoader       The classloader whose classpath will be traversed
	 * @param interfaceFilter   A Set of fully qualified interface names to search for
	 *                          or null to return classes implementing all interfaces
	 * @param packageFilter     A Set of fully qualified package names to search for or
	 *                          or null to return classes in all packages
	 * @param jarFilter         A Set of jar file names to search for or null to return
	 *                          classes from all jars
	 * @return A Map of a Set of Classes keyed to their interface names
	 *
	 * @throws ClassNotFoundException if the current thread's classloader cannot load
	 *                                a requested class for any reason
	 */
	public static Map<String, Set<Class<?>>> findClasses(ClassLoader classLoader,
			Set<String> interfaceFilter,
			Set<String> packageFilter,
			Set<String> jarFilter,
			boolean filterAbstract)
			throws ClassNotFoundException {

		Map<String, Set<Class<?>>> classTable = new HashMap<String, Set<Class<?>>>();
		Object[] classPaths;
		try {
			// get a list of all classpaths
			classPaths = ((java.net.URLClassLoader) classLoader).getURLs();
		}
		catch(ClassCastException cce){
			// or cast failed; tokenize the system classpath
			classPaths = System.getProperty("java.class.path", "").split(File.pathSeparator);
		}

		for(int h = 0; h < classPaths.length; h++){
			Enumeration<?> files = null;
			JarFile module = null;
			// for each classpath ...
			File classPath = new File( (URL.class).isInstance(classPaths[h]) ?
					((URL)classPaths[h]).getFile() : classPaths[h].toString() );



			if (classPath.isDirectory() && jarFilter == null){   // is our classpath a directory and jar filters are not active?
				//System.out.println("List dir: " + classPath.getAbsolutePath());
				List<String> dirListing = new ArrayList<String>();
				// get a recursive listing of this classpath
				recursivelyListDir(dirListing, classPath, new StringBuffer() );
				// an enumeration wrapping our list of files
				files = Collections.enumeration( dirListing );
			}
			else if (classPath.getName().endsWith(".jar") ){    // is our classpath a jar?
				//System.out.println("Inspect file: " + classPath.getAbsolutePath());
				// skip any jars not list in the filter
				if( jarFilter != null && !jarFilter.contains( classPath.getName() ) ){
					continue;
				}
                boolean failed = true;
				try{
					// if our resource is a jar, instantiate a jarfile using the full path to resource
					module = new JarFile( classPath );
                    failed = false;
				}catch (MalformedURLException mue){
					throw new ClassNotFoundException("Bad classpath. Error: " + mue.getMessage());
				}catch (IOException io){
					//throw new ClassNotFoundException("jar file '" + classPath.getName() + "' could not be instantiate from file path. Error: " + io.getMessage());
                    System.out.println("ERROR: jar file '" + classPath.getName() + "' could not be instantiate from file path. Error: " + io.getMessage());
				}
                if (! failed) {
				    // get an enumeration of the files in this jar
				    files = module.entries();
                }
			}

			// for each file path in our directory or jar
			while( files != null && files.hasMoreElements() ){
				// get each fileName
				String fileName = files.nextElement().toString();

				// we only want the class files
				if( fileName.endsWith(".class") ){
					// convert our full filename to a fully qualified class name
					String className = fileName.replaceAll("/", ".").substring(0, fileName.length() - 6);

					// debug class list
					//System.out.println("Class: " + className);
					// skip any classes in packages not explicitly requested in our package filter
					if (packageFilter != null) {
						boolean skip = true;
						Iterator<String> it = packageFilter.iterator();
						while (it.hasNext()) {
							String filter = it.next() + ".";
							//System.out.println("\tFilter: " + filter + " Class: " + className);
							if (className.startsWith(filter)) {
								skip = false;
								break;
							}
						}
						if (skip) {
							continue;
						}
					}
					//System.out.println("\tAdd Class: " + className);
					// get the class for our class name
					Class<?> theClass = null;
					try{
						theClass = Class.forName(className, false, classLoader);
					}catch(NoClassDefFoundError e){
						//System.out.println("Skipping class '" + className + "' for reason " + e.getMessage());
						continue;
					}
					// skip interfaces
					if( theClass.isInterface() ){
						continue;
					}

					if (filterAbstract && Modifier.isAbstract(theClass.getModifiers())) {
						continue;
					}
					//then get an array of all the interfaces in our class
					Class<?> [] classInterfaces = theClass.getInterfaces();

					// for each interface in this class, add both class and interface into the map
					String interfaceName = null;
					for (int i = 0; i < classInterfaces.length || (i == 0 && interfaceFilter == null); i++) {
						if(i < classInterfaces.length){
							interfaceName = classInterfaces[i].getName();
							// was this interface requested?
							if( interfaceFilter != null && !interfaceFilter.contains(interfaceName) ){
								continue;
							}
						}
						// is this interface already in the map?
						if( classTable.containsKey( interfaceName ) ){
							// if so then just add this class to the end of the list of classes implementing this interface
							classTable.get(interfaceName).add( theClass );
						}else{
							// else create a new list initialised with our first class and put the list into the map
							Set<Class<?>> allClasses = new HashSet<Class<?>>();
							allClasses.add( theClass );
							classTable.put(interfaceName, allClasses);
						}
					}

				}
			}

			// close the jar if it was used
			if(module != null){
				try{
					module.close();
				}catch(IOException ioe){
					throw new ClassNotFoundException("The module jar file '" + classPath.getName() +
							"' could not be closed. Error: " + ioe.getMessage());
				}
			}

		} // end for loop

		return classTable;
	} // end method

	/**
	 * Recursively lists a directory while generating relative paths. This is a helper function for findClasses.
	 * Note: Uses a StringBuffer to avoid the excessive overhead of multiple String concatentation
	 *
	 * @param dirListing     A list variable for storing the directory listing as a list of Strings
	 * @param dir                 A File for the directory to be listed
	 * @param relativePath A StringBuffer used for building the relative paths
	 */
	private static void recursivelyListDir(List<String> dirListing, File dir, StringBuffer relativePath){
		int prevLen; // used to undo append operations to the StringBuffer


		// if the dir is really a directory
		if( dir.isDirectory() ){
			// get a list of the files in this directory
			File[] files = dir.listFiles();
			// for each file in the present dir
			for(int i = 0; i < files.length; i++){
				// store our original relative path string length
				prevLen = relativePath.length();
				// call this function recursively with file list from present
				// dir and relateveto appended with present dir
				recursivelyListDir(dirListing, files[i], relativePath.append( prevLen == 0 ? "" : "/" ).append( files[i].getName() ) );
				//  delete subdirectory previously appended to our relative path
				relativePath.delete(prevLen, relativePath.length());
			}
		}else{
			// this dir is a file; append it to the relativeto path and add it to the directory listing
			dirListing.add( relativePath.toString() );
		}
	}
}