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