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