View Javadoc
1   /**
2    *    Copyright 2009-2019 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  
18  import java.io.IOException;
19  import java.lang.annotation.Annotation;
20  import java.util.HashSet;
21  import java.util.List;
22  import java.util.Set;
23  
24  import org.apache.ibatis.logging.Log;
25  import org.apache.ibatis.logging.LogFactory;
26  
27  /**
28   * <p>ResolverUtil is used to locate classes that are available in the/a class path and meet
29   * arbitrary conditions. The two most common conditions are that a class implements/extends
30   * another class, or that is it annotated with a specific annotation. However, through the use
31   * of the {@link Test} class it is possible to search using arbitrary conditions.</p>
32   *
33   * <p>A ClassLoader is used to locate all locations (directories and jar files) in the class
34   * path that contain classes within certain packages, and then to load those classes and
35   * check them. By default the ClassLoader returned by
36   * {@code Thread.currentThread().getContextClassLoader()} is used, but this can be overridden
37   * by calling {@link #setClassLoader(ClassLoader)} prior to invoking any of the {@code find()}
38   * methods.</p>
39   *
40   * <p>General searches are initiated by calling the
41   * {@link #find(org.apache.ibatis.io.ResolverUtil.Test, String)} ()} method and supplying
42   * a package name and a Test instance. This will cause the named package <b>and all sub-packages</b>
43   * to be scanned for classes that meet the test. There are also utility methods for the common
44   * use cases of scanning multiple packages for extensions of particular classes, or classes
45   * annotated with a specific annotation.</p>
46   *
47   * <p>The standard usage pattern for the ResolverUtil class is as follows:</p>
48   *
49   * <pre>
50   * ResolverUtil&lt;ActionBean&gt; resolver = new ResolverUtil&lt;ActionBean&gt;();
51   * resolver.findImplementation(ActionBean.class, pkg1, pkg2);
52   * resolver.find(new CustomTest(), pkg1);
53   * resolver.find(new CustomTest(), pkg2);
54   * Collection&lt;ActionBean&gt; beans = resolver.getClasses();
55   * </pre>
56   *
57   * @author Tim Fennell
58   */
59  public class ResolverUtil<T> {
60    /*
61     * An instance of Log to use for logging in this class.
62     */
63    private static final Log log = LogFactory.getLog(ResolverUtil.class);
64  
65    /**
66     * A simple interface that specifies how to test classes to determine if they
67     * are to be included in the results produced by the ResolverUtil.
68     */
69    public interface Test {
70      /**
71       * Will be called repeatedly with candidate classes. Must return True if a class
72       * is to be included in the results, false otherwise.
73       */
74      boolean matches(Class<?> type);
75    }
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      private Class<?> parent;
83  
84      /** Constructs an IsA test using the supplied Class as the parent class/interface. */
85      public IsA(Class<?> parentType) {
86        this.parent = parentType;
87      }
88  
89      /** Returns true if type is assignable to the parent type supplied in the constructor. */
90      @Override
91      public boolean matches(Class<?> type) {
92        return type != null && parent.isAssignableFrom(type);
93      }
94  
95      @Override
96      public String toString() {
97        return "is assignable to " + parent.getSimpleName();
98      }
99    }
100 
101   /**
102    * A Test that checks to see if each class is annotated with a specific annotation. If it
103    * is, then the test returns true, otherwise false.
104    */
105   public static class AnnotatedWith implements Test {
106     private Class<? extends Annotation> annotation;
107 
108     /** Constructs an AnnotatedWith test for the specified annotation type. */
109     public AnnotatedWith(Class<? extends Annotation> annotation) {
110       this.annotation = annotation;
111     }
112 
113     /** Returns true if the type is annotated with the class provided to the constructor. */
114     @Override
115     public boolean matches(Class<?> type) {
116       return type != null && type.isAnnotationPresent(annotation);
117     }
118 
119     @Override
120     public String toString() {
121       return "annotated with @" + annotation.getSimpleName();
122     }
123   }
124 
125   /** The set of matches being accumulated. */
126   private Set<Class<? extends T>> matches = new HashSet<>();
127 
128   /**
129    * The ClassLoader to use when looking for classes. If null then the ClassLoader returned
130    * by Thread.currentThread().getContextClassLoader() will be used.
131    */
132   private ClassLoader classloader;
133 
134   /**
135    * Provides access to the classes discovered so far. If no calls have been made to
136    * any of the {@code find()} methods, this set will be empty.
137    *
138    * @return the set of classes that have been discovered.
139    */
140   public Set<Class<? extends T>> getClasses() {
141     return matches;
142   }
143 
144   /**
145    * Returns the classloader that will be used for scanning for classes. If no explicit
146    * ClassLoader has been set by the 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   /**
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   /**
165    * Attempts to discover classes that are assignable to the type provided. In the case
166    * that an interface is provided this method will collect implementations. In the case
167    * of a non-interface class, subclasses will be collected.  Accumulated classes can be
168    * accessed by calling {@link #getClasses()}.
169    *
170    * @param parent the class of interface to find subclasses or implementations of
171    * @param packageNames one or more package names to scan (including subpackages) for classes
172    */
173   public ResolverUtil<T> findImplementations(Class<?> parent, String... packageNames) {
174     if (packageNames == null) {
175       return this;
176     }
177 
178     Test test = new IsA(parent);
179     for (String pkg : packageNames) {
180       find(test, pkg);
181     }
182 
183     return this;
184   }
185 
186   /**
187    * Attempts to discover classes that are annotated with the annotation. Accumulated
188    * classes can be accessed by calling {@link #getClasses()}.
189    *
190    * @param annotation the annotation that should be present on matching classes
191    * @param packageNames one or more package names to scan (including subpackages) for classes
192    */
193   public ResolverUtil<T> findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
194     if (packageNames == null) {
195       return this;
196     }
197 
198     Test test = new AnnotatedWith(annotation);
199     for (String pkg : packageNames) {
200       find(test, pkg);
201     }
202 
203     return this;
204   }
205 
206   /**
207    * Scans for classes starting at the package provided and descending into subpackages.
208    * Each class is offered up to the Test as it is discovered, and if the Test returns
209    * true the class is retained.  Accumulated classes can be fetched by calling
210    * {@link #getClasses()}.
211    *
212    * @param test an instance of {@link Test} that will be used to filter classes
213    * @param packageName the name of the package from which to start scanning for
214    *        classes, e.g. {@code net.sourceforge.stripes}
215    */
216   public ResolverUtil<T> find(Test test, String packageName) {
217     String path = getPackagePath(packageName);
218 
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 
230     return this;
231   }
232 
233   /**
234    * Converts a Java package name to a path that can be looked up with a call to
235    * {@link ClassLoader#getResources(String)}.
236    *
237    * @param packageName The Java package name to convert to a path
238    */
239   protected String getPackagePath(String packageName) {
240     return packageName == null ? null : packageName.replace('.', '/');
241   }
242 
243   /**
244    * Add the class designated by the fully qualified class name provided to the set of
245    * resolved classes if and only if it is approved by the Test supplied.
246    *
247    * @param test the test used to determine if the class matches
248    * @param fqn the fully qualified name of a class
249    */
250   @SuppressWarnings("unchecked")
251   protected void addIfMatching(Test test, String fqn) {
252     try {
253       String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
254       ClassLoader loader = getClassLoader();
255       if (log.isDebugEnabled()) {
256         log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
257       }
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 }