View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *  http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.any23.util;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.UnsupportedEncodingException;
23  import java.net.URL;
24  import java.net.URLDecoder;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.Enumeration;
28  import java.util.List;
29  import java.util.jar.JarEntry;
30  import java.util.jar.JarFile;
31  
32  /**
33   * This class provides utility methods for discovering classes in packages.
34   *
35   * @author Michele Mostarda (mostarda@fbk.eu)
36   */
37  public class DiscoveryUtils {
38  
39      private static final String FILE_PREFIX = "file:";
40      private static final String CLASS_SUFFIX = ".class";
41  
42      /**
43       * Scans all classes accessible from the context class loader which belong to the given package and sub-packages.
44       *
45       * @param packageName
46       *            the root package.
47       * 
48       * @return list of matching classes.
49       */
50      public static List<Class> getClassesInPackage(String packageName) {
51          final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
52          assert classLoader != null;
53          final String path = packageName.replace('.', '/');
54          final Enumeration<URL> resources;
55          try {
56              resources = classLoader.getResources(path);
57          } catch (IOException ioe) {
58              throw new IllegalStateException("Error while retrieving internal resource path.", ioe);
59          }
60          final List<File> dirs = new ArrayList<File>();
61          while (resources.hasMoreElements()) {
62              final URL resource = resources.nextElement();
63              final String fileName = resource.getFile();
64              final String fileNameDecoded;
65              try {
66                  fileNameDecoded = URLDecoder.decode(fileName, "UTF-8");
67              } catch (UnsupportedEncodingException uee) {
68                  throw new IllegalStateException("Error while decoding class file name.", uee);
69              }
70              dirs.add(new File(fileNameDecoded));
71          }
72          @SuppressWarnings("rawtypes")
73          final ArrayList<Class> classes = new ArrayList<Class>();
74          for (File directory : dirs) {
75              classes.addAll(findClasses(directory, packageName));
76          }
77          return classes;
78      }
79  
80      /**
81       * Scans all classes accessible from the context class loader which belong to the given package and sub-packages and
82       * filter them by ones implementing the specified interface <code>iface</code>.
83       *
84       * @param packageName
85       *            the root package.
86       * @param filter
87       *            the interface/class filter.
88       * 
89       * @return list of matching classes.
90       */
91      public static List<Class> getClassesInPackage(String packageName, Class<?> filter) {
92          final List<Class> classesInPackage = getClassesInPackage(packageName);
93          @SuppressWarnings("rawtypes")
94          final List<Class> result = new ArrayList<Class>();
95          Class<?> superClazz;
96          for (Class<?> clazz : classesInPackage) {
97              if (clazz.equals(filter)) {
98                  continue;
99              }
100             superClazz = clazz.getSuperclass();
101             if ((superClazz != null && superClazz.equals(filter)) || contains(clazz.getInterfaces(), filter)) {
102                 result.add(clazz);
103             }
104         }
105         return result;
106     }
107 
108     /**
109      * Find all classes within the specified location by package name.
110      *
111      * @param location
112      *            class location.
113      * @param packageName
114      *            package name.
115      * 
116      * @return list of detected classes.
117      */
118     private static List<Class> findClasses(File location, String packageName) {
119         final String locationPath = location.getPath();
120         if (locationPath.indexOf(FILE_PREFIX) == 0) {
121             return findClassesInJAR(locationPath);
122         }
123         return findClassesInDir(location, packageName);
124     }
125 
126     /**
127      * Find all classes within a JAR in a given prefix addressed with syntax <code>file:<path/to.jar>!<path/to/package>.
128      *
129      * @param location
130      *            package location.
131      * 
132      * @return list of detected classes.
133      */
134     private static List<Class> findClassesInJAR(String location) {
135         final String[] sections = location.split("!");
136         if (sections.length != 2) {
137             throw new IllegalArgumentException("Invalid JAR location.");
138         }
139         final String jarLocation = sections[0].substring(FILE_PREFIX.length());
140         final String packagePath = sections[1].substring(1);
141 
142         try {
143             @SuppressWarnings("resource")
144             final JarFile jarFile = new JarFile(jarLocation);
145             final Enumeration<JarEntry> entries = jarFile.entries();
146             @SuppressWarnings("rawtypes")
147             final List<Class> result = new ArrayList<Class>();
148             JarEntry current;
149             String entryName;
150             String clazzName;
151             Class<?> clazz;
152             while (entries.hasMoreElements()) {
153                 current = entries.nextElement();
154                 entryName = current.getName();
155                 if (StringUtils.isPrefix(packagePath, entryName) && StringUtils.isSuffix(CLASS_SUFFIX, entryName)
156                         && !entryName.contains("$")) {
157                     try {
158                         clazzName = entryName.substring(0, entryName.length() - CLASS_SUFFIX.length()).replaceAll("/",
159                                 ".");
160                         clazz = Class.forName(clazzName);
161                     } catch (ClassNotFoundException cnfe) {
162                         throw new IllegalStateException("Error while loading detected class.", cnfe);
163                     }
164                     result.add(clazz);
165                 }
166             }
167             return result;
168         } catch (IOException ioe) {
169             throw new RuntimeException("Error while opening JAR file.", ioe);
170         }
171     }
172 
173     /**
174      * Recursive method used to find all classes in a given directory and sub-dirs.
175      *
176      * @param directory
177      *            The base directory
178      * @param packageName
179      *            The package name for classes found inside the base directory
180      * 
181      * @return The classes
182      */
183     private static List<Class> findClassesInDir(File directory, String packageName) {
184         if (!directory.exists()) {
185             return Collections.emptyList();
186         }
187         @SuppressWarnings("rawtypes")
188         final List<Class> classes = new ArrayList<Class>();
189         File[] files = directory.listFiles();
190         for (File file : files) {
191             String fileName = file.getName();
192             if (file.isDirectory()) {
193                 assert !fileName.contains(".");
194                 classes.addAll(findClassesInDir(file, packageName + "." + fileName));
195             } else if (fileName.endsWith(".class") && !fileName.contains("$")) {
196                 try {
197                     Class<?> clazz;
198                     try {
199                         clazz = Class.forName(packageName + '.' + fileName.substring(0, fileName.length() - 6));
200                     } catch (ExceptionInInitializerError e) {
201                         /*
202                          * happen, for example, in classes, which depend on Spring to inject some beans, and which fail,
203                          * if dependency is not fulfilled
204                          */
205                         clazz = Class.forName(packageName + '.' + fileName.substring(0, fileName.length() - 6), false,
206                                 Thread.currentThread().getContextClassLoader());
207                     }
208                     classes.add(clazz);
209                 } catch (ClassNotFoundException cnfe) {
210                     throw new IllegalStateException("Error while loading detected class.", cnfe);
211                 }
212             }
213         }
214         return classes;
215     }
216 
217     private static boolean contains(Object[] list, Object t) {
218         for (Object o : list) {
219             if (o.equals(t)) {
220                 return true;
221             }
222         }
223         return false;
224     }
225 
226     private DiscoveryUtils() {
227     }
228 
229 }