com.complexible.common.util.ClassPath.java Source code

Java tutorial

Introduction

Here is the source code for com.complexible.common.util.ClassPath.java

Source

/*
 * Copyright (c) 2005-2011 Clark & Parsia, LLC. <http://www.clarkparsia.com>
 *
 * 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 com.complexible.common.util;

import com.complexible.common.io.Files2;
import com.google.common.collect.Collections2;
import com.google.common.collect.Sets;
import com.google.common.base.Predicate;

import java.io.File;
import java.io.IOException;
import java.io.FileFilter;

import java.util.List;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Collection;
import java.util.Enumeration;

import java.util.Set;

import java.util.jar.JarFile;
import java.util.jar.JarEntry;

import java.lang.reflect.Modifier;

import java.net.URLClassLoader;
import java.net.URL;
import java.net.URLConnection;
import java.net.JarURLConnection;
import java.net.MalformedURLException;

/**
 * <p>Utility class for managing the system class path.  It will return all classes on the class path, and
 * can override the system class loader in order to provide the ability to add classes to the path at runtime and then
 * be able to find them (via Class.forName) and load/instantiate them in a running application.</p>
 *
 * @author Michael Grove
 * @since 1.0
 * @version 2.0
 */
public class ClassPath {
    final public static boolean QUIET = true;

    private static Collection<Class> mClasses;

    private static MutableURLClassLoader mLoader;

    static {
        mLoader = new MutableURLClassLoader(new URL[] {}, Thread.currentThread().getContextClassLoader());

        Thread.currentThread().setContextClassLoader(mLoader);
    }

    /**
     * Return all the classes which implement/extend the given class and are instantiable (ie not abstract,
     * not interfaces themselves)
     * @param theClass the parent class/interface
     * @return all matching classes
     */
    public static Collection<Class> instantiableClasses(Class<?> theClass) {
        return Collections2.filter(classes(theClass), new Predicate<Class>() {
            public boolean apply(final Class theClass) {
                return !theClass.isInterface() && !Modifier.isAbstract(theClass.getModifiers());
            }
        });
    }

    /**
     * Get all the classes which implement/extend the given class
     * @param theInterface the parent class/interface
     * @return all matching classes
     */
    public static Collection<Class> classes(final Class<?> theInterface) {
        return Collections2.filter(classes(), new Predicate<Class>() {
            public boolean apply(final Class theObject) {
                return theInterface.isAssignableFrom(theObject);
            }
        });
    }

    /**
     * Add a URL to a jar which contains classes that should be loaded into the application
     * @param theURL the URL to a jar file to load
     */
    public static void add(URL... theURL) {

        Set<URL> curr = Sets.newHashSet(mLoader.getURLs());
        Set<URL> newURLS = Sets.newHashSet(theURL);

        if (!Sets.intersection(curr, newURLS).isEmpty()) {
            curr.removeAll(newURLS);

            mLoader = new MutableURLClassLoader(curr.toArray(new URL[curr.size()]), mLoader.getParent());
        }

        mLoader.addURL(theURL);

        // invalidate our cache
        mClasses = null;
    }

    /**
     * Add Files on the local disk that the utility should look into for jars & classes in addition to just what is
     * specified on the system class path.
     * @param theFiles the files to add
     */
    public static void add(File... theFiles) {
        for (File aFile : theFiles) {
            if (!aFile.exists() || !aFile.canRead()) {
                continue;
            }

            List<File> aJarList = Files2.listFiles(aFile, new JarFileFilter());
            for (File aJarFile : aJarList) {
                try {
                    mLoader.addURL(aJarFile.toURI().toURL());
                } catch (MalformedURLException e) {
                    // TODO: log me
                }
            }
        }

        // invalidate our cache
        mClasses = null;
    }

    /**
     * Return the list of classes on the class path and otherwise associated with the class loader
     * @return the classes
     */
    public static Collection<Class> classes() {
        if (mClasses == null) {
            Collection<Class> aClassList = new HashSet<Class>();

            String aSystemPath = System.getProperty("java.class.path");

            if (aSystemPath != null) {
                List<File> aFileList = new ArrayList<File>();

                for (String aPath : aSystemPath.split(File.pathSeparator)) {
                    aFileList.add(new File(aPath));
                }

                for (File aFile : aFileList) {
                    if (!aFile.exists()) {
                        // maybe a bum entry on the class path, so just skip it
                        continue;
                    } else if (aFile.isDirectory()) {
                        // either a directory of jar files, or a dir of class files
                        aClassList.addAll(listClassesFromDir(aFile));
                    } else {
                        // probably a jar file...
                        aClassList.addAll(listClassesFromJar(aFile));
                    }
                }
            }

            for (URL aURL : mLoader.getURLs()) {
                if (aURL.toString().endsWith(".jar")) {
                    if (!aURL.toString().startsWith("jar:")) {
                        try {
                            aURL = new URL("jar:" + aURL.toString() + "!/");
                        } catch (MalformedURLException e) {
                            // no-op, this should happen, but we should TODO: log this
                            e.printStackTrace();
                        }
                    }

                    aClassList.addAll(listClassesFromJar(aURL));
                } else if (aURL.toString().endsWith(".class")) {
                    // fuck, skip this.  i cant' imagine anyone pointing at a single class file.
                } else {
                    // don't know what to do in this case, lets just skip it
                }
            }

            mClasses = aClassList;
        }

        return mClasses;
    }

    /**
     * Return the list of classes in the given directory.  This will find any "loose" classes as well as classes packaged
     * inside of jar files.  It will also recurse through subdirectories to find classes.
     * @param theFile the directory to load from
     * @return the list of classes in the directory
     */
    public static Collection<? extends Class> listClassesFromDir(final File theFile) {
        // a dir with either jars or classes, or both

        Collection<Class> aClassList = new HashSet<Class>();

        // recurse the directory and file all the jar files and load the classes in them
        List<File> aJarList = Files2.listFiles(theFile, new JarFileFilter());
        for (File aJarFile : aJarList) {
            aClassList.addAll(listClassesFromJar(aJarFile));
        }

        // now find all the class files and load the classes specified by them
        List<File> aClassFileList = Files2.listFiles(theFile, new ClassFileFilter());
        for (File aClassFile : aClassFileList) {
            Class aClass = _class(classNameFromFileName(
                    aClassFile.getAbsolutePath().substring(theFile.getAbsolutePath().length() + 1)));
            if (aClass != null) {
                aClassList.add(aClass);
            }
        }

        return aClassList;
    }

    /**
     * Return the class with the given class name, filtering for system classes.
     * @param theClassName the class name
     * @return the class, or null if it cannot be loaded
     */
    private static Class _class(String theClassName) {
        if (theClassName == null) {
            return null;
        }

        // skip system classes and known libraries, we don't really care if we include them,
        // this is mostly for user defined stuff.  this is hackish, but it should cut down on the overhead of
        // reading everything in, both in performance and in memory requirements.
        if (theClassName.startsWith("java.") || theClassName.startsWith("javax.") || theClassName.startsWith("sun.")
                || theClassName.startsWith("com.sun.") || theClassName.startsWith("apple.")
                || theClassName.startsWith("com.apple") || theClassName.startsWith("org.jdesktop.")) {
            return null;
        }

        try {
            return get(theClassName);
        } catch (Throwable e) {
            if (!QUIET) {
                e.printStackTrace();
            }
        }

        return null;
    }

    /**
     * Return the list of classes from a JarFile object
     * @param theFile the JarFile to get classes from
     * @return the list of classes in the jar file
     */
    private static Collection<? extends Class> listClassesFromJarFile(JarFile theFile) {
        Collection<Class> aClassList = new HashSet<Class>();

        Enumeration<JarEntry> aEntries = theFile.entries();
        while (aEntries.hasMoreElements()) {
            JarEntry aEntry = aEntries.nextElement();
            if (aEntry.getName().endsWith(".class")) {
                // if it looks like a class file, and talks like a class file, then it must be...
                Class aClass = _class(classNameFromFileName(aEntry.getName()));

                if (aClass != null) {
                    aClassList.add(aClass);
                }
            }
        }

        return aClassList;
    }

    /**
     * Returns the list of classes at the specified jar URL
     * @param theURL the jar URL
     * @return returns the list of classes at the jar location, or an empty collection if there are no classes, it's not
     * a Jar URL or the URL cannot be opened.
     */
    public static Collection<? extends Class> listClassesFromJar(URL theURL) {
        try {
            URLConnection aConn = theURL.openConnection();

            if (!(aConn instanceof JarURLConnection)) {
                // TODo: log warning
                return new HashSet<Class>();
            }

            JarURLConnection aJarConn = (JarURLConnection) aConn;

            return listClassesFromJarFile(aJarConn.getJarFile());
        } catch (IOException e) {
            // TODO: log warning
            return new HashSet<Class>();
        }
    }

    /**
     * Get a class by its class name.  This will use a custom class loader.
     * @param theName the fully qualified name of the class to load
     * @return the class, or null if it is not found or otherwise could not be loaded.
     */
    public static Class get(String theName) {
        try {
            return Class.forName(theName, true, mLoader);
        } catch (ClassNotFoundException e) {
            return null;
        }
    }

    /**
     * Returns the list of classes in the Jar file at the specified file location
     * @param theFile the file location of the jar
     * @return the list of classes in the jar file
     */
    public static Collection<? extends Class> listClassesFromJar(File theFile) {
        try {
            if (theFile.canRead()) {
                return listClassesFromJarFile(new JarFile(theFile));
            }
        } catch (IOException e) {
            if (!QUIET) {
                e.printStackTrace();
            }
        }

        return new HashSet<Class>();
    }

    /**
     * Given a file name for a .class file, return the class name of the class represented by the file name
     * @param theName the path of the .class file
     * @return the fully qualified class name of the class
     */
    private static String classNameFromFileName(final String theName) {
        // chop off the extension (.class) and replace the dir separators with .
        return theName.substring(0, theName.length() - ".class".length()).replaceAll("/|\\\\", "\\.");
    }

    /**
     * FileFilter implementation for filtering a list of files so only class files are included
     */
    private static class ClassFileFilter implements FileFilter {
        /**
         * @inheritDoc
         */
        public boolean accept(final File thePathname) {
            return thePathname.getName().toLowerCase().endsWith(".class");
        }
    }

    /**
     * FileFilter implementation for filtering a list of files to only include jar files
     */
    private static class JarFileFilter implements FileFilter {
        /**
         * @inheritDoc
         */
        public boolean accept(final File thePathname) {
            return thePathname.getName().toLowerCase().endsWith(".jar");
        }
    }

    /**
     * Extends URLClassLoader for the sole purpose of exposing the addURL method.
     */
    public static class MutableURLClassLoader extends URLClassLoader {

        public MutableURLClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent);
        }

        public void addURL(URL... theURLs) {
            for (URL aURL : theURLs) {
                super.addURL(aURL);
            }
        }
    }
}