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