View Javadoc

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