ResolverUtil.java

  1. /*
  2.  *    Copyright 2009-2021 the original author or authors.
  3.  *
  4.  *    Licensed under the Apache License, Version 2.0 (the "License");
  5.  *    you may not use this file except in compliance with the License.
  6.  *    You may obtain a copy of the License at
  7.  *
  8.  *       http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  *    Unless required by applicable law or agreed to in writing, software
  11.  *    distributed under the License is distributed on an "AS IS" BASIS,
  12.  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  *    See the License for the specific language governing permissions and
  14.  *    limitations under the License.
  15.  */
  16. package org.apache.ibatis.io;

  17. import java.io.IOException;
  18. import java.lang.annotation.Annotation;
  19. import java.util.HashSet;
  20. import java.util.List;
  21. import java.util.Set;

  22. import org.apache.ibatis.logging.Log;
  23. import org.apache.ibatis.logging.LogFactory;

  24. /**
  25.  * <p>ResolverUtil is used to locate classes that are available in the/a class path and meet
  26.  * arbitrary conditions. The two most common conditions are that a class implements/extends
  27.  * another class, or that is it annotated with a specific annotation. However, through the use
  28.  * of the {@link Test} class it is possible to search using arbitrary conditions.</p>
  29.  *
  30.  * <p>A ClassLoader is used to locate all locations (directories and jar files) in the class
  31.  * path that contain classes within certain packages, and then to load those classes and
  32.  * check them. By default the ClassLoader returned by
  33.  * {@code Thread.currentThread().getContextClassLoader()} is used, but this can be overridden
  34.  * by calling {@link #setClassLoader(ClassLoader)} prior to invoking any of the {@code find()}
  35.  * methods.</p>
  36.  *
  37.  * <p>General searches are initiated by calling the {@link #find(Test, String)} and supplying
  38.  * a package name and a Test instance. This will cause the named package <b>and all sub-packages</b>
  39.  * to be scanned for classes that meet the test. There are also utility methods for the common
  40.  * use cases of scanning multiple packages for extensions of particular classes, or classes
  41.  * annotated with a specific annotation.</p>
  42.  *
  43.  * <p>The standard usage pattern for the ResolverUtil class is as follows:</p>
  44.  *
  45.  * <pre>
  46.  * ResolverUtil&lt;ActionBean&gt; resolver = new ResolverUtil&lt;ActionBean&gt;();
  47.  * resolver.findImplementation(ActionBean.class, pkg1, pkg2);
  48.  * resolver.find(new CustomTest(), pkg1);
  49.  * resolver.find(new CustomTest(), pkg2);
  50.  * Collection&lt;ActionBean&gt; beans = resolver.getClasses();
  51.  * </pre>
  52.  *
  53.  * @author Tim Fennell
  54.  * @param <T>
  55.  *          the generic type
  56.  */
  57. public class ResolverUtil<T> {

  58.   /**
  59.    * An instance of Log to use for logging in this class.
  60.    */
  61.   private static final Log log = LogFactory.getLog(ResolverUtil.class);

  62.   /**
  63.    * A simple interface that specifies how to test classes to determine if they
  64.    * are to be included in the results produced by the ResolverUtil.
  65.    */
  66.   public interface Test {

  67.     /**
  68.      * Will be called repeatedly with candidate classes. Must return True if a class
  69.      * is to be included in the results, false otherwise.
  70.      *
  71.      * @param type
  72.      *          the type
  73.      * @return true, if successful
  74.      */
  75.     boolean matches(Class<?> type);
  76.   }

  77.   /**
  78.    * A Test that checks to see if each class is assignable to the provided class. Note
  79.    * that this test will match the parent type itself if it is presented for matching.
  80.    */
  81.   public static class IsA implements Test {

  82.     /** The parent. */
  83.     private Class<?> parent;

  84.     /**
  85.      * Constructs an IsA test using the supplied Class as the parent class/interface.
  86.      *
  87.      * @param parentType
  88.      *          the parent type
  89.      */
  90.     public IsA(Class<?> parentType) {
  91.       this.parent = parentType;
  92.     }

  93.     /** Returns true if type is assignable to the parent type supplied in the constructor. */
  94.     @Override
  95.     public boolean matches(Class<?> type) {
  96.       return type != null && parent.isAssignableFrom(type);
  97.     }

  98.     @Override
  99.     public String toString() {
  100.       return "is assignable to " + parent.getSimpleName();
  101.     }
  102.   }

  103.   /**
  104.    * A Test that checks to see if each class is annotated with a specific annotation. If it
  105.    * is, then the test returns true, otherwise false.
  106.    */
  107.   public static class AnnotatedWith implements Test {

  108.     /** The annotation. */
  109.     private Class<? extends Annotation> annotation;

  110.     /**
  111.      * Constructs an AnnotatedWith test for the specified annotation type.
  112.      *
  113.      * @param annotation
  114.      *          the annotation
  115.      */
  116.     public AnnotatedWith(Class<? extends Annotation> annotation) {
  117.       this.annotation = annotation;
  118.     }

  119.     /** Returns true if the type is annotated with the class provided to the constructor. */
  120.     @Override
  121.     public boolean matches(Class<?> type) {
  122.       return type != null && type.isAnnotationPresent(annotation);
  123.     }

  124.     @Override
  125.     public String toString() {
  126.       return "annotated with @" + annotation.getSimpleName();
  127.     }
  128.   }

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

  131.   /**
  132.    * The ClassLoader to use when looking for classes. If null then the ClassLoader returned
  133.    * by Thread.currentThread().getContextClassLoader() will be used.
  134.    */
  135.   private ClassLoader classloader;

  136.   /**
  137.    * Provides access to the classes discovered so far. If no calls have been made to
  138.    * any of the {@code find()} methods, this set will be empty.
  139.    *
  140.    * @return the set of classes that have been discovered.
  141.    */
  142.   public Set<Class<? extends T>> getClasses() {
  143.     return matches;
  144.   }

  145.   /**
  146.    * Returns the classloader that will be used for scanning for classes. If no explicit
  147.    * ClassLoader has been set by the calling, the context class loader will be used.
  148.    *
  149.    * @return the ClassLoader that will be used to scan for classes
  150.    */
  151.   public ClassLoader getClassLoader() {
  152.     return classloader == null ? Thread.currentThread().getContextClassLoader() : classloader;
  153.   }

  154.   /**
  155.    * Sets an explicit ClassLoader that should be used when scanning for classes. If none
  156.    * is set then the context classloader will be used.
  157.    *
  158.    * @param classloader a ClassLoader to use when scanning for classes
  159.    */
  160.   public void setClassLoader(ClassLoader classloader) {
  161.     this.classloader = classloader;
  162.   }

  163.   /**
  164.    * Attempts to discover classes that are assignable to the type provided. In the case
  165.    * that an interface is provided this method will collect implementations. In the case
  166.    * of a non-interface class, subclasses will be collected.  Accumulated classes can be
  167.    * accessed by calling {@link #getClasses()}.
  168.    *
  169.    * @param parent
  170.    *          the class of interface to find subclasses or implementations of
  171.    * @param packageNames
  172.    *          one or more package names to scan (including subpackages) for classes
  173.    * @return the resolver util
  174.    */
  175.   public ResolverUtil<T> findImplementations(Class<?> parent, String... packageNames) {
  176.     if (packageNames == null) {
  177.       return this;
  178.     }

  179.     Test test = new IsA(parent);
  180.     for (String pkg : packageNames) {
  181.       find(test, pkg);
  182.     }

  183.     return this;
  184.   }

  185.   /**
  186.    * Attempts to discover classes that are annotated with the annotation. Accumulated
  187.    * classes can be accessed by calling {@link #getClasses()}.
  188.    *
  189.    * @param annotation
  190.    *          the annotation that should be present on matching classes
  191.    * @param packageNames
  192.    *          one or more package names to scan (including subpackages) for classes
  193.    * @return the resolver util
  194.    */
  195.   public ResolverUtil<T> findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
  196.     if (packageNames == null) {
  197.       return this;
  198.     }

  199.     Test test = new AnnotatedWith(annotation);
  200.     for (String pkg : packageNames) {
  201.       find(test, pkg);
  202.     }

  203.     return this;
  204.   }

  205.   /**
  206.    * Scans for classes starting at the package provided and descending into subpackages.
  207.    * Each class is offered up to the Test as it is discovered, and if the Test returns
  208.    * true the class is retained.  Accumulated classes can be fetched by calling
  209.    * {@link #getClasses()}.
  210.    *
  211.    * @param test
  212.    *          an instance of {@link Test} that will be used to filter classes
  213.    * @param packageName
  214.    *          the name of the package from which to start scanning for classes, e.g. {@code net.sourceforge.stripes}
  215.    * @return the resolver util
  216.    */
  217.   public ResolverUtil<T> find(Test test, String packageName) {
  218.     String path = getPackagePath(packageName);

  219.     try {
  220.       List<String> children = VFS.getInstance().list(path);
  221.       for (String child : children) {
  222.         if (child.endsWith(".class")) {
  223.           addIfMatching(test, child);
  224.         }
  225.       }
  226.     } catch (IOException ioe) {
  227.       log.error("Could not read package: " + packageName, ioe);
  228.     }

  229.     return this;
  230.   }

  231.   /**
  232.    * Converts a Java package name to a path that can be looked up with a call to
  233.    * {@link ClassLoader#getResources(String)}.
  234.    *
  235.    * @param packageName
  236.    *          The Java package name to convert to a path
  237.    * @return the package path
  238.    */
  239.   protected String getPackagePath(String packageName) {
  240.     return packageName == null ? null : packageName.replace('.', '/');
  241.   }

  242.   /**
  243.    * Add the class designated by the fully qualified class name provided to the set of
  244.    * resolved classes if and only if it is approved by the Test supplied.
  245.    *
  246.    * @param test the test used to determine if the class matches
  247.    * @param fqn the fully qualified name of a class
  248.    */
  249.   @SuppressWarnings("unchecked")
  250.   protected void addIfMatching(Test test, String fqn) {
  251.     try {
  252.       String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
  253.       ClassLoader loader = getClassLoader();
  254.       if (log.isDebugEnabled()) {
  255.         log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
  256.       }

  257.       Class<?> type = loader.loadClass(externalName);
  258.       if (test.matches(type)) {
  259.         matches.add((Class<T>) type);
  260.       }
  261.     } catch (Throwable t) {
  262.       log.warn("Could not examine class '" + fqn + "'" + " due to a "
  263.           + t.getClass().getName() + " with message: " + t.getMessage());
  264.     }
  265.   }
  266. }