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