View Javadoc

1   /*
2    * $Id: ClasspathScanner.java 22312 2011-07-04 07:28:06Z dirk.olmes $
3    * --------------------------------------------------------------------------------------
4    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
5    *
6    * The software in this package is published under the terms of the CPAL v1.0
7    * license, a copy of which has been included with this distribution in the
8    * LICENSE.txt file.
9    */
10  package org.mule.util.scan;
11  
12  import org.mule.config.ExceptionHelper;
13  import org.mule.util.ClassUtils;
14  import org.mule.util.FileUtils;
15  import org.mule.util.scan.annotations.AnnotationFilter;
16  import org.mule.util.scan.annotations.AnnotationTypeFilter;
17  import org.mule.util.scan.annotations.AnnotationsScanner;
18  import org.mule.util.scan.annotations.ClosableClassReader;
19  import org.mule.util.scan.annotations.MetaAnnotationTypeFilter;
20  
21  import java.io.File;
22  import java.io.FileInputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.lang.annotation.Annotation;
26  import java.lang.annotation.ElementType;
27  import java.lang.annotation.Target;
28  import java.lang.reflect.Modifier;
29  import java.net.URL;
30  import java.util.Collection;
31  import java.util.Enumeration;
32  import java.util.HashSet;
33  import java.util.Set;
34  import java.util.jar.JarEntry;
35  import java.util.jar.JarFile;
36  
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  import org.objectweb.asm.ClassReader;
40  
41  /**
42   * This class can be used to scan the classpath for classtypes (or interfaces they
43   * implement) or for annotations on the classpath. The type of scanner used depends
44   * on the class type passed in. There are currently 3 types of scanner;
45   * <ul>
46   * <li>{@link InterfaceClassScanner} - will search for all class that are assignable
47   * to the interface provided</li>
48   * <li>{@link ImplementationClassScanner} - will search for all classes that extend a
49   * base type</li>
50   * <li>{@link AnnotationsScanner} - will search for classes with specific
51   * annotations, this can also seach for meta annotations</li>
52   * </ul>
53   * This scanner uses ASM to search class byte code rather than the classes themselves
54   * making orders of magnitude better performance and uses a lot less memory. ASM
55   * seems to be the fasted of the byte code manipulation libraries i.e. JavaAssist or
56   * BCEL Note that the scanner will not scan inner or anonymous classes.
57   */
58  public class ClasspathScanner
59  {
60      public static final int INCLUDE_ABSTRACT = 0x01;
61      public static final int INCLUDE_INTERFACE = 0x02;
62      public static final int INCLUDE_INNER = 0x04;
63      public static final int INCLUDE_ANONYMOUS = 0x08;
64  
65      public static final int DEFAULT_FLAGS = 0x0;
66      
67      /**
68       * logger used by this class
69       */
70      protected transient final Log logger = LogFactory.getLog(ClasspathScanner.class);
71  
72      private ClassLoader classLoader;
73      
74      private String[] basepaths;
75  
76      public ClasspathScanner(String... basepaths)
77      {
78          this.classLoader = Thread.currentThread().getContextClassLoader();
79          this.basepaths = basepaths;
80      }
81  
82      public ClasspathScanner(ClassLoader classLoader, String... basepaths)
83      {
84          this.classLoader = classLoader;
85          this.basepaths = basepaths;
86      }
87  
88      public <T> Set<Class<T>> scanFor(Class<T> clazz) throws IOException
89      {
90          return scanFor(clazz, DEFAULT_FLAGS);
91      }
92  
93      public <T> Set<Class<T>> scanFor(Class<T> clazz, int flags) throws IOException
94      {
95          Set<Class<T>> classes = new HashSet<Class<T>>();
96  
97          for (int i = 0; i < basepaths.length; i++)
98          {
99              String basepath = basepaths[i];
100 
101             Enumeration<URL> urls = classLoader.getResources(basepath.trim());
102             while (urls.hasMoreElements())
103             {
104                 URL url = urls.nextElement();
105                 if (url.getProtocol().equalsIgnoreCase("file"))
106                 {
107                     classes.addAll(processFileUrl(url, basepath, clazz, flags));
108                 }
109                 else if (url.getProtocol().equalsIgnoreCase("jar"))
110                 {
111                     classes.addAll(processJarUrl(url, basepath, clazz, flags));
112                 }
113                 else if (url.getProtocol().equalsIgnoreCase("bundleresource"))
114                 {
115                     logger.debug("Classpath contains an OSGi bundle resource which Mule does not know how to access, therefore this resource will be ignored: + " + url.toString());
116                 }
117                 else
118                 {
119                     throw new IllegalArgumentException("Do not understand how to handle protocol: " + url.getProtocol());
120                 }
121             }
122         }
123         return classes;
124     }
125 
126     protected <T> Set<Class<T>> processJarUrl(URL url, String basepath, Class<T> clazz, int flags) throws IOException
127     {
128         Set<Class<T>> set = new HashSet<Class<T>>();
129         String path = url.getFile().substring(5, url.getFile().indexOf("!"));
130         // We can't URLDecoder.decode(path) since some encoded chars are allowed on file uris
131         path = path.replaceAll("%20", " ");
132         JarFile jar = new JarFile(path);
133 
134         for (Enumeration<JarEntry> entries = jar.entries(); entries.hasMoreElements();)
135         {
136             JarEntry entry = entries.nextElement();
137             if (entry.getName().startsWith(basepath) && entry.getName().endsWith(".class"))
138             {
139                 try
140                 {
141                     String name = entry.getName();
142                     // Ignore anonymous and inner classes
143                     if (name.contains("$") && !hasFlag(flags, INCLUDE_INNER))
144                     {
145                         continue;
146                     }
147                     
148                     URL classURL = classLoader.getResource(name);
149                     InputStream classStream = classURL.openStream();
150                     ClassReader reader = new ClosableClassReader(classStream);
151 
152                     ClassScanner visitor = getScanner(clazz);
153                     reader.accept(visitor, 0);
154                     if (visitor.isMatch())
155                     {
156                         @SuppressWarnings("unchecked")
157                         Class<T> loadedClass = (Class<T>) loadClass(visitor.getClassName());
158                         addClassToSet(loadedClass, set, flags);
159                     }
160                 }
161                 catch (Exception e)
162                 {
163                     if (logger.isDebugEnabled())
164                     {
165                         Throwable t = ExceptionHelper.getRootException(e);
166                         logger.debug(String.format("%s: caused by: %s", e.toString(), t.toString()));
167                     }
168                 }
169             }
170         }
171         jar.close();
172         
173         return set;
174     }
175 
176     protected boolean hasFlag(int flags, int flag)
177     {
178         return (flags & flag) != 0;
179     }
180 
181     protected <T> Set<Class<T>> processFileUrl(URL url, String basepath, Class<T> clazz, int flags) throws IOException
182     {
183         Set<Class<T>> set = new HashSet<Class<T>>();
184         String urlBase = url.getFile();
185         //We can't URLDecoder.decode(path) since some encoded chars are allowed on file uris
186         urlBase = urlBase.replaceAll("%20", " ");
187         File dir = new File(urlBase);
188         if(!dir.isDirectory())
189         {
190             logger.warn("Cannot process File URL: " + url + ". Path is not a directory");
191             return set;
192         }
193 
194         @SuppressWarnings("unchecked")
195         Collection<File> files = FileUtils.listFiles(new File(urlBase), new String[]{"class"}, true);
196         for (File file : files)
197         {
198             try
199             {
200                 //Ignore anonymous and inner classes
201                 if (file.getName().contains("$") && !hasFlag(flags, INCLUDE_INNER))
202                 {
203                     continue;
204                 }
205                 InputStream classStream = new FileInputStream(file);
206                 ClassReader reader = new ClosableClassReader(classStream);
207 
208                 ClassScanner visitor = getScanner(clazz);
209                 reader.accept(visitor, 0);
210                 if (visitor.isMatch())
211                 {
212                     @SuppressWarnings("unchecked")
213                     Class<T> loadedClass = (Class<T>) loadClass(visitor.getClassName());
214                     addClassToSet(loadedClass, set, flags);
215                 }
216             }
217             catch (IOException e)
218             {
219                 if (logger.isDebugEnabled())
220                 {
221                     Throwable t = ExceptionHelper.getRootException(e);
222                     logger.debug(String.format("%s: caused by: %s", e.toString(), t.toString()));
223                 }
224             }
225         }
226         return set;
227     }
228 
229     protected <T> void addClassToSet(Class<T> c, Set<Class<T>> set, int flags)
230     {
231         if (c != null)
232         {
233             synchronized (set)
234             {
235                 if (c.isInterface())
236                 {
237                     if (hasFlag(flags, INCLUDE_INTERFACE))
238                     {
239                         set.add(c);
240                     }
241                 }
242                 else if (Modifier.isAbstract(c.getModifiers()))
243                 {
244                     if (hasFlag(flags, INCLUDE_ABSTRACT))
245                     {
246                         set.add(c);
247                     }
248                 }
249                 else
250                 {
251                     set.add(c);
252                 }
253             }
254         }
255     }
256 
257     protected Class<?> loadClass(String name)
258     {
259         String c = name.replace("/", ".");
260         try
261         {
262             return ClassUtils.loadClass(c, classLoader);
263         }
264         catch (ClassNotFoundException e)
265         {
266             if (logger.isWarnEnabled())
267             {
268                 logger.warn(String.format("%s : %s", c, e.toString()));
269             }
270             return null;
271         }
272     }
273 
274     /**
275      * Works out the correct scanner based on the class passed in
276      * <p/>
277      * Note that these could be better architected by breaking out filters into strategy objects, but for now this
278      * suits my needs
279      *
280      * @param clazz the type to scan for
281      * @return a scanner suitable for handling the type passed in
282      * @see AnnotationsScanner
283      * @see InterfaceClassScanner
284      * @see ImplementationClassScanner
285      */
286     protected ClassScanner getScanner(Class<?> clazz)
287     {
288         if (clazz.isInterface())
289         {
290             if (clazz.isAnnotation())
291             {
292                 @SuppressWarnings("unchecked")
293                 Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) clazz;
294                 
295                 AnnotationFilter filter = null;
296                 Annotation[] annos = clazz.getDeclaredAnnotations();
297                 for (int i = 0; i < annos.length; i++)
298                 {
299                     Annotation anno = annos[i];
300                     if (anno instanceof Target)
301                     {
302                         if (((Target) anno).value()[0] == ElementType.ANNOTATION_TYPE)
303                         {
304                             filter = new MetaAnnotationTypeFilter(annotationClass, classLoader);
305                         }
306                     }
307                 }
308                 if (filter == null)
309                 {
310                     filter = new AnnotationTypeFilter(annotationClass);
311                 }
312                 return new AnnotationsScanner(filter);
313             }
314             else
315             {
316                 return new InterfaceClassScanner(clazz);
317             }
318         }
319         else
320         {
321             return new ImplementationClassScanner(clazz);
322         }
323     }
324 }