Sunday, May 9, 2010

Java, Know the Subclasses of a class.

While I was working on one of my project I came across with a need to know all the subclasses of particular class in a classpath.

How does one go about and try to find all subclasses of a given class (or all implementors of a given interface) in Java? As of now, I have a method to do this. The method is:

1.Get a list of all class names that exist on the class path
2.Load each class and test to see if it is a subclass or implementor of the desired class or interface

In Eclipse, there is a nice feature called the Type Hierarchy that manages to show this quite efficiently. How does one go about and do it programmatically?

I know Scanning for classes is not easy with pure Java, And the Spring framework offers a class called ClassPathScanningCandidateComponentProvider that can do what you need.

But here is a way of achieving it with in stand alone applications.

May not be an efficient way of coding, used Vectors in some places, which is not required, we can safely replace them with ArrayList, Sesrializing might not be the appropriate solution, but logic works well.. here the class which I implemented named with SubClassFinder.java.

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.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.Vector;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * Will scan and list all the subclasses of a given class or Interface,
 * Its even search for classes in a jar file too.. 
 * <li>Get a list of all class names that exist on the class path</li>
 *  <li>
 * Load each class and test to see if</li>
 * While scanning all the classes it serializes the results, 
 * 
 * 
 * @author rajendra
 */
public class SubClassFinder {
 private Class<?> classToSearch = null;
 private Map<URL, String> classpathLocations = new HashMap<URL, String>();
 private Map<Class<?>, URL> results = new HashMap<Class<?>, URL>();
 private List<Throwable> errors = new ArrayList<Throwable>();

 public SubClassFinder() {
  refreshLocations();
 }

 /**
  * Rescan the classpath, cacheing all possible file locations.
  */
 public final void refreshLocations() {
  synchronized (classpathLocations) {
   classpathLocations = getClasspathLocations();
  }
 }

 /**
  * @param fqcn
  *            Name of superclass/interface on which to search
  */
 public final Vector<Class<?>> findSubclasses(String fqcn) {
  synchronized (classpathLocations) {
   synchronized (results) {
    try {
     classToSearch = null;
     errors = new ArrayList<Throwable>();
     results = new TreeMap<Class<?>, URL>(CLASS_COMPARATOR);
     // filter malformed FQCN
     if (fqcn.startsWith(".") || fqcn.endsWith(".")) {
      return new Vector<Class<?>>();
     }

     // Determine search class from fqcn
     try {
      classToSearch = Class.forName(fqcn);
     } catch (ClassNotFoundException ex) {
      errors.add(ex);
      return new Vector<Class<?>>();
     }

     return findSubclasses(classToSearch, classpathLocations);
    } finally {
    }
   }
  }
 }

 public final List<Throwable> getErrors() {
  return new ArrayList<Throwable>(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.
  */
 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.
  */
 public final Map<URL, String> getClasspathLocations() {
  Map<URL, String> map = new TreeMap<URL, String>(URL_COMPARATOR);
  File file = null;

  String pathSep = System.getProperty("path.separator");
  String classpath = System.getProperty("java.class.path");
  // System.out.println ("classpath=" + classpath);

  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() {
  public boolean accept(File f) {
   if (f.exists() && f.isDirectory())
    return true;
   else
    return false;
  }
 };

 private final static Comparator<URL> URL_COMPARATOR = new Comparator<URL>() {
  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<?>>() {
  public int compare(Class<?> c1, Class<?> c2) {
   return String.valueOf(c1).compareTo(String.valueOf(c2));
  }
 };

 private final 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 (int i = 0; i < dirs.length; i++) {
   try {
    // add the present package
    map.put(new URL("file://" + dirs[i].getCanonicalPath()), name
      + dirs[i].getName());
   } catch (IOException ioe) {
    return;
   }

   include(name + dirs[i].getName(), dirs[i], 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 final Vector<Class<?>> findSubclasses(Class<?> superClass,
   Map<URL, String> locations) {
  Vector<Class<?>> v = new Vector<Class<?>>();

  Vector<Class<?>> w = null; // new Vector<Class<?>> ();

  Iterator<URL> it = locations.keySet().iterator();
  while (it.hasNext()) {
   URL url = it.next();
   // System.out.println (url + "-->" + locations.get (url));

   w = findSubclasses(url, locations.get(url), superClass);
   if (w != null && (w.size() > 0))
    v.addAll(w);
  }

  return v;
 }

 private final Vector<Class<?>> findSubclasses(URL location,
   String packageName, Class<?> superClass) {
  // System.out.println ("looking in package:" + packageName);
  // System.out.println ("looking for  class:" + superClass);

  synchronized (results) {

   // hash guarantees unique names...
   Map<Class<?>, URL> thisResult = new TreeMap<Class<?>, URL>(
     CLASS_COMPARATOR);
   Vector<Class<?>> v = new Vector<Class<?>>(); // ...but return a
   // vector

   // TODO: double-check for null search class
   String fqcn = classToSearch.getName();

   List<URL> knownLocations = new ArrayList<URL>();
   knownLocations.add(location);
   // TODO: add getResourceLocations() to this list

   // iterate matching package locations...
   for (int loc = 0; loc < knownLocations.size(); loc++) {
    URL url = knownLocations.get(loc);

    // Get a File object for the package
    File directory = new File(url.getFile());

    // System.out.println ("\tlooking in " + directory);

    if (directory.exists()) {
     // Get the list of the files contained in the package
     String[] files = directory.list();
     for (int i = 0; i < files.length; i++) {
      // we are only interested in .class files
      if (files[i].endsWith(".class")) {
       // removes the .class extension
       String classname = files[i].substring(0, files[i]
         .length() - 6);

       // System.out.println ("\t\tchecking file " +
       // classname);

       try {
        Class<?> c = Class.forName(packageName + "."
          + classname);
        if (superClass.isAssignableFrom(c)
          && !fqcn.equals(packageName + "."
            + classname)) {
         thisResult.put(c, url);
        }
       } catch (ClassNotFoundException cnfex) {
        errors.add(cnfex);
        // System.err.println(cnfex);
       } catch (Exception ex) {
        errors.add(ex);
        // System.err.println (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();

      // System.out.println ("starts=" + starts);
      // System.out.println ("JarFile=" + jarFile);

      Enumeration<JarEntry> e = jarFile.entries();
      while (e.hasMoreElements()) {
       JarEntry entry = e.nextElement();
       String entryname = entry.getName();

       // System.out.println ("\tconsidering entry: " +
       // entryname);

       if (!entry.isDirectory()
         && entryname.endsWith(".class")) {
        String classname = entryname.substring(0,
          entryname.length() - 6);
        if (classname.startsWith("/"))
         classname = classname.substring(1);
        classname = classname.replace('/', '.');

        // System.out.println ("\t\ttesting classname: "
        // + classname);

        try {
         // TODO: verify this block
         Class<?> c = Class.forName(classname);

         if (superClass.isAssignableFrom(c)
           && !fqcn.equals(classname)) {
          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) {
      // System.err.println(ioex);
      errors.add(ioex);
     }
    }
   } // while

   // System.out.println ("results = " + thisResult);

   results.putAll(thisResult);

   Iterator<Class<?>> it = thisResult.keySet().iterator();
   while (it.hasNext()) {
    v.add(it.next());
   }
   return v;

  } // synch results
 }

 public static void main(String[] args) {
  args = new String[] { "java.lang.Object" };

  SubClassFinder finder = null;
  Vector<Class<?>> v = null;
  List<Throwable> errors = null;

  if (args.length == 1) {
   finder = new SubClassFinder();
   v = finder.findSubclasses(args[0]);
   errors = finder.getErrors();
  } else {
   System.out
     .println("Usage: java SubClassFinder <fully.qualified.superclass.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)) : "?"));
   }
  } else {
   System.out.println("No subclasses of " + args[0] + " found.");
  }
 }
}

6 comments:

Anonymous said...

Dude very nice post It absolutely fills my requirement, I just copied n executed. :) It works great, Thanks allot, I ll follow your blog from now on..

Anonymous said...

Nice brief and this enter helped me alot in my college assignement. Say thank you you seeking your information.

Anonymous said...

The authoritative answer

Anonymous said...

Great..!!! Even spring does the same, but this is very handful for stand alone applications..

Stefan said...

I solved the performance issues by 1. changing the key type of the 'classpathLocations' map from URL to String. Putting URLs in maps seems to be slow because of needed comparision with existing entries AFAIK
2. adding a package filter, which causes that only packages will be browsed that starts with the given notation

Anonymous said...

Thanks man, I was desperate to find something like this. You just saved me a lot of time!