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