001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.hbase;
020
021import java.io.File;
022import java.io.FileFilter;
023import java.io.FileInputStream;
024import java.io.IOException;
025import java.net.URL;
026import java.util.ArrayList;
027import java.util.Enumeration;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Set;
031import java.util.jar.JarEntry;
032import java.util.jar.JarInputStream;
033import java.util.regex.Matcher;
034import java.util.regex.Pattern;
035
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039/**
040 * A class that finds a set of classes that are locally accessible
041 * (from .class or .jar files), and satisfy the conditions that are
042 * imposed by name and class filters provided by the user.
043 */
044public class ClassFinder {
045  private static final Logger LOG = LoggerFactory.getLogger(ClassFinder.class);
046  private static String CLASS_EXT = ".class";
047
048  private ResourcePathFilter resourcePathFilter;
049  private FileNameFilter fileNameFilter;
050  private ClassFilter classFilter;
051  private FileFilter fileFilter;
052
053  public interface ResourcePathFilter {
054    boolean isCandidatePath(String resourcePath, boolean isJar);
055  }
056
057  public interface FileNameFilter {
058    boolean isCandidateFile(String fileName, String absFilePath);
059  }
060
061  public interface ClassFilter {
062    boolean isCandidateClass(Class<?> c);
063  }
064
065  public static class Not implements ResourcePathFilter, FileNameFilter, ClassFilter {
066    private ResourcePathFilter resourcePathFilter;
067    private FileNameFilter fileNameFilter;
068    private ClassFilter classFilter;
069
070    public Not(ResourcePathFilter resourcePathFilter){this.resourcePathFilter = resourcePathFilter;}
071    public Not(FileNameFilter fileNameFilter){this.fileNameFilter = fileNameFilter;}
072    public Not(ClassFilter classFilter){this.classFilter = classFilter;}
073
074    @Override
075    public boolean isCandidatePath(String resourcePath, boolean isJar) {
076      return !resourcePathFilter.isCandidatePath(resourcePath, isJar);
077    }
078    @Override
079    public boolean isCandidateFile(String fileName, String absFilePath) {
080      return !fileNameFilter.isCandidateFile(fileName, absFilePath);
081    }
082    @Override
083    public boolean isCandidateClass(Class<?> c) {
084      return !classFilter.isCandidateClass(c);
085    }
086  }
087
088  public static class And implements ClassFilter, ResourcePathFilter {
089    ClassFilter[] classFilters;
090    ResourcePathFilter[] resourcePathFilters;
091
092    public And(ClassFilter...classFilters) { this.classFilters = classFilters; }
093    public And(ResourcePathFilter... resourcePathFilters) {
094      this.resourcePathFilters = resourcePathFilters;
095    }
096
097    @Override
098    public boolean isCandidateClass(Class<?> c) {
099      for (ClassFilter filter : classFilters) {
100        if (!filter.isCandidateClass(c)) {
101          return false;
102        }
103      }
104      return true;
105    }
106
107    @Override public boolean isCandidatePath(String resourcePath, boolean isJar) {
108      for (ResourcePathFilter filter : resourcePathFilters) {
109        if (!filter.isCandidatePath(resourcePath, isJar)) {
110          return false;
111        }
112      }
113      return true;
114    }
115  }
116
117  public ClassFinder() {
118    this(null, null, null);
119  }
120
121  public ClassFinder(ResourcePathFilter resourcePathFilter,
122      FileNameFilter fileNameFilter, ClassFilter classFilter) {
123    this.resourcePathFilter = resourcePathFilter;
124    this.classFilter = classFilter;
125    this.fileNameFilter = fileNameFilter;
126    this.fileFilter = new FileFilterWithName(fileNameFilter);
127  }
128
129  /**
130   * Finds the classes in current package (of ClassFinder) and nested packages.
131   * @param proceedOnExceptions whether to ignore exceptions encountered for
132   *        individual jars/files/classes, and proceed looking for others.
133   */
134  public Set<Class<?>> findClasses(boolean proceedOnExceptions)
135    throws ClassNotFoundException, IOException, LinkageError {
136    return findClasses(this.getClass().getPackage().getName(), proceedOnExceptions);
137  }
138
139  /**
140   * Finds the classes in a package and nested packages.
141   * @param packageName package names
142   * @param proceedOnExceptions whether to ignore exceptions encountered for
143   *        individual jars/files/classes, and proceed looking for others.
144   */
145  public Set<Class<?>> findClasses(String packageName, boolean proceedOnExceptions)
146    throws ClassNotFoundException, IOException, LinkageError {
147    final String path = packageName.replace('.', '/');
148    final Pattern jarResourceRe = Pattern.compile("^file:(.+\\.jar)!/" + path + "$");
149
150    Enumeration<URL> resources = ClassLoader.getSystemClassLoader().getResources(path);
151    List<File> dirs = new ArrayList<>();
152    List<String> jars = new ArrayList<>();
153
154    while (resources.hasMoreElements()) {
155      URL resource = resources.nextElement();
156      String resourcePath = resource.getFile();
157      Matcher matcher = jarResourceRe.matcher(resourcePath);
158      boolean isJar = matcher.find();
159      resourcePath = isJar ? matcher.group(1) : resourcePath;
160      if (null == this.resourcePathFilter
161          || this.resourcePathFilter.isCandidatePath(resourcePath, isJar)) {
162        LOG.debug("Looking in " + resourcePath + "; isJar=" + isJar);
163        if (isJar) {
164          jars.add(resourcePath);
165        } else {
166          dirs.add(new File(resourcePath));
167        }
168      }
169    }
170
171    Set<Class<?>> classes = new HashSet<>();
172    for (File directory : dirs) {
173      classes.addAll(findClassesFromFiles(directory, packageName, proceedOnExceptions));
174    }
175    for (String jarFileName : jars) {
176      classes.addAll(findClassesFromJar(jarFileName, packageName, proceedOnExceptions));
177    }
178    return classes;
179  }
180
181  private Set<Class<?>> findClassesFromJar(String jarFileName,
182      String packageName, boolean proceedOnExceptions)
183    throws IOException, ClassNotFoundException, LinkageError {
184    JarInputStream jarFile = null;
185    try {
186      jarFile = new JarInputStream(new FileInputStream(jarFileName));
187    } catch (IOException ioEx) {
188      LOG.warn("Failed to look for classes in " + jarFileName + ": " + ioEx);
189      throw ioEx;
190    }
191
192    Set<Class<?>> classes = new HashSet<>();
193    JarEntry entry = null;
194    try {
195      while (true) {
196        try {
197          entry = jarFile.getNextJarEntry();
198        } catch (IOException ioEx) {
199          if (!proceedOnExceptions) {
200            throw ioEx;
201          }
202          LOG.warn("Failed to get next entry from " + jarFileName + ": " + ioEx);
203          break;
204        }
205        if (entry == null) {
206          break; // loop termination condition
207        }
208
209        String className = entry.getName();
210        if (!className.endsWith(CLASS_EXT)) {
211          continue;
212        }
213        int ix = className.lastIndexOf('/');
214        String fileName = (ix >= 0) ? className.substring(ix + 1) : className;
215        if (null != this.fileNameFilter
216            && !this.fileNameFilter.isCandidateFile(fileName, className)) {
217          continue;
218        }
219        className =
220            className.substring(0, className.length() - CLASS_EXT.length()).replace('/', '.');
221        if (!className.startsWith(packageName)) {
222          continue;
223        }
224        Class<?> c = makeClass(className, proceedOnExceptions);
225        if (c != null) {
226          if (!classes.add(c)) {
227            LOG.warn("Ignoring duplicate class " + className);
228          }
229        }
230      }
231      return classes;
232    } finally {
233      jarFile.close();
234    }
235  }
236
237  private Set<Class<?>> findClassesFromFiles(File baseDirectory, String packageName,
238      boolean proceedOnExceptions) throws ClassNotFoundException, LinkageError {
239    Set<Class<?>> classes = new HashSet<>();
240    if (!baseDirectory.exists()) {
241      LOG.warn(baseDirectory.getAbsolutePath() + " does not exist");
242      return classes;
243    }
244
245    File[] files = baseDirectory.listFiles(this.fileFilter);
246    if (files == null) {
247      LOG.warn("Failed to get files from " + baseDirectory.getAbsolutePath());
248      return classes;
249    }
250
251    for (File file : files) {
252      final String fileName = file.getName();
253      if (file.isDirectory()) {
254        classes.addAll(findClassesFromFiles(file, packageName + "." + fileName,
255            proceedOnExceptions));
256      } else {
257        String className = packageName + '.'
258            + fileName.substring(0, fileName.length() - CLASS_EXT.length());
259        Class<?> c = makeClass(className, proceedOnExceptions);
260        if (c != null) {
261          if (!classes.add(c)) {
262            LOG.warn("Ignoring duplicate class " + className);
263          }
264        }
265      }
266    }
267    return classes;
268  }
269
270  private Class<?> makeClass(String className, boolean proceedOnExceptions)
271    throws ClassNotFoundException, LinkageError {
272    try {
273      Class<?> c = Class.forName(className, false, this.getClass().getClassLoader());
274      boolean isCandidateClass = null == classFilter || classFilter.isCandidateClass(c);
275      return isCandidateClass ? c : null;
276    } catch (NoClassDefFoundError|ClassNotFoundException classNotFoundEx) {
277      if (!proceedOnExceptions) {
278        throw classNotFoundEx;
279      }
280      LOG.debug("Failed to instantiate or check " + className + ": " + classNotFoundEx);
281    } catch (LinkageError linkageEx) {
282      if (!proceedOnExceptions) {
283        throw linkageEx;
284      }
285      LOG.debug("Failed to instantiate or check " + className + ": " + linkageEx);
286    }
287    return null;
288  }
289
290  private static class FileFilterWithName implements FileFilter {
291    private FileNameFilter nameFilter;
292
293    public FileFilterWithName(FileNameFilter nameFilter) {
294      this.nameFilter = nameFilter;
295    }
296
297    @Override
298    public boolean accept(File file) {
299      return file.isDirectory()
300          || (file.getName().endsWith(CLASS_EXT)
301              && (null == nameFilter
302                || nameFilter.isCandidateFile(file.getName(), file.getAbsolutePath())));
303    }
304  }
305}