ResolverUtil.java

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  153.   /**
  154.    * Sets an explicit ClassLoader that should be used when scanning for classes. If none is set then the context
  155.    * classloader will be used.
  156.    *
  157.    * @param classloader
  158.    *          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 that an interface is provided
  165.    * this method will collect implementations. In the case of a non-interface class, subclasses will be collected.
  166.    * Accumulated classes can be accessed by calling {@link #getClasses()}.
  167.    *
  168.    * @param parent
  169.    *          the class of interface to find subclasses or implementations of
  170.    * @param packageNames
  171.    *          one or more package names to scan (including subpackages) for classes
  172.    *
  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 classes can be accessed by calling
  187.    * {@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.    *
  194.    * @return the resolver util
  195.    */
  196.   public ResolverUtil<T> findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
  197.     if (packageNames == null) {
  198.       return this;
  199.     }

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

  204.     return this;
  205.   }

  206.   /**
  207.    * Scans for classes starting at the package provided and descending into subpackages. Each class is offered up to the
  208.    * Test as it is discovered, and if the Test returns true the class is retained. Accumulated classes can be fetched by
  209.    * calling {@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.    *
  216.    * @return the resolver util
  217.    */
  218.   public ResolverUtil<T> find(Test test, String packageName) {
  219.     String path = getPackagePath(packageName);

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

  230.     return this;
  231.   }

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

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

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