001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.camel.util;
018
019 import java.io.File;
020 import java.io.FileInputStream;
021 import java.io.IOException;
022 import java.lang.annotation.Annotation;
023 import java.lang.reflect.Method;
024 import java.net.URL;
025 import java.net.URLDecoder;
026 import java.util.Arrays;
027 import java.util.Enumeration;
028 import java.util.HashSet;
029 import java.util.Set;
030 import java.util.jar.JarEntry;
031 import java.util.jar.JarInputStream;
032
033 import org.apache.commons.logging.Log;
034 import org.apache.commons.logging.LogFactory;
035
036 /**
037 * <p>
038 * ResolverUtil is used to locate classes that are available in the/a class path
039 * and meet arbitrary conditions. The two most common conditions are that a
040 * class implements/extends another class, or that is it annotated with a
041 * specific annotation. However, through the use of the {@link Test} class it is
042 * possible to search using arbitrary conditions.
043 * </p>
044 * <p/>
045 * <p>
046 * A ClassLoader is used to locate all locations (directories and jar files) in
047 * the class path that contain classes within certain packages, and then to load
048 * those classes and check them. By default the ClassLoader returned by
049 * {@code Thread.currentThread().getContextClassLoader()} is used, but this can
050 * be overridden by calling {@link #setClassLoaders(Set)} prior to
051 * invoking any of the {@code find()} methods.
052 * </p>
053 * <p/>
054 * <p>
055 * General searches are initiated by calling the
056 * {@link #find(ResolverUtil.Test, String)} ()} method and supplying a package
057 * name and a Test instance. This will cause the named package <b>and all
058 * sub-packages</b> to be scanned for classes that meet the test. There are
059 * also utility methods for the common use cases of scanning multiple packages
060 * for extensions of particular classes, or classes annotated with a specific
061 * annotation.
062 * </p>
063 * <p/>
064 * <p>
065 * The standard usage pattern for the ResolverUtil class is as follows:
066 * </p>
067 * <p/>
068 * <pre>
069 * esolverUtil<ActionBean> resolver = new ResolverUtil<ActionBean>();
070 * esolver.findImplementation(ActionBean.class, pkg1, pkg2);
071 * esolver.find(new CustomTest(), pkg1);
072 * esolver.find(new CustomTest(), pkg2);
073 * ollection<ActionBean> beans = resolver.getClasses();
074 * </pre>
075 *
076 * @author Tim Fennell
077 */
078 public class ResolverUtil<T> {
079 protected static final transient Log LOG = LogFactory.getLog(ResolverUtil.class);
080
081 /**
082 * A simple interface that specifies how to test classes to determine if
083 * they are to be included in the results produced by the ResolverUtil.
084 */
085 public static interface Test {
086 /**
087 * Will be called repeatedly with candidate classes. Must return True if
088 * a class is to be included in the results, false otherwise.
089 */
090 boolean matches(Class type);
091 }
092
093 /**
094 * A Test that checks to see if each class is assignable to the provided
095 * class. Note that this test will match the parent type itself if it is
096 * presented for matching.
097 */
098 public static class IsA implements Test {
099 private Class parent;
100
101 /**
102 * Constructs an IsA test using the supplied Class as the parent
103 * class/interface.
104 */
105 public IsA(Class parentType) {
106 this.parent = parentType;
107 }
108
109 /**
110 * Returns true if type is assignable to the parent type supplied in the
111 * constructor.
112 */
113 public boolean matches(Class type) {
114 return type != null && parent.isAssignableFrom(type);
115 }
116
117 @Override
118 public String toString() {
119 return "is assignable to " + parent.getSimpleName();
120 }
121 }
122
123 /**
124 * A Test that checks to see if each class is annotated with a specific
125 * annotation. If it is, then the test returns true, otherwise false.
126 */
127 public static class AnnotatedWith implements Test {
128 private Class<? extends Annotation> annotation;
129
130 /**
131 * Constructs an AnnotatedWith test for the specified annotation type.
132 */
133 public AnnotatedWith(Class<? extends Annotation> annotation) {
134 this.annotation = annotation;
135 }
136
137 /**
138 * Returns true if the type is annotated with the class provided to the
139 * constructor.
140 */
141 public boolean matches(Class type) {
142 return type != null && type.isAnnotationPresent(annotation);
143 }
144
145 @Override
146 public String toString() {
147 return "annotated with @" + annotation.getSimpleName();
148 }
149 }
150
151 /**
152 * The set of matches being accumulated.
153 */
154 private Set<Class<? extends T>> matches = new HashSet<Class<? extends T>>();
155
156 /**
157 * The ClassLoader to use when looking for classes. If null then the
158 * ClassLoader returned by Thread.currentThread().getContextClassLoader()
159 * will be used.
160 */
161 private Set<ClassLoader> classLoaders;
162
163 /**
164 * Provides access to the classes discovered so far. If no calls have been
165 * made to any of the {@code find()} methods, this set will be empty.
166 *
167 * @return the set of classes that have been discovered.
168 */
169 public Set<Class<? extends T>> getClasses() {
170 return matches;
171 }
172
173
174 /**
175 * Returns the classloaders that will be used for scanning for classes. If no
176 * explicit ClassLoader has been set by the calling, the context class
177 * loader will and the one that has loaded this class ResolverUtil be used.
178 *
179 * @return the ClassLoader instances that will be used to scan for classes
180 */
181 public Set<ClassLoader> getClassLoaders() {
182 if (classLoaders == null) {
183 classLoaders = new HashSet<ClassLoader>();
184 classLoaders.add(Thread.currentThread().getContextClassLoader());
185 classLoaders.add(ResolverUtil.class.getClassLoader());
186 }
187 return classLoaders;
188 }
189
190 /**
191 * Sets the ClassLoader instances that should be used when scanning for
192 * classes. If none is set then the context classloader will be used.
193 *
194 * @param classLoaders a ClassLoader to use when scanning for classes
195 */
196 public void setClassLoaders(Set<ClassLoader> classLoaders) {
197 this.classLoaders = classLoaders;
198 }
199
200 /**
201 * Attempts to discover classes that are assignable to the type provided. In
202 * the case that an interface is provided this method will collect
203 * implementations. In the case of a non-interface class, subclasses will be
204 * collected. Accumulated classes can be accessed by calling
205 * {@link #getClasses()}.
206 *
207 * @param parent the class of interface to find subclasses or
208 * implementations of
209 * @param packageNames one or more package names to scan (including
210 * subpackages) for classes
211 */
212 public void findImplementations(Class parent, String... packageNames) {
213 if (packageNames == null) {
214 return;
215 }
216
217 if (LOG.isDebugEnabled()) {
218 LOG.debug("Searching for implementations of " + parent.getName() + " in packages: " + Arrays
219 .asList(packageNames));
220 }
221
222 Test test = new IsA(parent);
223 for (String pkg : packageNames) {
224 find(test, pkg);
225 }
226
227 if (LOG.isDebugEnabled()) {
228 LOG.debug("Found: " + getClasses());
229 }
230 }
231
232 /**
233 * Attempts to discover classes that are annotated with to the annotation.
234 * Accumulated classes can be accessed by calling {@link #getClasses()}.
235 *
236 * @param annotation the annotation that should be present on matching
237 * classes
238 * @param packageNames one or more package names to scan (including
239 * subpackages) for classes
240 */
241 public void findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
242 if (packageNames == null) {
243 return;
244 }
245
246 if (LOG.isDebugEnabled()) {
247 LOG.debug("Searching for annotations of " + annotation.getName() + " in packages: " + Arrays
248 .asList(packageNames));
249 }
250
251 Test test = new AnnotatedWith(annotation);
252 for (String pkg : packageNames) {
253 find(test, pkg);
254 }
255
256 if (LOG.isDebugEnabled()) {
257 LOG.debug("Found: " + getClasses());
258 }
259 }
260
261 /**
262 * Scans for classes starting at the package provided and descending into
263 * subpackages. Each class is offered up to the Test as it is discovered,
264 * and if the Test returns true the class is retained. Accumulated classes
265 * can be fetched by calling {@link #getClasses()}.
266 *
267 * @param test an instance of {@link Test} that will be used to filter
268 * classes
269 * @param packageName the name of the package from which to start scanning
270 * for classes, e.g. {@code net.sourceforge.stripes}
271 */
272 public void find(Test test, String packageName) {
273 packageName = packageName.replace('.', '/');
274
275 Set<ClassLoader> set = getClassLoaders();
276 for (ClassLoader classLoader : set) {
277 find(test, packageName, classLoader);
278 }
279 }
280
281 protected void find(Test test, String packageName, ClassLoader loader) {
282 if (LOG.isTraceEnabled()) {
283 LOG.trace("Searching for: " + test + " in package: " + packageName + " using classloader: "
284 + loader.getClass().getName());
285 }
286 if (loader.getClass().getName().endsWith(
287 "org.apache.felix.framework.searchpolicy.ContentClassLoader")) {
288 LOG.trace("This is not an URL classloader, skipping");
289 //this classloader is in OSGI env which is not URLClassloader, we should resort to the
290 //BundleDelegatingClassLoader in OSGI, so just return
291 return;
292 }
293 try {
294 Method mth = loader.getClass().getMethod("getBundle", new Class[] {});
295 if (mth != null) {
296 // it's osgi bundle class loader, so we need to load implementation in bundles
297 if (LOG.isDebugEnabled()) {
298 LOG.debug("Loading from osgi buindle using classloader: " + loader);
299 }
300 loadImplementationsInBundle(test, packageName, loader, mth);
301 return;
302 }
303 } catch (NoSuchMethodException e) {
304 LOG.trace("It's not an osgi bundle classloader");
305 }
306
307 Enumeration<URL> urls;
308 try {
309 urls = getResources(loader, packageName);
310 if (!urls.hasMoreElements()) {
311 LOG.trace("No URLs returned by classloader");
312 }
313 } catch (IOException ioe) {
314 LOG.warn("Could not read package: " + packageName, ioe);
315 return;
316 }
317
318 while (urls.hasMoreElements()) {
319 URL url = null;
320 try {
321 url = urls.nextElement();
322 if (LOG.isTraceEnabled()) {
323 LOG.trace("URL from classloader: " + url);
324 }
325
326 String urlPath = url.getFile();
327 urlPath = URLDecoder.decode(urlPath, "UTF-8");
328 if (LOG.isTraceEnabled()) {
329 LOG.trace("Decoded urlPath: " + urlPath);
330 }
331
332 // If it's a file in a directory, trim the stupid file: spec
333 if (urlPath.startsWith("file:")) {
334 urlPath = urlPath.substring(5);
335 }
336
337 // osgi bundles should be skipped
338 if (urlPath.startsWith("bundle:")) {
339 LOG.trace("It's a virtual osgi bundle, skipping");
340 continue;
341 }
342
343 // Else it's in a JAR, grab the path to the jar
344 if (urlPath.indexOf('!') > 0) {
345 urlPath = urlPath.substring(0, urlPath.indexOf('!'));
346 }
347
348 if (LOG.isTraceEnabled()) {
349 LOG.trace("Scanning for classes in [" + urlPath + "] matching criteria: " + test);
350 }
351
352 File file = new File(urlPath);
353 if (file.isDirectory()) {
354 if (LOG.isDebugEnabled()) {
355 LOG.debug("Loading from directory: " + file);
356 }
357 loadImplementationsInDirectory(test, packageName, file);
358 } else {
359 if (LOG.isDebugEnabled()) {
360 LOG.debug("Loading from jar: " + file);
361 }
362 loadImplementationsInJar(test, packageName, file);
363 }
364 } catch (IOException ioe) {
365 LOG.warn("Could not read entries in url: " + url, ioe);
366 }
367 }
368 }
369
370 /**
371 * Strategy to get the resources by the given classloader.
372 * <p/>
373 * Notice that in WebSphere platforms there is a {@link org.apache.camel.util.WebSphereResolverUtil}
374 * to take care of WebSphere's odditiy of resource loading.
375 *
376 * @param loader the classloader
377 * @param packageName the packagename for the package to load
378 * @return URL's for the given package
379 * @throws IOException is thrown by the classloader
380 */
381 protected Enumeration<URL> getResources(ClassLoader loader, String packageName) throws IOException {
382 if (LOG.isTraceEnabled()) {
383 LOG.trace("Getting resource URL for package: " + packageName + " with classloader: " + loader);
384 }
385 return loader.getResources(packageName);
386 }
387
388 private void loadImplementationsInBundle(Test test, String packageName, ClassLoader loader, Method mth) {
389 // Use an inner class to avoid a NoClassDefFoundError when used in a non-osgi env
390 Set<String> urls = OsgiUtil.getImplementationsInBundle(test, packageName, loader, mth);
391 if (urls != null) {
392 for (String url : urls) {
393 // substring to avoid leading slashes
394 addIfMatching(test, url);
395 }
396 }
397 }
398
399 private static final class OsgiUtil {
400 private OsgiUtil() {
401 // Helper class
402 }
403 static Set<String> getImplementationsInBundle(Test test, String packageName, ClassLoader loader, Method mth) {
404 try {
405 org.osgi.framework.Bundle bundle = (org.osgi.framework.Bundle) mth.invoke(loader);
406 org.osgi.framework.Bundle[] bundles = bundle.getBundleContext().getBundles();
407 Set<String> urls = new HashSet<String>();
408 for (org.osgi.framework.Bundle bd : bundles) {
409 if (LOG.isTraceEnabled()) {
410 LOG.trace("Searching in bundle:" + bd);
411 }
412 Enumeration<URL> paths = bd.findEntries("/" + packageName, "*.class", true);
413 while (paths != null && paths.hasMoreElements()) {
414 URL path = paths.nextElement();
415 urls.add(path.getPath().substring(1));
416 }
417 }
418 return urls;
419 } catch (Throwable t) {
420 LOG.error("Could not search osgi bundles for classes matching criteria: " + test
421 + "due to an Exception: " + t.getMessage());
422 return null;
423 }
424 }
425 }
426
427
428 /**
429 * Finds matches in a physical directory on a filesystem. Examines all files
430 * within a directory - if the File object is not a directory, and ends with
431 * <i>.class</i> the file is loaded and tested to see if it is acceptable
432 * according to the Test. Operates recursively to find classes within a
433 * folder structure matching the package structure.
434 *
435 * @param test a Test used to filter the classes that are discovered
436 * @param parent the package name up to this directory in the package
437 * hierarchy. E.g. if /classes is in the classpath and we wish to
438 * examine files in /classes/org/apache then the values of
439 * <i>parent</i> would be <i>org/apache</i>
440 * @param location a File object representing a directory
441 */
442 private void loadImplementationsInDirectory(Test test, String parent, File location) {
443 File[] files = location.listFiles();
444 StringBuilder builder = null;
445
446 for (File file : files) {
447 builder = new StringBuilder(100);
448 String name = file.getName();
449 if (name != null) {
450 name = name.trim();
451 builder.append(parent).append("/").append(name);
452 String packageOrClass = parent == null ? name : builder.toString();
453
454 if (file.isDirectory()) {
455 loadImplementationsInDirectory(test, packageOrClass, file);
456 } else if (name.endsWith(".class")) {
457 addIfMatching(test, packageOrClass);
458 }
459 }
460 }
461 }
462
463 /**
464 * Finds matching classes within a jar files that contains a folder
465 * structure matching the package structure. If the File is not a JarFile or
466 * does not exist a warning will be logged, but no error will be raised.
467 *
468 * @param test a Test used to filter the classes that are discovered
469 * @param parent the parent package under which classes must be in order to
470 * be considered
471 * @param jarfile the jar file to be examined for classes
472 */
473 private void loadImplementationsInJar(Test test, String parent, File jarfile) {
474 JarInputStream jarStream = null;
475 try {
476 jarStream = new JarInputStream(new FileInputStream(jarfile));
477
478 JarEntry entry;
479 while ((entry = jarStream.getNextJarEntry()) != null) {
480 String name = entry.getName();
481 if (name != null) {
482 name = name.trim();
483 if (!entry.isDirectory() && name.startsWith(parent) && name.endsWith(".class")) {
484 addIfMatching(test, name);
485 }
486 }
487 }
488 } catch (IOException ioe) {
489 LOG.error("Could not search jar file '" + jarfile + "' for classes matching criteria: " + test
490 + " due to an IOException: " + ioe.getMessage(), ioe);
491 } finally {
492 ObjectHelper.close(jarStream, jarfile.getPath(), LOG);
493 }
494 }
495
496 /**
497 * Add the class designated by the fully qualified class name provided to
498 * the set of resolved classes if and only if it is approved by the Test
499 * supplied.
500 *
501 * @param test the test used to determine if the class matches
502 * @param fqn the fully qualified name of a class
503 */
504 protected void addIfMatching(Test test, String fqn) {
505 try {
506 String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
507 Set<ClassLoader> set = getClassLoaders();
508 boolean found = false;
509 for (ClassLoader classLoader : set) {
510 if (LOG.isTraceEnabled()) {
511 LOG.trace("Testing for class " + externalName + " matches criteria [" + test + "]");
512 }
513 try {
514 Class type = classLoader.loadClass(externalName);
515 if (test.matches(type)) {
516 if (LOG.isTraceEnabled()) {
517 LOG.trace("Found class: " + type + " in classloader: " + classLoader);
518 }
519 matches.add((Class<T>)type);
520 }
521 found = true;
522 break;
523 } catch (ClassNotFoundException e) {
524 LOG.debug("Could not find class '" + fqn + "' in classloader: " + classLoader
525 + ". Reason: " + e, e);
526 } catch (NoClassDefFoundError e) {
527 LOG.debug("Could not find the class defintion '" + fqn + "' in classloader: " + classLoader
528 + ". Reason: " + e, e);
529 }
530 }
531 if (!found) {
532 LOG.warn("Could not find class '" + fqn + "' in any classloaders: " + set);
533 }
534 } catch (Throwable t) {
535 LOG.warn("Could not examine class '" + fqn + "' due to a " + t.getClass().getName()
536 + " with message: " + t.getMessage(), t);
537 }
538 }
539
540 }