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