AutoDiscoverCarambaConfigUtil.java :  » Web-Framework » Caramba » org » caramba » config » Java Open Source

Java Open Source » Web Framework » Caramba 
Caramba » org » caramba » config » AutoDiscoverCarambaConfigUtil.java
/* Copyright 2005-2006 Tim Fennell
 *
 * 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.caramba.config;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.servlet.ServletContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

/**
 * <p>ResolverUtil is used to locate classes that implement an interface or extend a given base
 * class. It does this in two different ways.  The first way is by accessing the
 * {@link Thread#getContextClassLoader() Context ClassLoader} and attempting to discover the set
 * of URLs that are used for classloading.  The second mechanism uses the {@link ServletContext}
 * to discover classes under {@code /WEB-INF/classes/} and jar files under {@code /WEB-INF/lib/}</p>.
 * <p/>
 * <p>The first mechanism is generally preferred since it can usually discover classes in more
 * locations, but it requires that the context class loader be a subclass of {@link URLClassLoader}.
 * Most containers use class loaders that extend URLClassloader, but not all do.  Since accessing
 * resources through the ServletContext is mandated to work in the Servlet specification this should
 * work in all containers.</p>
 * <p/>
 * <p>Since scanning all classpath entries and/or jars under {@code /WEB-INF/lib/} can take a
 * non-trivial amount of time, it is possible to filter the set of locations and packages that
 * are examined.  This is done by supplying Collections of filter patterns.  The
 * {@code locationFilters} are used to match the locations (directories, jar files, etc.) examined.
 * The {@code packageFilters} restricts the set of classes loaded by package.  In both cases a
 * simple sub-string match is used.  For example if location patterns of ["project1", project2"] are
 * supplied, you would see the following:</p>
 * <p/>
 * <pre>
 * lib/project1/dependencies/dep1.jar  -> scanned
 * lib/project3/dependencies/dep79.jar -> not scanned
 * WEB-INF/lib/project1-web.jar        -> scanned
 * WEB-INF/classes                     -> not scanned
 * lib/project2/project2-business.jar  -> scanned
 * </pre>
 * <p/>
 * <p>If no location filters are supplied, all discovered locations will be scanned for classes.
 * If no package filters are supplied, all classes discovered will be checked.</p>
 * <p/>
 * <p>At first glance it may seem redundant to provide the class type being searched for at
 * instantiation time, and again when invoking one of the {@code load()} methods.  However,
 * this allows for certain usages that would not otherwise be possible.  For example, the
 * following is used to find all collections that support ordering of some kind:</p>
 * <p/>
 * <pre>
 * ResolverUtil&lt;Collection&gt; resolver = new ResolverUtil&lt;Collection&gt;();
 * resolver.loadImplementationsFromContextClassloader(List.class);
 * resolver.loadImplementationsFromContextClassloader(SortedSet.class);
 * Set&lt;Class&lt;? extends Collection&gt;&gt; classes = resolver.getClasses();
 * </pre>
 *
 * @author Tim Fennell
 */
public class AutoDiscoverCarambaConfigUtil<T> {
    /**
     * An instance of Log to use for logging in this class.
     */
    private static final transient Log log = LogFactory.getLog(AutoDiscoverCarambaConfigUtil.class);

    /**
     * Set of filter strings used to match URLs to check for classes.
     */
    private Set<String> locationFilters = new HashSet<String>();

    /**
     * Set of filter strings used to match package names of classes to load and check.
     */
    private Set<String> packageFilters = new HashSet<String>();

    /**
     * The set of implementations being accumulated.
     */
    private Set<Class<? extends T>> implementations = new HashSet<Class<? extends T>>();

    /**
     * Sets the collection of location filter patterns to use when deciding whether to check
     * a given location for classes.  Removes any "*" wildcards from the String just in case.
     *
     * @param patterns a set of patterns used to match locations for finding classes
     */
    public void setLocationFilters(Collection<String> patterns) {
        // Try and bullet proof this a little by removing any * characters folks
        // might have added, thinking we actually support wild-carding ;)
        this.locationFilters.clear();
        for (String pattern : patterns) {
            locationFilters.add(pattern.replace("*", ""));
        }
    }

    /**
     * Sets the collection of package filter patterns to use when deciding whether to load
     * and examine classes.
     *
     * @param patterns a set of patterns to match against fully qualified class names
     */
    public void setPackageFilters(Collection<String> patterns) {
        this.packageFilters.clear();
        for (String pattern : patterns) {
            packageFilters.add(pattern.replace("*", "").replace(".", "/"));
        }
    }

    /**
     * Provides access to the classes discovered so far. If neither of
     * {@link #loadImplementationsFromContextClassloader(Class)} or
     * been called, this set will be empty.
     *
     * @return the set of classes that have been discovered.
     */
    public Set<Class<? extends T>> getClasses() {
        return implementations;
    }

    /**
     * <p>Attempts to locate, load and examine classes using the ServletContext to load resources
     * from {@code /WEB-INF/}. While dependent on the Servlet API and restricted to looking for
     * classes in {@code /WEB-INF/classes} and libraries in {@code /WEB-INF/lib}, this method
     * should work in all servlet containers regardless of classloading implementation.</p>
     * <p/>
     * <p>Locations and classes are examined with respect to any filters set.  Classes are
     * stored internally and may be accessed (along with any other previously resolved classes)
     * by calling {@link #getClasses()}.</p>
     *
     * @param parentType an interface or class to find implementations or subclasses of.
     * @param context    a ServletContext from which to load resources
     */
    public void loadImplementationsFromServletContext(Class<? extends T> parentType,
                                                      ServletContext context, boolean pCheckLibDir) {
        // Always scan WEB-INF/classes
        log.info("Checking for classes in /WEB-INF/classes using ServletContext resources.");
        loadImplementationsFromServletContext(parentType, "/WEB-INF/classes/", context);

        if (pCheckLibDir) {
            // Now scan WEB-INF/lib
            Set<String> jars = context.getResourcePaths("/WEB-INF/lib/");
            if (jars != null) {
                for (String jarName : jars) {
                    if (matchesAny(jarName, locationFilters)) {
                        //                    log.info("Checking web application library '", jarName,
                        //                             "' for instances of ", parentType.getName());

                        loadImplementationsInJar(parentType,
                                context.getResourceAsStream(jarName),
                                jarName);
                    }
                }
            }
        }
    }

    /**
     * Internal method that will find any classes in the supplied sub-directory of
     * {@code /WEB-INF/classes} and then recurse for any directories found within the
     * current directory.
     *
     * @param parentType an interface or class to find implementations or subclasses of.
     * @param context    a ServletContext from which to load resources
     * @param path       the path within /WEB-INF/classes to be checked
     */
    private void loadImplementationsFromServletContext(Class<? extends T> parentType,
                                                       String path,
                                                       ServletContext context) {
        Set<String> paths = context.getResourcePaths(path);
        if (paths != null) {
            for (String subPath : paths) {
                // Recurse for directories
                if (subPath.endsWith("/")) {
                    loadImplementationsFromServletContext(parentType, subPath, context);
                } else if (subPath.endsWith(".class")) {
                    addIfAssignableTo(parentType, subPath.replace("/WEB-INF/classes/", ""));
                }
            }
        }
    }


    /**
     * <p>Locates all implementations of an interface in the classloader being used by this thread.
     * Scans the current classloader and all parents.  Scans only in the URLs in the ClassLoaders
     * which match the filters provided, and within those URLs only checks classes within the
     * packages defined by the package filters provided.</p>
     * <p/>
     * <p>This method relies on the fact that most ClassLoaders in the wild extend the built-in
     * {@link URLClassLoader}. This is relied upon because there is no standard way to discover
     * the set of locations from which a ClassLoader is loading classes.  The URLClassLoader
     * exposes methods to discover this, and those are made use of to within this method.</p>
     *
     * @param parentType an interface or class to find implementations or subclasses of.
     * @return true if the classloader was a subclass of {@link URLClassLoader} and was scanned,
     *         false if the classloader could not be scanned.
     */
    public boolean loadImplementationsFromContextClassloader(Class<? extends T> parentType) {
        ClassLoader loader = this.getClass().getClassLoader();

        // If it's not a URLClassLoader, we can't deal with it!
        if (!(loader instanceof URLClassLoader)) {
//            log.error("The current ClassLoader is not castable to a URLClassLoader. ClassLoader ",
//                    "is of type [", loader.getClass().getName(), "]. Cannot scan ClassLoader for ",
//                    "implementations of ", parentType.getClass().getName(), ". When this is the ",
//                    "case you *must* put your ActionBean classes in either /WEB-INF/classes ",
//                    "or in a jar in /WEB-INF/lib for Stripes to find them."
//            );
            return false;
        } else {
            Collection<URL> urls = new HashSet<URL>();
            while (loader != null) {
                try {
                    URLClassLoader urlLoader = (URLClassLoader) loader;
                    urls.addAll(Arrays.asList(urlLoader.getURLs()));
                }
                catch (Exception e) { /* Do nothing */ }
                loader = loader.getParent();
            }

            for (URL url : urls) {
                String path = url.getFile();
                System.out.println("loading path: " + path);
                try {
                    path = URLDecoder.decode(path, "UTF-8");
                }
                catch (UnsupportedEncodingException e) { /* UTF-8 is a required encoding */ }
                File location = new File(path);

                // Manage what happens when Resin decides to return URLs that do not
                // match reality!  For whatever reason, Resin decides to return some JAR
                // URLs with an extra '!/' on the end of the jar file name and a file:
                // in front even though that's the protocol spec, not the path!
                if (!location.exists()) {
                    if (path.endsWith("!/")) path = path.substring(0, path.length() - 2);
                    if (path.startsWith("file:")) path = path.substring(5);
                    location = new File(path);
                }

                // Only process the URL if it matches one of our filter strings
                if (matchesAny(path, locationFilters)) {
//                    log.info("Checking URL '", path, "' for instances of ", parentType.getName());
                    if (location.isDirectory()) {
                        loadImplementationsInDirectory(parentType, null, location);
                    } else {
                        loadImplementationsInJar(parentType, null, path);
                    }
                }
            }

            return true;
        }
    }

    /**
     * Checks to see if one or more of the filter strings occurs within the string specified. If
     * so, returns true.  Otherwise returns false.
     *
     * @param text    the text within which to look for the filter strings
     * @param filters a set of substrings to look for in the text
     */
    private boolean matchesAny(String text, Set<String> filters) {
        if (filters.size() == 0) {
            return true;
        }
        for (String filter : filters) {
            if (text.indexOf(filter) != -1) {
                return true;
            }
        }
        return false;
    }

    /**
     * Finds implementations of an interface in a physical directory on a filesystem.  Examines all
     * files within a directory - if the File object is not a directory, and ends with <i>.class</i>
     * the file is loaded and tested to see if it is an implemenation of the interface.  Operates
     * recursively to find classes within a folder structure matching the package structure.
     *
     * @param parentType an interface or class to find implementations or subclasses of.
     * @param parent     the package name up to this directory in the package hierarchy.  E.g. if
     *                   /classes is in the classpath and we wish to examine files in /classes/org/apache then
     *                   the values of <i>parent</i> would be <i>org/apache</i>
     * @param location   a File object representing a directory
     */
    private void loadImplementationsInDirectory(Class<? extends T> parentType,
                                                String parent, File location) {
        File[] files = location.listFiles();
        StringBuilder builder = null;

        for (File file : files) {
            builder = new StringBuilder(100);
            builder.append(parent).append("/").append(file.getName());
            String packageOrClass = (parent == null ? file.getName() : builder.toString());

            if (file.isDirectory()) {
                loadImplementationsInDirectory(parentType, packageOrClass, file);
            } else if (file.getName().endsWith(".class")) {
                if (matchesAny(packageOrClass, packageFilters)) {
                    addIfAssignableTo(parentType, packageOrClass);
                }
            }
        }
    }

    /**
     * Finds implementations of an interface within a jar files that contains a folder structure
     * matching the package structure.  If the File is not a JarFile or does not exist a warning
     * will be logged, but no error will be raised.  In this case an empty Set will be returned.
     *
     * @param parentType  an interface or class to find implementations or subclasses of.
     * @param inputStream a regular (non-jar/non-zip) input stream from which to read the
     *                    jar file in question
     * @param location    the location of the jar file being examined. Used to create the input
     *                    stream if the input stream is null, and to log appropriate error messages
     */
    private void loadImplementationsInJar(Class<? extends T> parentType,
                                          InputStream inputStream,
                                          String location) {

        try {
            JarEntry entry;
            if (inputStream == null) inputStream = new FileInputStream(location);
            JarInputStream jarStream = new JarInputStream(inputStream);

            while ((entry = jarStream.getNextJarEntry()) != null) {
                String name = entry.getName();
                if (!entry.isDirectory() && name.endsWith(".class")) {
                    if (matchesAny(name, this.packageFilters)) {
                        addIfAssignableTo(parentType, name);
                    }
                }
            }
        }
        catch (IOException ioe) {
//            log.error("Could not search jar file '", location, "' for implementations of ",
//                      parentType.getName(), "due to an IOException: ", ioe.getMessage());
        }
    }

    /**
     * Add the class designated by the fully qualified class name provided to the set of
     * resolved classes if and only if it extends/implements the parent type supplied.
     *
     * @param parentType the interface or class to add implementations or subclasses of.
     * @param fqn        the fully qualified name of a class
     */
    private void addIfAssignableTo(Class<? extends T> parentType, String fqn) {
        try {
//            log.trace("Checking to see if class '", fqn, "' implements ", parentType.getName());
            ClassLoader loader = this.getClass().getClassLoader();
            String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');

            Class type = loader.loadClass(externalName);
            if (parentType.isAssignableFrom(type)) {
                implementations.add((Class<T>) type);
            }
        }
        catch (Throwable t) {
//            log.warn("Could not examine class '", fqn, "'", " due to a ",
//                     t.getClass().getName(), " with message: ", t.getMessage());
        }
    }
}
java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.