ResolverUtil.java

  1. /*
  2.  *    Copyright 2010-2022 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.migration.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. /**
  23.  * <p>
  24.  * ResolverUtil is used to locate classes that are available in the/a class path and meet arbitrary conditions. The two
  25.  * most common conditions are that a class implements/extends another class, or that is it annotated with a specific
  26.  * annotation. However, through the use of the {@link Test} class it is possible to search using arbitrary conditions.
  27.  * </p>
  28.  * <p>
  29.  * A ClassLoader is used to locate all locations (directories and jar files) in the class path that contain classes
  30.  * within certain packages, and then to load those classes and check them. By default the ClassLoader returned by
  31.  * {@code Thread.currentThread().getContextClassLoader()} is used, but this can be overridden by calling
  32.  * {@link #setClassLoader(ClassLoader)} prior to invoking any of the {@code find()} methods.
  33.  * </p>
  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.  * <p>
  41.  * The standard usage pattern for the ResolverUtil class is as follows:
  42.  * </p>
  43.  *
  44.  * <pre>
  45.  * ResolverUtil&lt;ActionBean&gt; resolver = new ResolverUtil&lt;ActionBean&gt;();
  46.  * resolver.findImplementation(ActionBean.class, pkg1, pkg2);
  47.  * resolver.find(new CustomTest(), pkg1);
  48.  * resolver.find(new CustomTest(), pkg2);
  49.  * Collection&lt;ActionBean&gt; beans = resolver.getClasses();
  50.  * </pre>
  51.  *
  52.  * @author Tim Fennell
  53.  *
  54.  * @param <T>
  55.  *          the generic type
  56.  */
  57. public class ResolverUtil<T> {

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

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

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

  79.     /** The parent. */
  80.     private Class<?> parent;

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

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

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

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

  105.     /** The annotation. */
  106.     private Class<? extends Annotation> annotation;

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

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

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

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

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

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

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

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

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

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

  181.     return this;
  182.   }

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

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

  202.     return this;
  203.   }

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

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

  228.     return this;
  229.   }

  230.   /**
  231.    * Converts a Java package name to a path that can be looked up with a call to
  232.    * {@link ClassLoader#getResources(String)}.
  233.    *
  234.    * @param packageName
  235.    *          The Java package name to convert to a path
  236.    *
  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 resolved classes if and only if
  244.    * it is approved by the Test supplied.
  245.    *
  246.    * @param test
  247.    *          the test used to determine if the class matches
  248.    * @param fqn
  249.    *          the fully qualified name of a class
  250.    */
  251.   @SuppressWarnings("unchecked")
  252.   protected void addIfMatching(Test test, String fqn) {
  253.     try {
  254.       String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
  255.       ClassLoader loader = getClassLoader();
  256.       // if (log.isDebugEnabled()) {
  257.       // log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
  258.       // }

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