Coverage Report - org.apache.camel.util.ResolverUtil
 
Classes in this File Line Coverage Branch Coverage Complexity
ResolverUtil
59% 
72% 
0
 
 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.camel.util;
 18  
 
 19  
 import java.io.File;
 20  
 import java.io.FileInputStream;
 21  
 import java.io.IOException;
 22  
 import java.lang.annotation.Annotation;
 23  
 import java.net.URL;
 24  
 import java.net.URLDecoder;
 25  
 import java.util.Enumeration;
 26  
 import java.util.HashSet;
 27  
 import java.util.Set;
 28  
 import java.util.jar.JarEntry;
 29  
 import java.util.jar.JarInputStream;
 30  
 
 31  
 import org.apache.commons.logging.Log;
 32  
 import org.apache.commons.logging.LogFactory;
 33  
 
 34  
 /**
 35  
  * <p>
 36  
  * ResolverUtil is used to locate classes that are available in the/a class path
 37  
  * and meet arbitrary conditions. The two most common conditions are that a
 38  
  * class implements/extends another class, or that is it annotated with a
 39  
  * specific annotation. However, through the use of the {@link Test} class it is
 40  
  * possible to search using arbitrary conditions.
 41  
  * </p>
 42  
  * 
 43  
  * <p>
 44  
  * A ClassLoader is used to locate all locations (directories and jar files) in
 45  
  * the class path that contain classes within certain packages, and then to load
 46  
  * those classes and check them. By default the ClassLoader returned by
 47  
  * {@code Thread.currentThread().getContextClassLoader()} is used, but this can
 48  
  * be overridden by calling {@link #setClassLoader(ClassLoader)} prior to
 49  
  * invoking any of the {@code find()} methods.
 50  
  * </p>
 51  
  * 
 52  
  * <p>
 53  
  * General searches are initiated by calling the
 54  
  * {@link #find(ResolverUtil.Test, String)} ()} method and supplying a package
 55  
  * name and a Test instance. This will cause the named package <b>and all
 56  
  * sub-packages</b> to be scanned for classes that meet the test. There are
 57  
  * also utility methods for the common use cases of scanning multiple packages
 58  
  * for extensions of particular classes, or classes annotated with a specific
 59  
  * annotation.
 60  
  * </p>
 61  
  * 
 62  
  * <p>
 63  
  * The standard usage pattern for the ResolverUtil class is as follows:
 64  
  * </p>
 65  
  * 
 66  
  * <pre>
 67  
  * esolverUtil&lt;ActionBean&gt; resolver = new ResolverUtil&lt;ActionBean&gt;();
 68  
  * esolver.findImplementation(ActionBean.class, pkg1, pkg2);
 69  
  * esolver.find(new CustomTest(), pkg1);
 70  
  * esolver.find(new CustomTest(), pkg2);
 71  
  * ollection&lt;ActionBean&gt; beans = resolver.getClasses();
 72  
  * </pre>
 73  
  * 
 74  
  * @author Tim Fennell
 75  
  */
 76  351
 public class ResolverUtil<T> {
 77  3
     private static final transient Log LOG = LogFactory.getLog(ResolverUtil.class);
 78  
 
 79  
     /**
 80  
      * A simple interface that specifies how to test classes to determine if
 81  
      * they are to be included in the results produced by the ResolverUtil.
 82  
      */
 83  
     public static interface Test {
 84  
         /**
 85  
          * Will be called repeatedly with candidate classes. Must return True if
 86  
          * a class is to be included in the results, false otherwise.
 87  
          */
 88  
         boolean matches(Class type);
 89  
     }
 90  
 
 91  
     /**
 92  
      * A Test that checks to see if each class is assignable to the provided
 93  
      * class. Note that this test will match the parent type itself if it is
 94  
      * presented for matching.
 95  
      */
 96  
     public static class IsA implements Test {
 97  
         private Class parent;
 98  
 
 99  
         /**
 100  
          * Constructs an IsA test using the supplied Class as the parent
 101  
          * class/interface.
 102  
          */
 103  0
         public IsA(Class parentType) {
 104  0
             this.parent = parentType;
 105  0
         }
 106  
 
 107  
         /**
 108  
          * Returns true if type is assignable to the parent type supplied in the
 109  
          * constructor.
 110  
          */
 111  
         public boolean matches(Class type) {
 112  0
             return type != null && parent.isAssignableFrom(type);
 113  
         }
 114  
 
 115  
         @Override
 116  
         public String toString() {
 117  0
             return "is assignable to " + parent.getSimpleName();
 118  
         }
 119  
     }
 120  
 
 121  
     /**
 122  
      * A Test that checks to see if each class is annotated with a specific
 123  
      * annotation. If it is, then the test returns true, otherwise false.
 124  
      */
 125  
     public static class AnnotatedWith implements Test {
 126  
         private Class<? extends Annotation> annotation;
 127  
 
 128  
         /** Construts an AnnotatedWith test for the specified annotation type. */
 129  141
         public AnnotatedWith(Class<? extends Annotation> annotation) {
 130  141
             this.annotation = annotation;
 131  141
         }
 132  
 
 133  
         /**
 134  
          * Returns true if the type is annotated with the class provided to the
 135  
          * constructor.
 136  
          */
 137  
         public boolean matches(Class type) {
 138  1551
             return type != null && type.isAnnotationPresent(annotation);
 139  
         }
 140  
 
 141  
         @Override
 142  
         public String toString() {
 143  1833
             return "annotated with @" + annotation.getSimpleName();
 144  
         }
 145  
     }
 146  
 
 147  
     /** The set of matches being accumulated. */
 148  351
     private Set<Class<? extends T>> matches = new HashSet<Class<? extends T>>();
 149  
 
 150  
     /**
 151  
      * The ClassLoader to use when looking for classes. If null then the
 152  
      * ClassLoader returned by Thread.currentThread().getContextClassLoader()
 153  
      * will be used.
 154  
      */
 155  
     private ClassLoader classloader;
 156  
 
 157  
     /**
 158  
      * Provides access to the classes discovered so far. If no calls have been
 159  
      * made to any of the {@code find()} methods, this set will be empty.
 160  
      * 
 161  
      * @return the set of classes that have been discovered.
 162  
      */
 163  
     public Set<Class<? extends T>> getClasses() {
 164  141
         return matches;
 165  
     }
 166  
 
 167  
     /**
 168  
      * Returns the classloader that will be used for scanning for classes. If no
 169  
      * explicit ClassLoader has been set by the calling, the context class
 170  
      * loader will be used.
 171  
      * 
 172  
      * @return the ClassLoader that will be used to scan for classes
 173  
      */
 174  
     public ClassLoader getClassLoader() {
 175  1692
         return classloader == null ? Thread.currentThread().getContextClassLoader() : classloader;
 176  
     }
 177  
 
 178  
     /**
 179  
      * Sets an explicit ClassLoader that should be used when scanning for
 180  
      * classes. If none is set then the context classloader will be used.
 181  
      * 
 182  
      * @param classloader a ClassLoader to use when scanning for classes
 183  
      */
 184  
     public void setClassLoader(ClassLoader classloader) {
 185  0
         this.classloader = classloader;
 186  0
     }
 187  
 
 188  
     /**
 189  
      * Attempts to discover classes that are assignable to the type provided. In
 190  
      * the case that an interface is provided this method will collect
 191  
      * implementations. In the case of a non-interface class, subclasses will be
 192  
      * collected. Accumulated classes can be accessed by calling
 193  
      * {@link #getClasses()}.
 194  
      * 
 195  
      * @param parent the class of interface to find subclasses or
 196  
      *                implementations of
 197  
      * @param packageNames one or more package names to scan (including
 198  
      *                subpackages) for classes
 199  
      */
 200  
     public void findImplementations(Class parent, String... packageNames) {
 201  0
         if (packageNames == null) {
 202  0
             return;
 203  
         }
 204  
 
 205  0
         Test test = new IsA(parent);
 206  0
         for (String pkg : packageNames) {
 207  0
             find(test, pkg);
 208  
         }
 209  0
     }
 210  
 
 211  
     /**
 212  
      * Attempts to discover classes that are annotated with to the annotation.
 213  
      * Accumulated classes can be accessed by calling {@link #getClasses()}.
 214  
      * 
 215  
      * @param annotation the annotation that should be present on matching
 216  
      *                classes
 217  
      * @param packageNames one or more package names to scan (including
 218  
      *                subpackages) for classes
 219  
      */
 220  
     public void findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
 221  141
         if (packageNames == null) {
 222  0
             return;
 223  
         }
 224  
 
 225  141
         Test test = new AnnotatedWith(annotation);
 226  282
         for (String pkg : packageNames) {
 227  141
             find(test, pkg);
 228  
         }
 229  141
     }
 230  
 
 231  
     /**
 232  
      * Scans for classes starting at the package provided and descending into
 233  
      * subpackages. Each class is offered up to the Test as it is discovered,
 234  
      * and if the Test returns true the class is retained. Accumulated classes
 235  
      * can be fetched by calling {@link #getClasses()}.
 236  
      * 
 237  
      * @param test an instance of {@link Test} that will be used to filter
 238  
      *                classes
 239  
      * @param packageName the name of the package from which to start scanning
 240  
      *                for classes, e.g. {@code net.sourceforge.stripes}
 241  
      */
 242  
     public void find(Test test, String packageName) {
 243  141
         packageName = packageName.replace('.', '/');
 244  141
         ClassLoader loader = getClassLoader();
 245  
         Enumeration<URL> urls;
 246  
 
 247  
         try {
 248  141
             urls = loader.getResources(packageName);
 249  0
         } catch (IOException ioe) {
 250  0
             LOG.warn("Could not read package: " + packageName, ioe);
 251  0
             return;
 252  141
         }
 253  
 
 254  423
         while (urls.hasMoreElements()) {
 255  
             try {
 256  282
                 String urlPath = urls.nextElement().getFile();
 257  282
                 urlPath = URLDecoder.decode(urlPath, "UTF-8");
 258  
 
 259  
                 // If it's a file in a directory, trim the stupid file: spec
 260  282
                 if (urlPath.startsWith("file:")) {
 261  0
                     urlPath = urlPath.substring(5);
 262  
                 }
 263  
 
 264  
                 // Else it's in a JAR, grab the path to the jar
 265  282
                 if (urlPath.indexOf('!') > 0) {
 266  0
                     urlPath = urlPath.substring(0, urlPath.indexOf('!'));
 267  
                 }
 268  
 
 269  282
                 LOG.debug("Scanning for classes in [" + urlPath + "] matching criteria: " + test);
 270  282
                 File file = new File(urlPath);
 271  282
                 if (file.isDirectory()) {
 272  282
                     loadImplementationsInDirectory(test, packageName, file);
 273  282
                 } else {
 274  0
                     loadImplementationsInJar(test, packageName, file);
 275  
                 }
 276  0
             } catch (IOException ioe) {
 277  0
                 LOG.warn("could not read entries", ioe);
 278  282
             }
 279  0
         }
 280  141
     }
 281  
 
 282  
     /**
 283  
      * Finds matches in a physical directory on a filesystem. Examines all files
 284  
      * within a directory - if the File object is not a directory, and ends with
 285  
      * <i>.class</i> the file is loaded and tested to see if it is acceptable
 286  
      * according to the Test. Operates recursively to find classes within a
 287  
      * folder structure matching the package structure.
 288  
      * 
 289  
      * @param test a Test used to filter the classes that are discovered
 290  
      * @param parent the package name up to this directory in the package
 291  
      *                hierarchy. E.g. if /classes is in the classpath and we
 292  
      *                wish to examine files in /classes/org/apache then the
 293  
      *                values of <i>parent</i> would be <i>org/apache</i>
 294  
      * @param location a File object representing a directory
 295  
      */
 296  
     private void loadImplementationsInDirectory(Test test, String parent, File location) {
 297  423
         File[] files = location.listFiles();
 298  423
         StringBuilder builder = null;
 299  
 
 300  2256
         for (File file : files) {
 301  1833
             builder = new StringBuilder(100);
 302  1833
             builder.append(parent).append("/").append(file.getName());
 303  1833
             String packageOrClass = parent == null ? file.getName() : builder.toString();
 304  
 
 305  1833
             if (file.isDirectory()) {
 306  141
                 loadImplementationsInDirectory(test, packageOrClass, file);
 307  141
             } else if (file.getName().endsWith(".class")) {
 308  1551
                 addIfMatching(test, packageOrClass);
 309  
             }
 310  
         }
 311  423
     }
 312  
 
 313  
     /**
 314  
      * Finds matching classes within a jar files that contains a folder
 315  
      * structure matching the package structure. If the File is not a JarFile or
 316  
      * does not exist a warning will be logged, but no error will be raised.
 317  
      * 
 318  
      * @param test a Test used to filter the classes that are discovered
 319  
      * @param parent the parent package under which classes must be in order to
 320  
      *                be considered
 321  
      * @param jarfile the jar file to be examined for classes
 322  
      */
 323  
     private void loadImplementationsInJar(Test test, String parent, File jarfile) {
 324  
 
 325  
         try {
 326  
             JarEntry entry;
 327  0
             JarInputStream jarStream = new JarInputStream(new FileInputStream(jarfile));
 328  
 
 329  0
             while ((entry = jarStream.getNextJarEntry()) != null) {
 330  0
                 String name = entry.getName();
 331  0
                 if (!entry.isDirectory() && name.startsWith(parent) && name.endsWith(".class")) {
 332  0
                     addIfMatching(test, name);
 333  
                 }
 334  0
             }
 335  0
         } catch (IOException ioe) {
 336  0
             LOG.error("Could not search jar file '" + jarfile + "' for classes matching criteria: " + test
 337  
                       + "due to an IOException: " + ioe.getMessage());
 338  0
         }
 339  0
     }
 340  
 
 341  
     /**
 342  
      * Add the class designated by the fully qualified class name provided to
 343  
      * the set of resolved classes if and only if it is approved by the Test
 344  
      * supplied.
 345  
      * 
 346  
      * @param test the test used to determine if the class matches
 347  
      * @param fqn the fully qualified name of a class
 348  
      */
 349  
     protected void addIfMatching(Test test, String fqn) {
 350  
         try {
 351  1551
             String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
 352  1551
             ClassLoader loader = getClassLoader();
 353  1551
             LOG.trace("Checking to see if class " + externalName + " matches criteria [" + test + "]");
 354  
 
 355  1551
             Class type = loader.loadClass(externalName);
 356  1551
             if (test.matches(type)) {
 357  705
                 matches.add((Class<T>)type);
 358  
             }
 359  0
         } catch (Throwable t) {
 360  0
             LOG.warn("Could not examine class '" + fqn + "' due to a " + t.getClass().getName()
 361  
                      + " with message: " + t.getMessage());
 362  1551
         }
 363  1551
     }
 364  
 }