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;
18  
19  import org.apache.logging.log4j.Logger;
20  import org.apache.logging.log4j.core.helpers.Loader;
21  import org.apache.logging.log4j.status.StatusLogger;
22  import org.osgi.framework.FrameworkUtil;
23  import org.osgi.framework.wiring.BundleWiring;
24  
25  import java.io.File;
26  import java.io.FileInputStream;
27  import java.io.FileNotFoundException;
28  import java.io.IOException;
29  import java.lang.annotation.Annotation;
30  import java.net.URI;
31  import java.net.URL;
32  import java.net.URLDecoder;
33  import java.util.Collection;
34  import java.util.Enumeration;
35  import java.util.HashSet;
36  import java.util.Set;
37  import java.util.jar.JarEntry;
38  import java.util.jar.JarInputStream;
39  
40  /**
41   * <p>ResolverUtil is used to locate classes that are available in the/a class path and meet
42   * arbitrary conditions. The two most common conditions are that a class implements/extends
43   * another class, or that is it annotated with a specific annotation. However, through the use
44   * of the {@link Test} class it is possible to search using arbitrary conditions.</p>
45   *
46   * <p>A ClassLoader is used to locate all locations (directories and jar files) in the class
47   * path that contain classes within certain packages, and then to load those classes and
48   * check them. By default the ClassLoader returned by
49   *  {@code Thread.currentThread().getContextClassLoader()} is used, but this can be overridden
50   * by calling {@link #setClassLoader(ClassLoader)} prior to invoking any of the {@code find()}
51   * methods.</p>
52   *
53   * <p>General searches are initiated by calling the
54   * {@link #find(ResolverUtil.Test, String...)} method and supplying
55   * a package name and a Test instance. This will cause the named package <b>and all sub-packages</b>
56   * to be scanned for classes that meet the test. There are also utility methods for the common
57   * use cases of scanning multiple packages for extensions of particular classes, or classes
58   * annotated with a specific annotation.</p>
59   *
60   * <p>The standard usage pattern for the ResolverUtil class is as follows:</p>
61   *
62   *<pre>
63   *ResolverUtil&lt;ActionBean&gt; resolver = new ResolverUtil&lt;ActionBean&gt;();
64   *resolver.findImplementation(ActionBean.class, pkg1, pkg2);
65   *resolver.find(new CustomTest(), pkg1);
66   *resolver.find(new CustomTest(), pkg2);
67   *Collection&lt;ActionBean&gt; beans = resolver.getClasses();
68   *</pre>
69   *
70   * <p>This class was copied from Stripes - http://stripes.mc4j.org/confluence/display/stripes/Home
71   * </p>
72   *
73   * @author Tim Fennell
74   */
75  public class ResolverUtil {
76      /** An instance of Log to use for logging in this class. */
77      private static final Logger LOG = StatusLogger.getLogger();
78  
79      private static final String VFSZIP = "vfszip";
80  
81      private static final String BUNDLE_RESOURCE = "bundleresource";
82  
83      /** The set of matches being accumulated. */
84      private final Set<Class<?>> classMatches = new HashSet<Class<?>>();
85  
86      /** The set of matches being accumulated. */
87      private final Set<URI> resourceMatches = new HashSet<URI>();
88  
89      /**
90       * The ClassLoader to use when looking for classes. If null then the ClassLoader returned
91       * by Thread.currentThread().getContextClassLoader() will be used.
92       */
93      private ClassLoader classloader;
94  
95      /**
96       * Provides access to the classes discovered so far. If no calls have been made to
97       * any of the {@code find()} methods, this set will be empty.
98       *
99       * @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 }