1 /*
2 * Copyright 2010-2026 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
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 /**
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 * <p>
31 * A ClassLoader is used to locate all locations (directories and jar files) in the class path that contain classes
32 * within certain packages, and then to load those classes and check them. By default the ClassLoader returned by
33 * {@code Thread.currentThread().getContextClassLoader()} is used, but this can be overridden by calling
34 * {@link #setClassLoader(ClassLoader)} prior to invoking any of the {@code find()} methods.
35 * </p>
36 * <p>
37 * General searches are initiated by calling the {@link #find(Test, String)} and supplying a package name and a Test
38 * instance. This will cause the named package <b>and all sub-packages</b> to be scanned for classes that meet the test.
39 * There are also utility methods for the common use cases of scanning multiple packages for extensions of particular
40 * classes, or classes annotated with a specific annotation.
41 * </p>
42 * <p>
43 * The standard usage pattern for the ResolverUtil class is as follows:
44 * </p>
45 *
46 * <pre>
47 * ResolverUtil<ActionBean> resolver = new ResolverUtil<ActionBean>();
48 * resolver.findImplementation(ActionBean.class, pkg1, pkg2);
49 * resolver.find(new CustomTest(), pkg1);
50 * resolver.find(new CustomTest(), pkg2);
51 * Collection<ActionBean> beans = resolver.getClasses();
52 * </pre>
53 *
54 * @param <T>
55 * the generic type
56 */
57 public class ResolverUtil<T> {
58
59 /**
60 * A simple interface that specifies how to test classes to determine if they are to be included in the results
61 * produced by the ResolverUtil.
62 */
63 public interface Test {
64
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 /**
78 * A Test that checks to see if each class is assignable to the provided class. Note that this test will match the
79 * parent type itself if it is presented for matching.
80 */
81 public static class IsA implements Test {
82
83 /** The parent. */
84 private Class<?> parent;
85
86 /**
87 * Constructs an IsA test using the supplied Class as the parent class/interface.
88 *
89 * @param parentType
90 * the parent type
91 */
92 public IsA(Class<?> parentType) {
93 this.parent = parentType;
94 }
95
96 /** Returns true if type is assignable to the parent type supplied in the constructor. */
97 @Override
98 public boolean matches(Class<?> type) {
99 return type != null && parent.isAssignableFrom(type);
100 }
101
102 @Override
103 public String toString() {
104 return "is assignable to " + parent.getSimpleName();
105 }
106 }
107
108 /**
109 * A Test that checks to see if each class is annotated with a specific annotation. If it is, then the test returns
110 * true, otherwise false.
111 */
112 public static class AnnotatedWith implements Test {
113
114 /** The annotation. */
115 private Class<? extends Annotation> annotation;
116
117 /**
118 * Constructs an AnnotatedWith test for the specified annotation type.
119 *
120 * @param annotation
121 * the annotation
122 */
123 public AnnotatedWith(Class<? extends Annotation> annotation) {
124 this.annotation = annotation;
125 }
126
127 /** Returns true if the type is annotated with the class provided to the constructor. */
128 @Override
129 public boolean matches(Class<?> type) {
130 return type != null && type.isAnnotationPresent(annotation);
131 }
132
133 @Override
134 public String toString() {
135 return "annotated with @" + annotation.getSimpleName();
136 }
137 }
138
139 /** The set of matches being accumulated. */
140 private Set<Class<? extends T>> matches = new HashSet<>();
141
142 /**
143 * The ClassLoader to use when looking for classes. If null then the ClassLoader returned by
144 * Thread.currentThread().getContextClassLoader() will be used.
145 */
146 private ClassLoader classloader;
147
148 /**
149 * Provides access to the classes discovered so far. If no calls have been made to any of the {@code find()} methods,
150 * this set will be empty.
151 *
152 * @return the set of classes that have been discovered.
153 */
154 public Set<Class<? extends T>> getClasses() {
155 return matches;
156 }
157
158 /**
159 * Returns the classloader that will be used for scanning for classes. If no explicit ClassLoader has been set by the
160 * calling, the context class loader will be used.
161 *
162 * @return the ClassLoader that will be used to scan for classes
163 */
164 public ClassLoader getClassLoader() {
165 return classloader == null ? Thread.currentThread().getContextClassLoader() : classloader;
166 }
167
168 /**
169 * Sets an explicit ClassLoader that should be used when scanning for classes. If none is set then the context
170 * classloader will be used.
171 *
172 * @param classloader
173 * a ClassLoader to use when scanning for classes
174 */
175 public void setClassLoader(ClassLoader classloader) {
176 this.classloader = classloader;
177 }
178
179 /**
180 * Attempts to discover classes that are assignable to the type provided. In the case that an interface is provided
181 * this method will collect implementations. In the case of a non-interface class, subclasses will be collected.
182 * Accumulated classes can be accessed by calling {@link #getClasses()}.
183 *
184 * @param parent
185 * the class of interface to find subclasses or implementations of
186 * @param packageNames
187 * one or more package names to scan (including subpackages) for classes
188 *
189 * @return the resolver util
190 */
191 public ResolverUtil<T> findImplementations(Class<?> parent, String... packageNames) {
192 if (packageNames == null) {
193 return this;
194 }
195
196 Test test = new IsA(parent);
197 for (String pkg : packageNames) {
198 find(test, pkg);
199 }
200
201 return this;
202 }
203
204 /**
205 * Attempts to discover classes that are annotated with the annotation. Accumulated classes can be accessed by calling
206 * {@link #getClasses()}.
207 *
208 * @param annotation
209 * the annotation that should be present on matching classes
210 * @param packageNames
211 * one or more package names to scan (including subpackages) for classes
212 *
213 * @return the resolver util
214 */
215 public ResolverUtil<T> findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
216 if (packageNames == null) {
217 return this;
218 }
219
220 Test test = new AnnotatedWith(annotation);
221 for (String pkg : packageNames) {
222 find(test, pkg);
223 }
224
225 return this;
226 }
227
228 /**
229 * Scans for classes starting at the package provided and descending into subpackages. Each class is offered up to the
230 * Test as it is discovered, and if the Test returns true the class is retained. Accumulated classes can be fetched by
231 * calling {@link #getClasses()}.
232 *
233 * @param test
234 * an instance of {@link Test} that will be used to filter classes
235 * @param packageName
236 * the name of the package from which to start scanning for classes, e.g. {@code net.sourceforge.stripes}
237 *
238 * @return the resolver util
239 */
240 public ResolverUtil<T> find(Test test, String packageName) {
241 String path = getPackagePath(packageName);
242
243 try {
244 List<String> children = VFS.getInstance().list(path);
245 for (String child : children) {
246 if (child.endsWith(".class")) {
247 addIfMatching(test, child);
248 }
249 }
250 } catch (IOException ioe) {
251 // log.error("Could not read package: " + packageName, ioe);
252 }
253
254 return this;
255 }
256
257 /**
258 * Converts a Java package name to a path that can be looked up with a call to
259 * {@link ClassLoader#getResources(String)}.
260 *
261 * @param packageName
262 * The Java package name to convert to a path
263 *
264 * @return the package path
265 */
266 protected String getPackagePath(String packageName) {
267 return packageName == null ? null : packageName.replace('.', '/');
268 }
269
270 /**
271 * Add the class designated by the fully qualified class name provided to the set of resolved classes if and only if
272 * it is approved by the Test supplied.
273 *
274 * @param test
275 * the test used to determine if the class matches
276 * @param fqn
277 * the fully qualified name of a class
278 */
279 @SuppressWarnings("unchecked")
280 protected void addIfMatching(Test test, String fqn) {
281 try {
282 String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
283 ClassLoader loader = getClassLoader();
284 // if (log.isDebugEnabled()) {
285 // log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
286 // }
287
288 Class<?> type = loader.loadClass(externalName);
289 if (test.matches(type)) {
290 matches.add((Class<T>) type);
291 }
292 } catch (Throwable t) {
293 // log.warn("Could not examine class '" + fqn + "'" + " due to a "
294 // + t.getClass().getName() + " with message: " + t.getMessage());
295 }
296 }
297 }