1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.apache.hadoop.hbase;
20  
21  import java.io.File;
22  import java.io.FileFilter;
23  import java.io.FileInputStream;
24  import java.io.IOException;
25  import java.lang.reflect.Method;
26  import java.lang.reflect.Modifier;
27  import java.net.URL;
28  import java.util.ArrayList;
29  import java.util.Enumeration;
30  import java.util.HashSet;
31  import java.util.List;
32  import java.util.Set;
33  import java.util.jar.*;
34  import java.util.regex.Matcher;
35  import java.util.regex.Pattern;
36  
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  
40  /**
41   * A class that finds a set of classes that are locally accessible
42   * (from .class or .jar files), and satisfy the conditions that are
43   * imposed by name and class filters provided by the user.
44   */
45  public class ClassFinder {
46    private static final Log LOG = LogFactory.getLog(ClassFinder.class);
47    private static String CLASS_EXT = ".class";
48  
49    private FileNameFilter fileNameFilter;
50    private ClassFilter classFilter;
51    private FileFilter fileFilter;
52  
53    public static interface FileNameFilter {
54      public boolean isCandidateFile(String fileName, String absFilePath);
55    };
56  
57    public static interface ClassFilter {
58      public boolean isCandidateClass(Class<?> c);
59    };
60  
61    public ClassFinder(FileNameFilter fileNameFilter, ClassFilter classFilter) {
62      this.classFilter = classFilter;
63      this.fileNameFilter = fileNameFilter;
64      this.fileFilter = new FileFilterWithName(fileNameFilter);
65    }
66  
67    /**
68     * Finds the classes in current package (of ClassFinder) and nested packages.
69     * @param proceedOnExceptions whether to ignore exceptions encountered for
70     *        individual jars/files/classes, and proceed looking for others.
71     */
72    public Set<Class<?>> findClasses(boolean proceedOnExceptions)
73      throws ClassNotFoundException, IOException, LinkageError {
74      return findClasses(this.getClass().getPackage().getName(), proceedOnExceptions);
75    }
76  
77    /**
78     * Finds the classes in a package and nested packages.
79     * @param packageName package names
80     * @param proceedOnExceptions whether to ignore exceptions encountered for
81     *        individual jars/files/classes, and proceed looking for others.
82     */
83    public Set<Class<?>> findClasses(String packageName, boolean proceedOnExceptions)
84      throws ClassNotFoundException, IOException, LinkageError {
85      final String path = packageName.replace('.', '/');
86      final Pattern jarResourceRe = Pattern.compile("^file:(.+\\.jar)!/" + path + "$");
87  
88      Enumeration<URL> resources = ClassLoader.getSystemClassLoader().getResources(path);
89      List<File> dirs = new ArrayList<File>();
90      List<String> jars = new ArrayList<String>();
91  
92      while (resources.hasMoreElements()) {
93        URL resource = resources.nextElement();
94        String resourcePath = resource.getFile();
95        Matcher matcher = jarResourceRe.matcher(resourcePath);
96        if (matcher.find()) {
97          jars.add(matcher.group(1));
98        } else {
99          dirs.add(new File(resource.getFile()));
100       }
101     }
102 
103     Set<Class<?>> classes = new HashSet<Class<?>>();
104     for (File directory : dirs) {
105       classes.addAll(findClassesFromFiles(directory, packageName, proceedOnExceptions));
106     }
107     for (String jarFileName : jars) {
108       classes.addAll(findClassesFromJar(jarFileName, packageName, proceedOnExceptions));
109     }
110     return classes;
111   }
112 
113   private Set<Class<?>> findClassesFromJar(String jarFileName,
114       String packageName, boolean proceedOnExceptions)
115     throws IOException, ClassNotFoundException, LinkageError {
116     JarInputStream jarFile = null;
117     try {
118       jarFile = new JarInputStream(new FileInputStream(jarFileName));
119     } catch (IOException ioEx) {
120       if (!proceedOnExceptions) {
121         throw ioEx;
122       }
123       LOG.error("Failed to look for classes in " + jarFileName + ": " + ioEx);
124     }
125 
126     Set<Class<?>> classes = new HashSet<Class<?>>();
127     JarEntry entry = null;
128     while (true) {
129       try {
130         entry = jarFile.getNextJarEntry();
131       } catch (IOException ioEx) {
132         if (!proceedOnExceptions) {
133           throw ioEx;
134         }
135         LOG.error("Failed to get next entry from " + jarFileName + ": " + ioEx);
136         break;
137       }
138       if (entry == null) {
139         break; // loop termination condition
140       }
141 
142       String className = entry.getName();
143       if (!className.endsWith(CLASS_EXT)) {
144         continue;
145       }
146       int ix = className.lastIndexOf('/');
147       String fileName = (ix >= 0) ? className.substring(ix + 1) : className;
148       if (!this.fileNameFilter.isCandidateFile(fileName, className)) {
149         continue;
150       }
151       className = className
152           .substring(0, className.length() - CLASS_EXT.length()).replace('/', '.');
153       if (!className.startsWith(packageName)) {
154         continue;
155       }
156       Class<?> c = makeClass(className, proceedOnExceptions);
157       if (c != null) {
158         if (!classes.add(c)) {
159           LOG.error("Ignoring duplicate class " + className);
160         }
161       }
162     }
163     return classes;
164   }
165 
166   private Set<Class<?>> findClassesFromFiles(File baseDirectory, String packageName,
167       boolean proceedOnExceptions) throws ClassNotFoundException, LinkageError {
168     Set<Class<?>> classes = new HashSet<Class<?>>();
169     if (!baseDirectory.exists()) {
170       LOG.error("Failed to find " + baseDirectory.getAbsolutePath());
171       return classes;
172     }
173 
174     File[] files = baseDirectory.listFiles(this.fileFilter);
175     if (files == null) {
176       LOG.error("Failed to get files from " + baseDirectory.getAbsolutePath());
177       return classes;
178     }
179 
180     for (File file : files) {
181       final String fileName = file.getName();
182       if (file.isDirectory()) {
183         classes.addAll(findClassesFromFiles(file, packageName + "." + fileName,
184             proceedOnExceptions));
185       } else {
186         String className = packageName + '.'
187             + fileName.substring(0, fileName.length() - CLASS_EXT.length());
188         Class<?> c = makeClass(className, proceedOnExceptions);
189         if (c != null) {
190           if (!classes.add(c)) {
191             LOG.error("Ignoring duplicate class " + className);
192           }
193         }
194       }
195     }
196     return classes;
197   }
198 
199   private Class<?> makeClass(String className, boolean proceedOnExceptions)
200     throws ClassNotFoundException, LinkageError {
201     try {
202       Class<?> c = Class.forName(className, false, this.getClass().getClassLoader());
203       return classFilter.isCandidateClass(c) ? c : null;
204     } catch (ClassNotFoundException classNotFoundEx) {
205       if (!proceedOnExceptions) {
206         throw classNotFoundEx;
207       }
208       LOG.error("Failed to instantiate or check " + className + ": " + classNotFoundEx);
209     } catch (LinkageError linkageEx) {
210       if (!proceedOnExceptions) {
211         throw linkageEx;
212       }
213       LOG.error("Failed to instantiate or check " + className + ": " + linkageEx);
214     }
215     return null;
216   }
217 
218   private class FileFilterWithName implements FileFilter {
219     private FileNameFilter nameFilter;
220 
221     public FileFilterWithName(FileNameFilter nameFilter) {
222       this.nameFilter = nameFilter;
223     }
224 
225     @Override
226     public boolean accept(File file) {
227       return file.isDirectory()
228           || (file.getName().endsWith(CLASS_EXT)
229               && nameFilter.isCandidateFile(file.getName(), file.getAbsolutePath()));
230     }
231   };
232 };