Coverage Report - liquibase.servicelocator.DefaultPackageScanClassResolver
 
Classes in this File Line Coverage Branch Coverage Complexity
DefaultPackageScanClassResolver
48%
88/180
34%
33/96
5.133
 
 1  
 package liquibase.servicelocator;
 2  
 
 3  
 import java.io.File;
 4  
 import java.io.FileInputStream;
 5  
 import java.io.IOException;
 6  
 import java.io.InputStream;
 7  
 import java.net.URI;
 8  
 import java.net.URISyntaxException;
 9  
 import java.net.URL;
 10  
 import java.net.URLConnection;
 11  
 import java.net.URLDecoder;
 12  
 import java.util.Arrays;
 13  
 import java.util.Collections;
 14  
 import java.util.Enumeration;
 15  
 import java.util.HashMap;
 16  
 import java.util.HashSet;
 17  
 import java.util.LinkedHashSet;
 18  
 import java.util.Map;
 19  
 import java.util.Set;
 20  
 import java.util.jar.JarEntry;
 21  
 import java.util.jar.JarInputStream;
 22  
 
 23  
 import liquibase.logging.Logger;
 24  
 import liquibase.logging.core.DefaultLogger;
 25  
 
 26  
 /**
 27  
  * Default implement of {@link PackageScanClassResolver}
 28  
  */
 29  9
 public class DefaultPackageScanClassResolver implements PackageScanClassResolver {
 30  
 
 31  1
     private static Map<String, Set<String>> classesByJarUrl = new HashMap<String, Set<String>>();
 32  
 
 33  9
     protected final transient Logger log = new DefaultLogger();
 34  
     private Set<ClassLoader> classLoaders;
 35  
     private Set<PackageScanFilter> scanFilters;
 36  
 
 37  
     @Override
 38  
     public void addClassLoader(ClassLoader classLoader) {
 39  
         try {
 40  12
             getClassLoaders().add(classLoader);
 41  0
         } catch (UnsupportedOperationException ex) {
 42  
             // Ignore this exception as the PackageScanClassResolver
 43  
             // don't want use any other classloader
 44  12
         }
 45  12
     }
 46  
 
 47  
     @Override
 48  
     public void addFilter(PackageScanFilter filter) {
 49  0
         if (scanFilters == null) {
 50  0
             scanFilters = new LinkedHashSet<PackageScanFilter>();
 51  
         }
 52  0
         scanFilters.add(filter);
 53  0
     }
 54  
 
 55  
     @Override
 56  
     public void removeFilter(PackageScanFilter filter) {
 57  0
         if (scanFilters != null) {
 58  0
             scanFilters.remove(filter);
 59  
         }
 60  0
     }
 61  
 
 62  
     @Override
 63  
     public Set<ClassLoader> getClassLoaders() {
 64  7300
         if (classLoaders == null) {
 65  0
             classLoaders = new HashSet<ClassLoader>();
 66  0
             ClassLoader ccl = Thread.currentThread().getContextClassLoader();
 67  0
             if (ccl != null) {
 68  0
                 log.debug("The thread context class loader: " + ccl + "  is used to load the class");
 69  0
                 classLoaders.add(ccl);
 70  
             }
 71  0
             classLoaders.add(DefaultPackageScanClassResolver.class.getClassLoader());
 72  
         }
 73  7300
         return classLoaders;
 74  
     }
 75  
 
 76  
     @Override
 77  
     public void setClassLoaders(Set<ClassLoader> classLoaders) {
 78  9
         this.classLoaders = classLoaders;
 79  9
     }
 80  
 
 81  
     @Override
 82  
     @SuppressWarnings("unchecked")
 83  
     public Set<Class<?>> findImplementations(Class parent, String... packageNames) {
 84  12
         if (packageNames == null) {
 85  0
             return Collections.EMPTY_SET;
 86  
         }
 87  
 
 88  12
         log.debug("Searching for implementations of " + parent.getName() + " in packages: "
 89  
                 + Arrays.asList(packageNames));
 90  
 
 91  12
         PackageScanFilter test = getCompositeFilter(new AssignableToPackageScanFilter(parent));
 92  12
         Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
 93  132
         for (String pkg : packageNames) {
 94  120
             find(test, pkg, classes);
 95  
         }
 96  
 
 97  12
         log.debug("Found: " + classes);
 98  
 
 99  12
         return classes;
 100  
     }
 101  
 
 102  
     @Override
 103  
     @SuppressWarnings("unchecked")
 104  
     public Set<Class<?>> findByFilter(PackageScanFilter filter, String... packageNames) {
 105  0
         if (packageNames == null) {
 106  0
             return Collections.EMPTY_SET;
 107  
         }
 108  
 
 109  0
         Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
 110  0
         for (String pkg : packageNames) {
 111  0
             find(filter, pkg, classes);
 112  
         }
 113  
 
 114  0
         log.debug("Found: " + classes);
 115  
 
 116  0
         return classes;
 117  
     }
 118  
 
 119  
     protected void find(PackageScanFilter test, String packageName, Set<Class<?>> classes) {
 120  120
         packageName = packageName.replace('.', '/');
 121  
 
 122  120
         Set<ClassLoader> set = getClassLoaders();
 123  
 
 124  120
         for (ClassLoader classLoader : set) {
 125  140
             find(test, packageName, classLoader, classes);
 126  
         }
 127  120
     }
 128  
 
 129  
     protected void find(PackageScanFilter test, String packageName, ClassLoader loader, Set<Class<?>> classes) {
 130  140
         log.debug("Searching for: " + test + " in package: " + packageName + " using classloader: "
 131  
                 + loader.getClass().getName());
 132  
 
 133  
         Enumeration<URL> urls;
 134  
         try {
 135  140
             urls = getResources(loader, packageName);
 136  140
             if (!urls.hasMoreElements()) {
 137  14
                 log.debug("No URLs returned by classloader");
 138  
             }
 139  0
         } catch (IOException ioe) {
 140  0
             log.warning("Cannot read package: " + packageName, ioe);
 141  0
             return;
 142  140
         }
 143  
 
 144  378
         while (urls.hasMoreElements()) {
 145  238
             URL url = null;
 146  
             try {
 147  238
                 url = urls.nextElement();
 148  238
                 log.debug("URL from classloader: " + url);
 149  
 
 150  238
                 url = customResourceLocator(url);
 151  
 
 152  238
                 String urlPath = url.getFile();
 153  238
                 String host = null;
 154  238
                 urlPath = URLDecoder.decode(urlPath, "UTF-8");
 155  
 
 156  238
                 if (url.getProtocol().equals("vfs") && !urlPath.startsWith("vfs")) {
 157  0
                     urlPath = "vfs:" + urlPath;
 158  
                 }
 159  238
                 if (url.getProtocol().equals("vfszip") && !urlPath.startsWith("vfszip")) {
 160  0
                     urlPath = "vfszip:" + urlPath;
 161  
                 }
 162  
 
 163  238
                 log.debug("Decoded urlPath: " + urlPath + " with protocol: " + url.getProtocol());
 164  
 
 165  
                 // If it's a file in a directory, trim the stupid file: spec
 166  238
                 if (urlPath.startsWith("file:")) {
 167  
                     // file path can be temporary folder which uses characters that the URLDecoder decodes wrong
 168  
                     // for example + being decoded to something else (+ can be used in temp folders on Mac OS)
 169  
                     // to remedy this then create new path without using the URLDecoder
 170  
                     try {
 171  0
                         URI uri = new URI(url.getFile());
 172  0
                         host = uri.getHost();
 173  0
                         urlPath = uri.getPath();
 174  0
                     } catch (URISyntaxException e) {
 175  
                         // fallback to use as it was given from the URLDecoder
 176  
                         // this allows us to work on Windows if users have spaces in paths
 177  0
                     }
 178  
 
 179  0
                     if (urlPath.startsWith("file:")) {
 180  0
                         urlPath = urlPath.substring(5);
 181  
                     }
 182  
                 }
 183  
 
 184  
                 // osgi bundles should be skipped
 185  238
                 if (url.toString().startsWith("bundle:") || urlPath.startsWith("bundle:")) {
 186  0
                     log.debug("It's a virtual osgi bundle, skipping");
 187  0
                     continue;
 188  
                 }
 189  
 
 190  
                 // Else it's in a JAR, grab the path to the jar
 191  238
                 if (urlPath.contains(".jar/")) {
 192  0
                     urlPath = urlPath.replace(".jar/", ".jar!/");
 193  
                 }
 194  
 
 195  238
                 if (urlPath.indexOf('!') > 0) {
 196  0
                     urlPath = urlPath.substring(0, urlPath.indexOf('!'));
 197  
                 }
 198  
 
 199  
                 // If a host component was given prepend it to the decoded path.
 200  
                 // This still has its problems as we silently skip user and password
 201  
                 // information etc. but it fixes UNC urls on windows.
 202  238
                 if (host != null) {
 203  0
                     if (urlPath.startsWith("/")) {
 204  0
                         urlPath = "//" + host + urlPath;
 205  
                     } else {
 206  0
                         urlPath = "//" + host + "/" + urlPath;
 207  
                     }
 208  
                 }
 209  
 
 210  238
                 log.debug("Scanning for classes in [" + urlPath + "] matching criteria: " + test);
 211  
 
 212  238
                 File file = new File(urlPath);
 213  238
                 if (file.isDirectory()) {
 214  238
                     log.debug("Loading from directory using file: " + file);
 215  238
                     loadImplementationsInDirectory(test, packageName, file, classes);
 216  
                 } else {
 217  
                     InputStream stream;
 218  0
                     if (urlPath.startsWith("http:") || urlPath.startsWith("https:") || urlPath.startsWith("sonicfs:")
 219  
                             || urlPath.startsWith("vfs:") || urlPath.startsWith("vfszip:")) {
 220  
                         // load resources using http/https
 221  
                         // sonic ESB requires to be loaded using a regular URLConnection
 222  0
                         URL urlStream = new URL(urlPath);
 223  0
                         log.debug("Loading from jar using " + urlStream.getProtocol() + ": " + urlPath);
 224  0
                         URLConnection con = urlStream.openConnection();
 225  
                         // disable cache mainly to avoid jar file locking on Windows
 226  0
                         con.setUseCaches(false);
 227  0
                         stream = con.getInputStream();
 228  0
                     } else {
 229  0
                         log.debug("Loading from jar using file: " + file);
 230  0
                         stream = new FileInputStream(file);
 231  
                     }
 232  
 
 233  0
                     loadImplementationsInJar(test, packageName, stream, urlPath, classes);
 234  
                 }
 235  0
             } catch (IOException e) {
 236  
                 // use debug logging to avoid being to noisy in logs
 237  0
                 log.debug("Cannot read entries in url: " + url, e);
 238  238
             }
 239  238
         }
 240  140
     }
 241  
 
 242  
     // We can override this method to support the custom ResourceLocator
 243  
 
 244  
     protected URL customResourceLocator(URL url) throws IOException {
 245  
         // Do nothing here
 246  238
         return url;
 247  
     }
 248  
 
 249  
     /**
 250  
      * Strategy to get the resources by the given classloader.
 251  
      * <p/>
 252  
      * Notice that in WebSphere platforms there is a {@link WebSpherePackageScanClassResolver} to take care of
 253  
      * WebSphere's odditiy of resource loading.
 254  
      * 
 255  
      * @param loader
 256  
      *            the classloader
 257  
      * @param packageName
 258  
      *            the packagename for the package to load
 259  
      * @return URL's for the given package
 260  
      * @throws IOException
 261  
      *             is thrown by the classloader
 262  
      */
 263  
     protected Enumeration<URL> getResources(ClassLoader loader, String packageName) throws IOException {
 264  140
         log.debug("Getting resource URL for package: " + packageName + " with classloader: " + loader);
 265  
 
 266  
         // If the URL is a jar, the URLClassloader.getResources() seems to require a trailing slash. The
 267  
         // trailing slash is harmless for other URLs
 268  140
         if (!packageName.endsWith("/")) {
 269  140
             packageName = packageName + "/";
 270  
         }
 271  140
         return loader.getResources(packageName);
 272  
     }
 273  
 
 274  
     private PackageScanFilter getCompositeFilter(PackageScanFilter filter) {
 275  12
         if (scanFilters != null) {
 276  0
             CompositePackageScanFilter composite = new CompositePackageScanFilter(scanFilters);
 277  0
             composite.addFilter(filter);
 278  0
             return composite;
 279  
         }
 280  12
         return filter;
 281  
     }
 282  
 
 283  
     /**
 284  
      * Finds matches in a physical directory on a filesystem. Examines all files within a directory - if the File object
 285  
      * is not a directory, and ends with <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 folder structure matching the package
 287  
      * structure.
 288  
      * 
 289  
      * @param test
 290  
      *            a Test used to filter the classes that are discovered
 291  
      * @param parent
 292  
      *            the package name up to this directory in the package hierarchy. E.g. if /classes is in the classpath
 293  
      *            and we wish to examine files in /classes/org/apache then the values of <i>parent</i> would be
 294  
      *            <i>org/apache</i>
 295  
      * @param location
 296  
      *            a File object representing a directory
 297  
      */
 298  
     private void loadImplementationsInDirectory(PackageScanFilter test, String parent, File location,
 299  
             Set<Class<?>> classes) {
 300  728
         File[] files = location.listFiles();
 301  728
         StringBuilder builder = null;
 302  
 
 303  8806
         for (File file : files) {
 304  8078
             builder = new StringBuilder(100);
 305  8078
             String name = file.getName();
 306  8078
             if (name != null) {
 307  8078
                 name = name.trim();
 308  8078
                 builder.append(parent).append("/").append(name);
 309  8078
                 String packageOrClass = parent == null ? name : builder.toString();
 310  
 
 311  8078
                 if (file.isDirectory()) {
 312  490
                     loadImplementationsInDirectory(test, packageOrClass, file, classes);
 313  7588
                 } else if (name.endsWith(".class")) {
 314  7168
                     addIfMatching(test, packageOrClass, classes);
 315  
                 }
 316  
             }
 317  
         }
 318  728
     }
 319  
 
 320  
     /**
 321  
      * Finds matching classes within a jar files that contains a folder structure matching the package structure. If the
 322  
      * File is not a JarFile or does not exist a warning will be logged, but no error will be raised.
 323  
      * 
 324  
      * @param test
 325  
      *            a Test used to filter the classes that are discovered
 326  
      * @param parent
 327  
      *            the parent package under which classes must be in order to be considered
 328  
      * @param stream
 329  
      *            the inputstream of the jar file to be examined for classes
 330  
      * @param urlPath
 331  
      *            the url of the jar file to be examined for classes
 332  
      */
 333  
     protected void loadImplementationsInJar(PackageScanFilter test, String parent, InputStream stream, String urlPath,
 334  
             Set<Class<?>> classes) {
 335  0
         JarInputStream jarStream = null;
 336  
         try {
 337  
 
 338  0
             if (!classesByJarUrl.containsKey(urlPath)) {
 339  0
                 Set<String> names = new HashSet<String>();
 340  
 
 341  0
                 if (stream instanceof JarInputStream) {
 342  0
                     jarStream = (JarInputStream) stream;
 343  
                 } else {
 344  0
                     jarStream = new JarInputStream(stream);
 345  
                 }
 346  
 
 347  
                 JarEntry entry;
 348  0
                 while ((entry = jarStream.getNextJarEntry()) != null) {
 349  0
                     String name = entry.getName();
 350  0
                     if (name != null) {
 351  0
                         name = name.trim();
 352  0
                         if (!entry.isDirectory() && name.endsWith(".class")) {
 353  0
                             names.add(name);
 354  
                         }
 355  
                     }
 356  0
                 }
 357  
 
 358  0
                 classesByJarUrl.put(urlPath, names);
 359  
             }
 360  
 
 361  0
             for (String name : classesByJarUrl.get(urlPath)) {
 362  0
                 if (name.startsWith(parent)) {
 363  0
                     addIfMatching(test, name, classes);
 364  
                 }
 365  
             }
 366  0
         } catch (IOException ioe) {
 367  0
             log.warning("Cannot search jar file '" + urlPath + "' for classes matching criteria: " + test
 368  
                     + " due to an IOException: " + ioe.getMessage(), ioe);
 369  
         } finally {
 370  0
             try {
 371  0
                 if (jarStream != null) {
 372  0
                     jarStream.close();
 373  
                 }
 374  0
             } catch (IOException ignore) {
 375  0
             }
 376  0
         }
 377  0
     }
 378  
 
 379  
     /**
 380  
      * Add the class designated by the fully qualified class name provided to the set of resolved classes if and only if
 381  
      * it is approved by the Test supplied.
 382  
      * 
 383  
      * @param test
 384  
      *            the test used to determine if the class matches
 385  
      * @param fqn
 386  
      *            the fully qualified name of a class
 387  
      */
 388  
     protected void addIfMatching(PackageScanFilter test, String fqn, Set<Class<?>> classes) {
 389  
         try {
 390  7168
             String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
 391  7168
             Set<ClassLoader> set = getClassLoaders();
 392  7168
             boolean found = false;
 393  7168
             for (ClassLoader classLoader : set) {
 394  7168
                 log.debug("Testing that class " + externalName + " matches criteria [" + test + "] using classloader:"
 395  
                         + classLoader);
 396  
                 try {
 397  7168
                     Class<?> type = classLoader.loadClass(externalName);
 398  7168
                     log.debug("Loaded the class: " + type + " in classloader: " + classLoader);
 399  7168
                     if (test.matches(type)) {
 400  392
                         log.debug("Found class: " + type + " which matches the filter in classloader: " + classLoader);
 401  392
                         classes.add(type);
 402  
                     }
 403  7168
                     found = true;
 404  7168
                     break;
 405  0
                 } catch (ClassNotFoundException e) {
 406  0
                     log.debug("Cannot find class '" + fqn + "' in classloader: " + classLoader + ". Reason: " + e, e);
 407  0
                 } catch (NoClassDefFoundError e) {
 408  0
                     log.debug("Cannot find the class definition '" + fqn + "' in classloader: " + classLoader
 409  
                             + ". Reason: " + e, e);
 410  0
                 } catch (Throwable e) {
 411  0
                     log.severe("Cannot load class '" + fqn + "' in classloader: " + classLoader + ".  Reason: " + e, e);
 412  0
                 }
 413  
             }
 414  7168
             if (!found) {
 415  
                 // use debug to avoid being noisy in logs
 416  0
                 log.debug("Cannot find class '" + fqn + "' in any classloaders: " + set);
 417  
             }
 418  0
         } catch (Exception e) {
 419  0
             log.warning(
 420  
                     "Cannot examine class '" + fqn + "' due to a " + e.getClass().getName() + " with message: "
 421  
                             + e.getMessage(), e);
 422  7168
         }
 423  7168
     }
 424  
 
 425  
 }