All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.logging.log4j.core.config.plugins.util.ResolverUtil Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache license, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the license for the specific language governing permissions and
 * limitations under the license.
 */
package org.apache.logging.log4j.core.config.plugins.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.util.Loader;
import org.apache.logging.log4j.status.StatusLogger;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.wiring.BundleWiring;

/**
 * 

* ResolverUtil is used to locate classes that are available in the/a class path and meet arbitrary conditions. The two * most common conditions are that a class implements/extends another class, or that is it annotated with a specific * annotation. However, through the use of the {@link Test} class it is possible to search using arbitrary conditions. *

* *

* A ClassLoader is used to locate all locations (directories and jar files) in the class path that contain classes * within certain packages, and then to load those classes and check them. By default the ClassLoader returned by * {@code Thread.currentThread().getContextClassLoader()} is used, but this can be overridden by calling * {@link #setClassLoader(ClassLoader)} prior to invoking any of the {@code find()} methods. *

* *

* General searches are initiated by calling the {@link #find(ResolverUtil.Test, String...)} method and supplying a * package name and a Test instance. This will cause the named package and all sub-packages to be scanned for * classes that meet the test. There are also utility methods for the common use cases of scanning multiple packages for * extensions of particular classes, or classes annotated with a specific annotation. *

* *

* The standard usage pattern for the ResolverUtil class is as follows: *

* *
 * ResolverUtil<ActionBean> resolver = new ResolverUtil<ActionBean>();
 * resolver.findImplementation(ActionBean.class, pkg1, pkg2);
 * resolver.find(new CustomTest(), pkg1);
 * resolver.find(new CustomTest(), pkg2);
 * Collection<ActionBean> beans = resolver.getClasses();
 * 
* *

* This class was copied and modified from Stripes - http://stripes.mc4j.org/confluence/display/stripes/Home *

*/ public class ResolverUtil { /** An instance of Log to use for logging in this class. */ private static final Logger LOGGER = StatusLogger.getLogger(); private static final String VFSZIP = "vfszip"; private static final String BUNDLE_RESOURCE = "bundleresource"; /** The set of matches being accumulated. */ private final Set> classMatches = new HashSet<>(); /** The set of matches being accumulated. */ private final Set resourceMatches = new HashSet<>(); /** * The ClassLoader to use when looking for classes. If null then the ClassLoader returned by * Thread.currentThread().getContextClassLoader() will be used. */ private ClassLoader classloader; /** * Provides access to the classes discovered so far. If no calls have been made to any of the {@code find()} * methods, this set will be empty. * * @return the set of classes that have been discovered. */ public Set> getClasses() { return classMatches; } /** * Returns the matching resources. * * @return A Set of URIs that match the criteria. */ public Set getResources() { return resourceMatches; } /** * Returns the classloader that will be used for scanning for classes. If no explicit ClassLoader has been set by * the calling, the context class loader will be used. * * @return the ClassLoader that will be used to scan for classes */ public ClassLoader getClassLoader() { return classloader != null ? classloader : (classloader = Loader.getClassLoader(ResolverUtil.class, null)); } /** * Sets an explicit ClassLoader that should be used when scanning for classes. If none is set then the context * classloader will be used. * * @param aClassloader * a ClassLoader to use when scanning for classes */ public void setClassLoader(final ClassLoader aClassloader) { this.classloader = aClassloader; } /** * Attempts to discover classes that pass the test. Accumulated classes can be accessed by calling * {@link #getClasses()}. * * @param test * the test to determine matching classes * @param packageNames * one or more package names to scan (including subpackages) for classes */ public void find(final Test test, final String... packageNames) { if (packageNames == null) { return; } for (final String pkg : packageNames) { findInPackage(test, pkg); } } /** * Scans for classes starting at the package provided and descending into subpackages. Each class is offered up to * the Test as it is discovered, and if the Test returns true the class is retained. Accumulated classes can be * fetched by calling {@link #getClasses()}. * * @param test * an instance of {@link Test} that will be used to filter classes * @param packageName * the name of the package from which to start scanning for classes, e.g. {@code net.sourceforge.stripes} */ public void findInPackage(final Test test, String packageName) { packageName = packageName.replace('.', '/'); final ClassLoader loader = getClassLoader(); Enumeration urls; try { urls = loader.getResources(packageName); } catch (final IOException ioe) { LOGGER.warn("Could not read package: " + packageName, ioe); return; } while (urls.hasMoreElements()) { try { final URL url = urls.nextElement(); final String urlPath = extractPath(url); LOGGER.info("Scanning for classes in [" + urlPath + "] matching criteria: " + test); // Check for a jar in a war in JBoss if (VFSZIP.equals(url.getProtocol())) { final String path = urlPath.substring(0, urlPath.length() - packageName.length() - 2); final URL newURL = new URL(url.getProtocol(), url.getHost(), path); @SuppressWarnings("resource") final JarInputStream stream = new JarInputStream(newURL.openStream()); try { loadImplementationsInJar(test, packageName, path, stream); } finally { close(stream, newURL); } } else if (BUNDLE_RESOURCE.equals(url.getProtocol())) { loadImplementationsInBundle(test, packageName); } else { final File file = new File(urlPath); if (file.isDirectory()) { loadImplementationsInDirectory(test, packageName, file); } else { loadImplementationsInJar(test, packageName, file); } } } catch (final IOException | URISyntaxException ioe) { LOGGER.warn("could not read entries", ioe); } } } String extractPath(final URL url) throws UnsupportedEncodingException, URISyntaxException { String urlPath = url.getPath(); // same as getFile but without the Query portion // System.out.println(url.getProtocol() + "->" + urlPath); // I would be surprised if URL.getPath() ever starts with "jar:" but no harm in checking if (urlPath.startsWith("jar:")) { urlPath = urlPath.substring(4); } // For jar: URLs, the path part starts with "file:" if (urlPath.startsWith("file:")) { urlPath = urlPath.substring(5); } // If it was in a JAR, grab the path to the jar if (urlPath.indexOf('!') > 0) { urlPath = urlPath.substring(0, urlPath.indexOf('!')); } // LOG4J2-445 // Finally, decide whether to URL-decode the file name or not... final String protocol = url.getProtocol(); final List neverDecode = Arrays.asList(VFSZIP, BUNDLE_RESOURCE); if (neverDecode.contains(protocol)) { return urlPath; } final String cleanPath = new URI(urlPath).getPath(); if (new File(cleanPath).exists()) { // if URL-encoded file exists, don't decode it return cleanPath; } return URLDecoder.decode(urlPath, StandardCharsets.UTF_8.name()); } private void loadImplementationsInBundle(final Test test, final String packageName) { final BundleWiring wiring = FrameworkUtil.getBundle(ResolverUtil.class).adapt(BundleWiring.class); final Collection list = wiring.listResources(packageName, "*.class", BundleWiring.LISTRESOURCES_RECURSE); for (final String name : list) { addIfMatching(test, name); } } /** * Finds matches in a physical directory on a filesystem. Examines all files within a directory - if the File object * is not a directory, and ends with .class the file is loaded and tested to see if it is acceptable * according to the Test. Operates recursively to find classes within a folder structure matching the package * structure. * * @param test * a Test used to filter the classes that are discovered * @param parent * the package name up to this directory in the package hierarchy. E.g. if /classes is in the classpath and * we wish to examine files in /classes/org/apache then the values of parent would be * org/apache * @param location * a File object representing a directory */ private void loadImplementationsInDirectory(final Test test, final String parent, final File location) { final File[] files = location.listFiles(); if (files == null) { return; } StringBuilder builder; for (final File file : files) { builder = new StringBuilder(); builder.append(parent).append('/').append(file.getName()); final String packageOrClass = parent == null ? file.getName() : builder.toString(); if (file.isDirectory()) { loadImplementationsInDirectory(test, packageOrClass, file); } else if (isTestApplicable(test, file.getName())) { addIfMatching(test, packageOrClass); } } } private boolean isTestApplicable(final Test test, final String path) { return test.doesMatchResource() || path.endsWith(".class") && test.doesMatchClass(); } /** * Finds matching classes within a jar files that contains a folder structure matching the package structure. If the * File is not a JarFile or does not exist a warning will be logged, but no error will be raised. * * @param test * a Test used to filter the classes that are discovered * @param parent * the parent package under which classes must be in order to be considered * @param jarFile * the jar file to be examined for classes */ private void loadImplementationsInJar(final Test test, final String parent, final File jarFile) { @SuppressWarnings("resource") JarInputStream jarStream = null; try { jarStream = new JarInputStream(new FileInputStream(jarFile)); loadImplementationsInJar(test, parent, jarFile.getPath(), jarStream); } catch (final FileNotFoundException ex) { LOGGER.error("Could not search jar file '" + jarFile + "' for classes matching criteria: " + test + " file not found", ex); } catch (final IOException ioe) { LOGGER.error("Could not search jar file '" + jarFile + "' for classes matching criteria: " + test + " due to an IOException", ioe); } finally { close(jarStream, jarFile); } } /** * @param jarStream * @param source */ private void close(final JarInputStream jarStream, final Object source) { if (jarStream != null) { try { jarStream.close(); } catch (final IOException e) { LOGGER.error("Error closing JAR file stream for {}", source, e); } } } /** * Finds matching classes within a jar files that contains a folder structure matching the package structure. If the * File is not a JarFile or does not exist a warning will be logged, but no error will be raised. * * @param test * a Test used to filter the classes that are discovered * @param parent * the parent package under which classes must be in order to be considered * @param stream * The jar InputStream */ private void loadImplementationsInJar(final Test test, final String parent, final String path, final JarInputStream stream) { try { JarEntry entry; while ((entry = stream.getNextJarEntry()) != null) { final String name = entry.getName(); if (!entry.isDirectory() && name.startsWith(parent) && isTestApplicable(test, name)) { addIfMatching(test, name); } } } catch (final IOException ioe) { LOGGER.error("Could not search jar file '" + path + "' for classes matching criteria: " + test + " due to an IOException", ioe); } } /** * Add the class designated by the fully qualified class name provided to the set of resolved classes if and only if * it is approved by the Test supplied. * * @param test * the test used to determine if the class matches * @param fqn * the fully qualified name of a class */ protected void addIfMatching(final Test test, final String fqn) { try { final ClassLoader loader = getClassLoader(); if (test.doesMatchClass()) { final String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.'); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Checking to see if class " + externalName + " matches criteria [" + test + ']'); } final Class type = loader.loadClass(externalName); if (test.matches(type)) { classMatches.add(type); } } if (test.doesMatchResource()) { URL url = loader.getResource(fqn); if (url == null) { url = loader.getResource(fqn.substring(1)); } if (url != null && test.matches(url.toURI())) { resourceMatches.add(url.toURI()); } } } catch (final Throwable t) { LOGGER.warn("Could not examine class '" + fqn, t); } } /** * A simple interface that specifies how to test classes to determine if they are to be included in the results * produced by the ResolverUtil. */ public interface Test { /** * Will be called repeatedly with candidate classes. Must return True if a class is to be included in the * results, false otherwise. * * @param type * The Class to match against. * @return true if the Class matches. */ boolean matches(Class type); /** * Test for a resource. * * @param resource * The URI to the resource. * @return true if the resource matches. */ boolean matches(URI resource); boolean doesMatchClass(); boolean doesMatchResource(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy