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
34   * for discovering classes in packages.
35   *
36   * @author Michele Mostarda (mostarda@fbk.eu)
37   */
38  public class DiscoveryUtils {
39  
40      private static final String FILE_PREFIX  = "file:";
41      private static final String CLASS_SUFFIX = ".class";
42  
43  
44      /**
45       * Scans all classes accessible from the context class loader
46       * which belong to the given package and sub-packages.
47       *
48       * @param packageName the root package.
49       * @return list of matching classes.
50       */
51      public static List<Class> getClassesInPackage(String packageName) {
52          final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
53          assert classLoader != null;
54          final String path = packageName.replace('.', '/');
55          final Enumeration<URL> resources;
56          try {
57              resources = classLoader.getResources(path);
58          } catch (IOException ioe) {
59              throw new IllegalStateException("Error while retrieving internal resource path.", ioe);
60          }
61          final List<File> dirs = new ArrayList<File>();
62          while (resources.hasMoreElements()) {
63              final URL resource = resources.nextElement();
64              final String fileName = resource.getFile();
65              final String fileNameDecoded;
66              try {
67                  fileNameDecoded = URLDecoder.decode(fileName, "UTF-8");
68              } catch (UnsupportedEncodingException uee) {
69                  throw new IllegalStateException("Error while decoding class file name.", uee);
70              }
71              dirs.add( new File(fileNameDecoded) );
72          }
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
82       * which belong to the given package and sub-packages and filter
83       * them by ones implementing the specified interface <code>iface</code>.
84       *
85       * @param packageName the root package.
86       * @param filter the interface/class filter.
87       * @return list of matching classes.
88       */
89      public static List<Class> getClassesInPackage(String packageName, Class filter) {
90          final List<Class> classesInPackage = getClassesInPackage(packageName);
91          final List<Class> result = new ArrayList<Class>();
92          Class superClazz;
93          for(Class clazz : classesInPackage) {
94              if(clazz.equals(filter)) {
95                  continue;
96              }
97              superClazz = clazz.getSuperclass();
98              if( ( superClazz != null && superClazz.equals(filter) ) || contains(clazz.getInterfaces(), filter) ) {
99                  result.add(clazz);
100             }
101         }
102         return result;
103     }
104 
105     /**
106      * Find all classes within the specified location by package name.
107      *
108      * @param location class location.
109      * @param packageName package name.
110      * @return list of detected classes.
111      */
112     private static List<Class> findClasses(File location, String packageName) {
113         final String locationPath = location.getPath();
114         if( locationPath.indexOf(FILE_PREFIX) == 0 ) {
115             return findClassesInJAR(locationPath);
116         }
117         return findClassesInDir(location, packageName);
118     }
119 
120     /**
121      * Find all classes within a JAR in a given prefix addressed with syntax
122      * <code>file:<path/to.jar>!<path/to/package>.
123      *
124      * @param location package location.
125      * @return list of detected classes.
126      */
127     private static List<Class> findClassesInJAR(String location) {
128         final String[] sections = location.split("!");
129         if(sections.length != 2) {
130             throw new IllegalArgumentException("Invalid JAR location.");
131         }
132         final String jarLocation = sections[0].substring(FILE_PREFIX.length());
133         final String packagePath = sections[1].substring(1);
134 
135         try {
136             final JarFile jarFile = new JarFile(jarLocation);
137             final Enumeration<JarEntry> entries = jarFile.entries();
138             final List<Class> result = new ArrayList<Class>();
139             JarEntry current;
140             String entryName;
141             String clazzName;
142             Class clazz;
143             while(entries.hasMoreElements()) {
144                 current = entries.nextElement();
145                 entryName = current.getName();
146                 if(
147                         StringUtils.isPrefix(packagePath, entryName)
148                                 &&
149                         StringUtils.isSuffix(CLASS_SUFFIX, entryName)
150                                 &&
151                         ! entryName.contains("$")
152                 ) {
153                     try {
154                         clazzName = entryName.substring(
155                                 0, entryName.length() - CLASS_SUFFIX.length()
156                         ).replaceAll("/",".");
157                         clazz = Class.forName(clazzName);
158                     } catch (ClassNotFoundException cnfe) {
159                         throw new IllegalStateException("Error while loading detected class.", cnfe);
160                     }
161                     result.add(clazz);
162                 }
163             }
164             return result;
165         } catch (IOException ioe) {
166             throw new RuntimeException("Error while opening JAR file.", ioe);
167         }
168     }
169 
170     /**
171      * Recursive method used to find all classes in a given directory and sub-dirs.
172      *
173      * @param directory   The base directory
174      * @param packageName The package name for classes found inside the base directory
175      * @return The classes
176      */
177     private static List<Class> findClassesInDir(File directory, String packageName) {
178         if (!directory.exists()) {
179             return Collections.emptyList();
180         }
181         final List<Class> classes = new ArrayList<Class>();
182         File[] files = directory.listFiles();
183         for (File file : files) {
184             String fileName = file.getName();
185             if (file.isDirectory()) {
186                 assert !fileName.contains(".");
187                 classes.addAll(findClassesInDir(file, packageName + "." + fileName));
188             } else if (fileName.endsWith(".class") && !fileName.contains("$")) {
189                 try {
190                     Class clazz;
191                     try {
192                         clazz = Class.forName(packageName + '.' + fileName.substring(0, fileName.length() - 6));
193                     } catch (ExceptionInInitializerError e) {
194                         /*
195                         happen, for example, in classes, which depend on Spring to inject some beans,
196                         and which fail, if dependency is not fulfilled
197                         */
198                         clazz = Class.forName(
199                                 packageName + '.' + fileName.substring(0, fileName.length() - 6),
200                                 false,
201                                 Thread.currentThread().getContextClassLoader()
202                         );
203                     }
204                     classes.add(clazz);
205                 } catch (ClassNotFoundException cnfe) {
206                     throw new IllegalStateException("Error while loading detected class.", cnfe);
207                 }
208             }
209         }
210         return classes;
211     }
212 
213     private static boolean contains(Object[] list, Object t) {
214         for(Object o : list) {
215             if( o.equals(t) ) {
216                 return true;
217             }
218         }
219         return false;
220     }
221 
222     private DiscoveryUtils(){}
223 
224 }