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}