View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements. See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache license, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License. You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the license for the specific language governing permissions and
    15   * limitations under the license.
    16   */
    17  package org.apache.logging.log4j.core.config.plugins.util;
    18
    19  import java.io.File;
    20  import java.io.FileInputStream;
    21  import java.io.FileNotFoundException;
    22  import java.io.IOException;
    23  import java.io.UnsupportedEncodingException;
    24  import java.net.URI;
    25  import java.net.URISyntaxException;
    26  import java.net.URL;
    27  import java.net.URLDecoder;
    28  import java.util.Arrays;
    29  import java.util.Collection;
    30  import java.util.Enumeration;
    31  import java.util.HashSet;
    32  import java.util.List;
    33  import java.util.Set;
    34  import java.util.jar.JarEntry;
    35  import java.util.jar.JarInputStream;
    36
    37  import org.apache.logging.log4j.Logger;
    38  import org.apache.logging.log4j.core.util.Constants;
    39  import org.apache.logging.log4j.core.util.Loader;
    40  import org.apache.logging.log4j.status.StatusLogger;
    41  import org.osgi.framework.FrameworkUtil;
    42  import org.osgi.framework.wiring.BundleWiring;
    43
    44  /**
    45   * <p>
    46   * ResolverUtil is used to locate classes that are available in the/a class path and meet arbitrary conditions. The two
    47   * most common conditions are that a class implements/extends another class, or that is it annotated with a specific
    48   * annotation. However, through the use of the {@link Test} class it is possible to search using arbitrary conditions.
    49   * </p>
    50   *
    51   * <p>
    52   * A ClassLoader is used to locate all locations (directories and jar files) in the class path that contain classes
    53   * within certain packages, and then to load those classes and check them. By default the ClassLoader returned by
    54   * {@code Thread.currentThread().getContextClassLoader()} is used, but this can be overridden by calling
    55   * {@link #setClassLoader(ClassLoader)} prior to invoking any of the {@code find()} methods.
    56   * </p>
    57   *
    58   * <p>
    59   * General searches are initiated by calling the {@link #find(ResolverUtil.Test, String...)} method and supplying a
    60   * package name and a Test instance. This will cause the named package <b>and all sub-packages</b> to be scanned for
    61   * classes that meet the test. There are also utility methods for the common use cases of scanning multiple packages for
    62   * extensions of particular classes, or classes annotated with a specific annotation.
    63   * </p>
    64   *
    65   * <p>
    66   * The standard usage pattern for the ResolverUtil class is as follows:
    67   * </p>
    68   *
    69   * <pre>
    70   * ResolverUtil&lt;ActionBean&gt; resolver = new ResolverUtil&lt;ActionBean&gt;();
    71   * resolver.findImplementation(ActionBean.class, pkg1, pkg2);
    72   * resolver.find(new CustomTest(), pkg1);
    73   * resolver.find(new CustomTest(), pkg2);
    74   * Collection&lt;ActionBean&gt; beans = resolver.getClasses();
    75   * </pre>
    76   *
    77   * <p>
    78   * This class was copied and modified from Stripes - http://stripes.mc4j.org/confluence/display/stripes/Home
    79   * </p>
    80   */
    81  public class ResolverUtil {
    82      /** An instance of Log to use for logging in this class. */
    83      private static final Logger LOGGER = StatusLogger.getLogger();
    84
    85      private static final String VFSZIP = "vfszip";
    86
    87      private static final String BUNDLE_RESOURCE = "bundleresource";
    88
    89      /** The set of matches being accumulated. */
    90      private final Set<Class<?>> classMatches = new HashSet<Class<?>>();
    91
    92      /** The set of matches being accumulated. */
    93      private final Set<URI> resourceMatches = new HashSet<URI>();
    94
    95      /**
    96       * The ClassLoader to use when looking for classes. If null then the ClassLoader returned by
    97       * Thread.currentThread().getContextClassLoader() will be used.
    98       */
    99      private ClassLoader classloader;
    100
    101     /**
    102      * Provides access to the classes discovered so far. If no calls have been made to any of the {@code find()}
    103      * methods, this set will be empty.
    104      *
    105      * @return the set of classes that have been discovered.
    106      */
    107     public Set<Class<?>> getClasses() {
    108         return classMatches;
    109     }
    110
    111     /**
    112      * Returns the matching resources.
    113      *
    114      * @return A Set of URIs that match the criteria.
    115      */
    116     public Set<URI> getResources() {
    117         return resourceMatches;
    118     }
    119
    120     /**
    121      * Returns the classloader that will be used for scanning for classes. If no explicit ClassLoader has been set by
    122      * the calling, the context class loader will be used.
    123      *
    124      * @return the ClassLoader that will be used to scan for classes
    125      */
    126     public ClassLoader getClassLoader() {
    127         return classloader != null ? classloader : (classloader = Loader.getClassLoader(ResolverUtil.class, null));
    128     }
    129
    130     /**
    131      * Sets an explicit ClassLoader that should be used when scanning for classes. If none is set then the context
    132      * classloader will be used.
    133      *
    134      * @param classloader
    135      *        a ClassLoader to use when scanning for classes
    136      */
    137     public void setClassLoader(final ClassLoader classloader) {
    138         this.classloader = classloader;
    139     }
    140
    141     /**
    142      * Attempts to discover classes that pass the test. Accumulated classes can be accessed by calling
    143      * {@link #getClasses()}.
    144      *
    145      * @param test
    146      *        the test to determine matching classes
    147      * @param packageNames
    148      *        one or more package names to scan (including subpackages) for classes
    149      */
    150     public void find(final Test test, final String... packageNames) {
    151         if (packageNames == null) {
    152             return;
    153         }
    154
    155         for (final String pkg : packageNames) {
    156             findInPackage(test, pkg);
    157         }
    158     }
    159
    160     /**
    161      * Scans for classes starting at the package provided and descending into subpackages. Each class is offered up to
    162      * the Test as it is discovered, and if the Test returns true the class is retained. Accumulated classes can be
    163      * fetched by calling {@link #getClasses()}.
    164      *
    165      * @param test
    166      *        an instance of {@link Test} that will be used to filter classes
    167      * @param packageName
    168      *        the name of the package from which to start scanning for classes, e.g. {@code net.sourceforge.stripes}
    169      */
    170     public void findInPackage(final Test test, String packageName) {
    171         packageName = packageName.replace('.', '/');
    172         final ClassLoader loader = getClassLoader();
    173         Enumeration<URL> urls;
    174
    175         try {
    176             urls = loader.getResources(packageName);
    177         } catch (final IOException ioe) {
    178             LOGGER.warn("Could not read package: " + packageName, ioe);
    179             return;
    180         }
    181
    182         while (urls.hasMoreElements()) {
    183             try {
    184                 final URL url = urls.nextElement();
    185                 final String urlPath = extractPath(url);
    186
    187                 LOGGER.info("Scanning for classes in [" + urlPath + "] matching criteria: " + test);
    188                 // Check for a jar in a war in JBoss
    189                 if (VFSZIP.equals(url.getProtocol())) {
    190                     final String path = urlPath.substring(0, urlPath.length() - packageName.length() - 2);
    191                     final URL newURL = new URL(url.getProtocol(), url.getHost(), path);
    192                     @SuppressWarnings("resource")
    193                     final JarInputStream stream = new JarInputStream(newURL.openStream());
    194                     try {
    195                         loadImplementationsInJar(test, packageName, path, stream);
    196                     } finally {
    197                         close(stream, newURL);
    198                     }
    199                 } else if (BUNDLE_RESOURCE.equals(url.getProtocol())) {
    200                     loadImplementationsInBundle(test, packageName);
    201                 } else {
    202                     final File file = new File(urlPath);
    203                     if (file.isDirectory()) {
    204                         loadImplementationsInDirectory(test, packageName, file);
    205                     } else {
    206                         loadImplementationsInJar(test, packageName, file);
    207                     }
    208                 }
    209             } catch (final IOException ioe) {
    210                 LOGGER.warn("could not read entries", ioe);
    211             } catch (URISyntaxException e) {
    212                 LOGGER.warn("could not read entries", e);
    213             }
    214         }
    215     }
    216
    217     String extractPath(final URL url) throws UnsupportedEncodingException, URISyntaxException {
    218         String urlPath = url.getPath(); // same as getFile but without the Query portion
    219         // System.out.println(url.getProtocol() + "->" + urlPath);
    220
    221         // I would be surprised if URL.getPath() ever starts with "jar:" but no harm in checking
    222         if (urlPath.startsWith("jar:")) {
    223             urlPath = urlPath.substring(4);
    224         }
    225         // For jar: URLs, the path part starts with "file:"
    226         if (urlPath.startsWith("file:")) {
    227             urlPath = urlPath.substring(5);
    228         }
    229         // If it was in a JAR, grab the path to the jar
    230         if (urlPath.indexOf('!') > 0) {
    231             urlPath = urlPath.substring(0, urlPath.indexOf('!'));
    232         }
    233
    234         // LOG4J2-445
    235         // Finally, decide whether to URL-decode the file name or not...
    236         final String protocol = url.getProtocol();
    237         final List<String> neverDecode = Arrays.asList(VFSZIP, BUNDLE_RESOURCE);
    238         if (neverDecode.contains(protocol)) {
    239             return urlPath;
    240         }
    241         final String cleanPath = new URI(urlPath).getPath();
    242         if (new File(cleanPath).exists()) {
    243             // if URL-encoded file exists, don't decode it
    244             return cleanPath;
    245         }
    246         return URLDecoder.decode(urlPath, Constants.UTF_8.name());
    247     }
    248
    249     private void loadImplementationsInBundle(final Test test, final String packageName) {
    250         // Do not remove the cast on the next line as removing it will cause a compile error on Java 7.
    251         @SuppressWarnings("RedundantCast")
    252         final BundleWiring wiring = (BundleWiring) FrameworkUtil.getBundle(ResolverUtil.class)
    253                 .adapt(BundleWiring.class);
    254         @SuppressWarnings("unchecked")
    255         final Collection<String> list = (Collection<String>) wiring.listResources(packageName, "*.class",
    256                 BundleWiring.LISTRESOURCES_RECURSE);
    257         for (final String name : list) {
    258             addIfMatching(test, name);
    259         }
    260     }
    261
    262     /**
    263      * Finds matches in a physical directory on a filesystem. Examines all files within a directory - if the File object
    264      * is not a directory, and ends with <i>.class</i> the file is loaded and tested to see if it is acceptable
    265      * according to the Test. Operates recursively to find classes within a folder structure matching the package
    266      * structure.
    267      *
    268      * @param test
    269      *        a Test used to filter the classes that are discovered
    270      * @param parent
    271      *        the package name up to this directory in the package hierarchy. E.g. if /classes is in the classpath and
    272      *        we wish to examine files in /classes/org/apache then the values of <i>parent</i> would be
    273      *        <i>org/apache</i>
    274      * @param location
    275      *        a File object representing a directory
    276      */
    277     private void loadImplementationsInDirectory(final Test test, final String parent, final File location) {
    278         final File[] files = location.listFiles();
    279         if (files == null) {
    280             return;
    281         }
    282
    283         StringBuilder builder;
    284         for (final File file : files) {
    285             builder = new StringBuilder();
    286             builder.append(parent).append('/').append(file.getName());
    287             final String packageOrClass = parent == null ? file.getName() : builder.toString();
    288
    289             if (file.isDirectory()) {
    290                 loadImplementationsInDirectory(test, packageOrClass, file);
    291             } else if (isTestApplicable(test, file.getName())) {
    292                 addIfMatching(test, packageOrClass);
    293             }
    294         }
    295     }
    296
    297     private boolean isTestApplicable(final Test test, final String path) {
    298         return test.doesMatchResource() || path.endsWith(".class") && test.doesMatchClass();
    299     }
    300
    301     /**
    302      * Finds matching classes within a jar files that contains a folder structure matching the package structure. If the
    303      * File is not a JarFile or does not exist a warning will be logged, but no error will be raised.
    304      *
    305      * @param test
    306      *        a Test used to filter the classes that are discovered
    307      * @param parent
    308      *        the parent package under which classes must be in order to be considered
    309      * @param jarFile
    310      *        the jar file to be examined for classes
    311      */
    312     private void loadImplementationsInJar(final Test test, final String parent, final File jarFile) {
    313         @SuppressWarnings("resource")
    314         JarInputStream jarStream = null;
    315         try {
    316             jarStream = new JarInputStream(new FileInputStream(jarFile));
    317             loadImplementationsInJar(test, parent, jarFile.getPath(), jarStream);
    318         } catch (final FileNotFoundException ex) {
    319             LOGGER.error("Could not search jar file '" + jarFile + "' for classes matching criteria: " + test
    320                     + " file not found", ex);
    321         } catch (final IOException ioe) {
    322             LOGGER.error("Could not search jar file '" + jarFile + "' for classes matching criteria: " + test
    323                     + " due to an IOException", ioe);
    324         } finally {
    325             close(jarStream, jarFile);
    326         }
    327     }
    328
    329     /**
    330      * @param jarStream
    331      * @param source
    332      */
    333     private void close(final JarInputStream jarStream, final Object source) {
    334         if (jarStream != null) {
    335             try {
    336                 jarStream.close();
    337             } catch (final IOException e) {
    338                 LOGGER.error("Error closing JAR file stream for {}", source, e);
    339             }
    340         }
    341     }
    342
    343     /**
    344      * Finds matching classes within a jar files that contains a folder structure matching the package structure. If the
    345      * File is not a JarFile or does not exist a warning will be logged, but no error will be raised.
    346      *
    347      * @param test
    348      *        a Test used to filter the classes that are discovered
    349      * @param parent
    350      *        the parent package under which classes must be in order to be considered
    351      * @param stream
    352      *        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             LOGGER.error("Could not search jar file '" + path + "' for classes matching criteria: " + test
    368                     + " 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 resolved classes if and only if
    374      * it is approved by the Test supplied.
    375      *
    376      * @param test
    377      *        the test used to determine if the class matches
    378      * @param fqn
    379      *        the fully qualified name of a class
    380      */
    381     protected void addIfMatching(final Test test, final String fqn) {
    382         try {
    383             final ClassLoader loader = getClassLoader();
    384             if (test.doesMatchClass()) {
    385                 final String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
    386                 if (LOGGER.isDebugEnabled()) {
    387                     LOGGER.debug("Checking to see if class " + externalName + " matches criteria [" + test + ']');
    388                 }
    389
    390                 final Class<?> type = loader.loadClass(externalName);
    391                 if (test.matches(type)) {
    392                     classMatches.add(type);
    393                 }
    394             }
    395             if (test.doesMatchResource()) {
    396                 URL url = loader.getResource(fqn);
    397                 if (url == null) {
    398                     url = loader.getResource(fqn.substring(1));
    399                 }
    400                 if (url != null && test.matches(url.toURI())) {
    401                     resourceMatches.add(url.toURI());
    402                 }
    403             }
    404         } catch (final Throwable t) {
    405             LOGGER.warn("Could not examine class '" + fqn, t);
    406         }
    407     }
    408
    409     /**
    410      * A simple interface that specifies how to test classes to determine if they are to be included in the results
    411      * produced by the ResolverUtil.
    412      */
    413     public interface Test {
    414         /**
    415          * Will be called repeatedly with candidate classes. Must return True if a class is to be included in the
    416          * results, false otherwise.
    417          *
    418          * @param type
    419          *        The Class to match against.
    420          * @return true if the Class matches.
    421          */
    422         boolean matches(Class<?> type);
    423
    424         /**
    425          * Test for a resource.
    426          *
    427          * @param resource
    428          *        The URI to the resource.
    429          * @return true if the resource matches.
    430          */
    431         boolean matches(URI resource);
    432
    433         boolean doesMatchClass();
    434
    435         boolean doesMatchResource();
    436     }
    437
    438 }