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