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