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