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  
28  
29  public class DefaultPackageScanClassResolver implements PackageScanClassResolver {
30  
31      private static Map<String, Set<String>> classesByJarUrl = new HashMap<String, Set<String>>();
32  
33      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              getClassLoaders().add(classLoader);
41          } catch (UnsupportedOperationException ex) {
42              
43              
44          }
45      }
46  
47      @Override
48      public void addFilter(PackageScanFilter filter) {
49          if (scanFilters == null) {
50              scanFilters = new LinkedHashSet<PackageScanFilter>();
51          }
52          scanFilters.add(filter);
53      }
54  
55      @Override
56      public void removeFilter(PackageScanFilter filter) {
57          if (scanFilters != null) {
58              scanFilters.remove(filter);
59          }
60      }
61  
62      @Override
63      public Set<ClassLoader> getClassLoaders() {
64          if (classLoaders == null) {
65              classLoaders = new HashSet<ClassLoader>();
66              ClassLoader ccl = Thread.currentThread().getContextClassLoader();
67              if (ccl != null) {
68                  log.debug("The thread context class loader: " + ccl + "  is used to load the class");
69                  classLoaders.add(ccl);
70              }
71              classLoaders.add(DefaultPackageScanClassResolver.class.getClassLoader());
72          }
73          return classLoaders;
74      }
75  
76      @Override
77      public void setClassLoaders(Set<ClassLoader> classLoaders) {
78          this.classLoaders = classLoaders;
79      }
80  
81      @Override
82      @SuppressWarnings("unchecked")
83      public Set<Class<?>> findImplementations(Class parent, String... packageNames) {
84          if (packageNames == null) {
85              return Collections.EMPTY_SET;
86          }
87  
88          log.debug("Searching for implementations of " + parent.getName() + " in packages: "
89                  + Arrays.asList(packageNames));
90  
91          PackageScanFilter test = getCompositeFilter(new AssignableToPackageScanFilter(parent));
92          Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
93          for (String pkg : packageNames) {
94              find(test, pkg, classes);
95          }
96  
97          log.debug("Found: " + classes);
98  
99          return classes;
100     }
101 
102     @Override
103     @SuppressWarnings("unchecked")
104     public Set<Class<?>> findByFilter(PackageScanFilter filter, String... packageNames) {
105         if (packageNames == null) {
106             return Collections.EMPTY_SET;
107         }
108 
109         Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
110         for (String pkg : packageNames) {
111             find(filter, pkg, classes);
112         }
113 
114         log.debug("Found: " + classes);
115 
116         return classes;
117     }
118 
119     protected void find(PackageScanFilter test, String packageName, Set<Class<?>> classes) {
120         packageName = packageName.replace('.', '/');
121 
122         Set<ClassLoader> set = getClassLoaders();
123 
124         for (ClassLoader classLoader : set) {
125             find(test, packageName, classLoader, classes);
126         }
127     }
128 
129     protected void find(PackageScanFilter test, String packageName, ClassLoader loader, Set<Class<?>> classes) {
130         log.debug("Searching for: " + test + " in package: " + packageName + " using classloader: "
131                 + loader.getClass().getName());
132 
133         Enumeration<URL> urls;
134         try {
135             urls = getResources(loader, packageName);
136             if (!urls.hasMoreElements()) {
137                 log.debug("No URLs returned by classloader");
138             }
139         } catch (IOException ioe) {
140             log.warning("Cannot read package: " + packageName, ioe);
141             return;
142         }
143 
144         while (urls.hasMoreElements()) {
145             URL url = null;
146             try {
147                 url = urls.nextElement();
148                 log.debug("URL from classloader: " + url);
149 
150                 url = customResourceLocator(url);
151 
152                 String urlPath = url.getFile();
153                 String host = null;
154                 urlPath = URLDecoder.decode(urlPath, "UTF-8");
155 
156                 if (url.getProtocol().equals("vfs") && !urlPath.startsWith("vfs")) {
157                     urlPath = "vfs:" + urlPath;
158                 }
159                 if (url.getProtocol().equals("vfszip") && !urlPath.startsWith("vfszip")) {
160                     urlPath = "vfszip:" + urlPath;
161                 }
162 
163                 log.debug("Decoded urlPath: " + urlPath + " with protocol: " + url.getProtocol());
164 
165                 
166                 if (urlPath.startsWith("file:")) {
167                     
168                     
169                     
170                     try {
171                         URI uri = new URI(url.getFile());
172                         host = uri.getHost();
173                         urlPath = uri.getPath();
174                     } catch (URISyntaxException e) {
175                         
176                         
177                     }
178 
179                     if (urlPath.startsWith("file:")) {
180                         urlPath = urlPath.substring(5);
181                     }
182                 }
183 
184                 
185                 if (url.toString().startsWith("bundle:") || urlPath.startsWith("bundle:")) {
186                     log.debug("It's a virtual osgi bundle, skipping");
187                     continue;
188                 }
189 
190                 
191                 if (urlPath.contains(".jar/")) {
192                     urlPath = urlPath.replace(".jar/", ".jar!/");
193                 }
194 
195                 if (urlPath.indexOf('!') > 0) {
196                     urlPath = urlPath.substring(0, urlPath.indexOf('!'));
197                 }
198 
199                 
200                 
201                 
202                 if (host != null) {
203                     if (urlPath.startsWith("/")) {
204                         urlPath = "//" + host + urlPath;
205                     } else {
206                         urlPath = "//" + host + "/" + urlPath;
207                     }
208                 }
209 
210                 log.debug("Scanning for classes in [" + urlPath + "] matching criteria: " + test);
211 
212                 File file = new File(urlPath);
213                 if (file.isDirectory()) {
214                     log.debug("Loading from directory using file: " + file);
215                     loadImplementationsInDirectory(test, packageName, file, classes);
216                 } else {
217                     InputStream stream;
218                     if (urlPath.startsWith("http:") || urlPath.startsWith("https:") || urlPath.startsWith("sonicfs:")
219                             || urlPath.startsWith("vfs:") || urlPath.startsWith("vfszip:")) {
220                         
221                         
222                         URL urlStream = new URL(urlPath);
223                         log.debug("Loading from jar using " + urlStream.getProtocol() + ": " + urlPath);
224                         URLConnection con = urlStream.openConnection();
225                         
226                         con.setUseCaches(false);
227                         stream = con.getInputStream();
228                     } else {
229                         log.debug("Loading from jar using file: " + file);
230                         stream = new FileInputStream(file);
231                     }
232 
233                     loadImplementationsInJar(test, packageName, stream, urlPath, classes);
234                 }
235             } catch (IOException e) {
236                 
237                 log.debug("Cannot read entries in url: " + url, e);
238             }
239         }
240     }
241 
242     
243 
244     protected URL customResourceLocator(URL url) throws IOException {
245         
246         return url;
247     }
248 
249     
250 
251 
252 
253 
254 
255 
256 
257 
258 
259 
260 
261 
262 
263     protected Enumeration<URL> getResources(ClassLoader loader, String packageName) throws IOException {
264         log.debug("Getting resource URL for package: " + packageName + " with classloader: " + loader);
265 
266         
267         
268         if (!packageName.endsWith("/")) {
269             packageName = packageName + "/";
270         }
271         return loader.getResources(packageName);
272     }
273 
274     private PackageScanFilter getCompositeFilter(PackageScanFilter filter) {
275         if (scanFilters != null) {
276             CompositePackageScanFilter composite = new CompositePackageScanFilter(scanFilters);
277             composite.addFilter(filter);
278             return composite;
279         }
280         return filter;
281     }
282 
283     
284 
285 
286 
287 
288 
289 
290 
291 
292 
293 
294 
295 
296 
297 
298     private void loadImplementationsInDirectory(PackageScanFilter test, String parent, File location,
299             Set<Class<?>> classes) {
300         File[] files = location.listFiles();
301         StringBuilder builder = null;
302 
303         for (File file : files) {
304             builder = new StringBuilder(100);
305             String name = file.getName();
306             if (name != null) {
307                 name = name.trim();
308                 builder.append(parent).append("/").append(name);
309                 String packageOrClass = parent == null ? name : builder.toString();
310 
311                 if (file.isDirectory()) {
312                     loadImplementationsInDirectory(test, packageOrClass, file, classes);
313                 } else if (name.endsWith(".class")) {
314                     addIfMatching(test, packageOrClass, classes);
315                 }
316             }
317         }
318     }
319 
320     
321 
322 
323 
324 
325 
326 
327 
328 
329 
330 
331 
332 
333     protected void loadImplementationsInJar(PackageScanFilter test, String parent, InputStream stream, String urlPath,
334             Set<Class<?>> classes) {
335         JarInputStream jarStream = null;
336         try {
337 
338             if (!classesByJarUrl.containsKey(urlPath)) {
339                 Set<String> names = new HashSet<String>();
340 
341                 if (stream instanceof JarInputStream) {
342                     jarStream = (JarInputStream) stream;
343                 } else {
344                     jarStream = new JarInputStream(stream);
345                 }
346 
347                 JarEntry entry;
348                 while ((entry = jarStream.getNextJarEntry()) != null) {
349                     String name = entry.getName();
350                     if (name != null) {
351                         name = name.trim();
352                         if (!entry.isDirectory() && name.endsWith(".class")) {
353                             names.add(name);
354                         }
355                     }
356                 }
357 
358                 classesByJarUrl.put(urlPath, names);
359             }
360 
361             for (String name : classesByJarUrl.get(urlPath)) {
362                 if (name.startsWith(parent)) {
363                     addIfMatching(test, name, classes);
364                 }
365             }
366         } catch (IOException ioe) {
367             log.warning("Cannot search jar file '" + urlPath + "' for classes matching criteria: " + test
368                     + " due to an IOException: " + ioe.getMessage(), ioe);
369         } finally {
370             try {
371                 if (jarStream != null) {
372                     jarStream.close();
373                 }
374             } catch (IOException ignore) {
375             }
376         }
377     }
378 
379     
380 
381 
382 
383 
384 
385 
386 
387 
388     protected void addIfMatching(PackageScanFilter test, String fqn, Set<Class<?>> classes) {
389         try {
390             String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
391             Set<ClassLoader> set = getClassLoaders();
392             boolean found = false;
393             for (ClassLoader classLoader : set) {
394                 log.debug("Testing that class " + externalName + " matches criteria [" + test + "] using classloader:"
395                         + classLoader);
396                 try {
397                     Class<?> type = classLoader.loadClass(externalName);
398                     log.debug("Loaded the class: " + type + " in classloader: " + classLoader);
399                     if (test.matches(type)) {
400                         log.debug("Found class: " + type + " which matches the filter in classloader: " + classLoader);
401                         classes.add(type);
402                     }
403                     found = true;
404                     break;
405                 } catch (ClassNotFoundException e) {
406                     log.debug("Cannot find class '" + fqn + "' in classloader: " + classLoader + ". Reason: " + e, e);
407                 } catch (NoClassDefFoundError e) {
408                     log.debug("Cannot find the class definition '" + fqn + "' in classloader: " + classLoader
409                             + ". Reason: " + e, e);
410                 } catch (Throwable e) {
411                     log.severe("Cannot load class '" + fqn + "' in classloader: " + classLoader + ".  Reason: " + e, e);
412                 }
413             }
414             if (!found) {
415                 
416                 log.debug("Cannot find class '" + fqn + "' in any classloaders: " + set);
417             }
418         } catch (Exception e) {
419             log.warning(
420                     "Cannot examine class '" + fqn + "' due to a " + e.getClass().getName() + " with message: "
421                             + e.getMessage(), e);
422         }
423     }
424 
425 }