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 static org.hamcrest.MatcherAssert.assertThat; 021import static org.hamcrest.Matchers.arrayContainingInAnyOrder; 022import static org.junit.jupiter.api.Assertions.assertEquals; 023import static org.junit.jupiter.api.Assertions.assertFalse; 024import static org.junit.jupiter.api.Assertions.assertTrue; 025 026import java.io.File; 027import java.io.FileInputStream; 028import java.io.FileOutputStream; 029import java.io.IOException; 030import java.io.PrintStream; 031import java.net.URL; 032import java.net.URLClassLoader; 033import java.util.HashSet; 034import java.util.Set; 035import java.util.concurrent.atomic.AtomicLong; 036import java.util.jar.Attributes; 037import java.util.jar.JarEntry; 038import java.util.jar.JarOutputStream; 039import java.util.jar.Manifest; 040import javax.tools.JavaCompiler; 041import javax.tools.ToolProvider; 042import org.apache.hadoop.hbase.testclassification.MiscTests; 043import org.apache.hadoop.hbase.testclassification.SmallTests; 044import org.junit.jupiter.api.AfterAll; 045import org.junit.jupiter.api.BeforeAll; 046import org.junit.jupiter.api.Tag; 047import org.junit.jupiter.api.Test; 048import org.junit.jupiter.api.TestInfo; 049import org.slf4j.Logger; 050import org.slf4j.LoggerFactory; 051 052@Tag(MiscTests.TAG) 053@Tag(SmallTests.TAG) 054public class TestClassFinder { 055 056 private static final Logger LOG = LoggerFactory.getLogger(TestClassFinder.class); 057 058 private static final HBaseCommonTestingUtil testUtil = new HBaseCommonTestingUtil(); 059 private static final String BASEPKG = "tfcpkg"; 060 private static final String PREFIX = "Prefix"; 061 062 // Use unique jar/class/package names in each test case with the help 063 // of these global counters; we are mucking with ClassLoader in this test 064 // and we don't want individual test cases to conflict via it. 065 private static AtomicLong testCounter = new AtomicLong(0); 066 private static AtomicLong jarCounter = new AtomicLong(0); 067 068 private static String basePath = null; 069 070 private static CustomClassloader classLoader; 071 072 @BeforeAll 073 public static void createTestDir() throws IOException { 074 basePath = testUtil.getDataTestDir(TestClassFinder.class.getSimpleName()).toString(); 075 if (!basePath.endsWith("/")) { 076 basePath += "/"; 077 } 078 // Make sure we get a brand new directory. 079 File testDir = new File(basePath); 080 if (testDir.exists()) { 081 deleteTestDir(); 082 } 083 assertTrue(testDir.mkdirs()); 084 LOG.info("Using new, clean directory=" + testDir); 085 086 classLoader = new CustomClassloader(new URL[0], ClassLoader.getSystemClassLoader()); 087 } 088 089 @AfterAll 090 public static void deleteTestDir() { 091 testUtil.cleanupTestDir(TestClassFinder.class.getSimpleName()); 092 } 093 094 @Test 095 public void testClassFinderCanFindClassesInJars() throws Exception { 096 long counter = testCounter.incrementAndGet(); 097 FileAndPath c1 = compileTestClass(counter, "", "c1"); 098 FileAndPath c2 = compileTestClass(counter, ".nested", "c2"); 099 FileAndPath c3 = compileTestClass(counter, "", "c3"); 100 packageAndLoadJar(c1, c3); 101 packageAndLoadJar(c2); 102 103 ClassFinder allClassesFinder = new ClassFinder(classLoader); 104 Set<Class<?>> allClasses = allClassesFinder.findClasses(makePackageName("", counter), false); 105 assertEquals(3, allClasses.size()); 106 } 107 108 @Test 109 public void testClassFinderHandlesConflicts() throws Exception { 110 long counter = testCounter.incrementAndGet(); 111 FileAndPath c1 = compileTestClass(counter, "", "c1"); 112 FileAndPath c2 = compileTestClass(counter, "", "c2"); 113 packageAndLoadJar(c1, c2); 114 packageAndLoadJar(c1); 115 116 ClassFinder allClassesFinder = new ClassFinder(classLoader); 117 Set<Class<?>> allClasses = allClassesFinder.findClasses(makePackageName("", counter), false); 118 assertEquals(2, allClasses.size()); 119 } 120 121 @Test 122 public void testClassFinderHandlesNestedPackages(TestInfo testInfo) throws Exception { 123 final String NESTED = ".nested"; 124 final String CLASSNAME1 = testInfo.getTestMethod().get().getName() + "1"; 125 final String CLASSNAME2 = testInfo.getTestMethod().get().getName() + "2"; 126 long counter = testCounter.incrementAndGet(); 127 FileAndPath c1 = compileTestClass(counter, "", "c1"); 128 FileAndPath c2 = compileTestClass(counter, NESTED, CLASSNAME1); 129 FileAndPath c3 = compileTestClass(counter, NESTED, CLASSNAME2); 130 packageAndLoadJar(c1, c2); 131 packageAndLoadJar(c3); 132 133 ClassFinder allClassesFinder = new ClassFinder(classLoader); 134 Set<Class<?>> nestedClasses = 135 allClassesFinder.findClasses(makePackageName(NESTED, counter), false); 136 assertEquals(2, nestedClasses.size()); 137 Class<?> nestedClass1 = makeClass(NESTED, CLASSNAME1, counter); 138 assertTrue(nestedClasses.contains(nestedClass1)); 139 Class<?> nestedClass2 = makeClass(NESTED, CLASSNAME2, counter); 140 assertTrue(nestedClasses.contains(nestedClass2)); 141 } 142 143 @Test 144 public void testClassFinderFiltersByNameInJar(TestInfo testInfo) throws Exception { 145 final long counter = testCounter.incrementAndGet(); 146 final String classNamePrefix = testInfo.getTestMethod().get().getName(); 147 LOG.info("Created jar " + createAndLoadJar("", classNamePrefix, counter)); 148 149 ClassFinder.FileNameFilter notExcNameFilter = 150 (fileName, absFilePath) -> !fileName.startsWith(PREFIX); 151 ClassFinder incClassesFinder = new ClassFinder(null, notExcNameFilter, null, classLoader); 152 Set<Class<?>> incClasses = incClassesFinder.findClasses(makePackageName("", counter), false); 153 assertEquals(1, incClasses.size()); 154 Class<?> incClass = makeClass("", classNamePrefix, counter); 155 assertTrue(incClasses.contains(incClass)); 156 } 157 158 @Test 159 public void testClassFinderFiltersByClassInJar(TestInfo testInfo) throws Exception { 160 final long counter = testCounter.incrementAndGet(); 161 final String classNamePrefix = testInfo.getTestMethod().get().getName(); 162 LOG.info("Created jar " + createAndLoadJar("", classNamePrefix, counter)); 163 164 final ClassFinder.ClassFilter notExcClassFilter = c -> !c.getSimpleName().startsWith(PREFIX); 165 ClassFinder incClassesFinder = new ClassFinder(null, null, notExcClassFilter, classLoader); 166 Set<Class<?>> incClasses = incClassesFinder.findClasses(makePackageName("", counter), false); 167 assertEquals(1, incClasses.size()); 168 Class<?> incClass = makeClass("", classNamePrefix, counter); 169 assertTrue(incClasses.contains(incClass)); 170 } 171 172 private static String createAndLoadJar(final String packageNameSuffix, 173 final String classNamePrefix, final long counter) throws Exception { 174 FileAndPath c1 = compileTestClass(counter, packageNameSuffix, classNamePrefix); 175 FileAndPath c2 = compileTestClass(counter, packageNameSuffix, PREFIX + "1"); 176 FileAndPath c3 = compileTestClass(counter, packageNameSuffix, PREFIX + classNamePrefix + "2"); 177 return packageAndLoadJar(c1, c2, c3); 178 } 179 180 @Test 181 public void testClassFinderFiltersByPathInJar(TestInfo testInfo) throws Exception { 182 final String CLASSNAME = testInfo.getTestMethod().get().getName(); 183 long counter = testCounter.incrementAndGet(); 184 FileAndPath c1 = compileTestClass(counter, "", CLASSNAME); 185 FileAndPath c2 = compileTestClass(counter, "", "c2"); 186 packageAndLoadJar(c1); 187 final String excludedJar = packageAndLoadJar(c2); 188 /* 189 * ResourcePathFilter will pass us the resourcePath as a path of a URL from the classloader. For 190 * Windows, the ablosute path and the one from the URL have different file separators. 191 */ 192 final String excludedJarResource = new File(excludedJar).toURI().getRawSchemeSpecificPart(); 193 194 final ClassFinder.ResourcePathFilter notExcJarFilter = 195 (resourcePath, isJar) -> !isJar || !resourcePath.equals(excludedJarResource); 196 ClassFinder incClassesFinder = new ClassFinder(notExcJarFilter, null, null, classLoader); 197 Set<Class<?>> incClasses = incClassesFinder.findClasses(makePackageName("", counter), false); 198 assertEquals(1, incClasses.size()); 199 Class<?> incClass = makeClass("", CLASSNAME, counter); 200 assertTrue(incClasses.contains(incClass)); 201 } 202 203 @Test 204 public void testClassFinderCanFindClassesInDirs(TestInfo testInfo) throws Exception { 205 // Make some classes for us to find. Class naming and packaging is kinda cryptic. 206 // TODO: Fix. 207 final long counter = testCounter.incrementAndGet(); 208 final String classNamePrefix = testInfo.getTestMethod().get().getName(); 209 String pkgNameSuffix = testInfo.getTestMethod().get().getName(); 210 LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter)); 211 ClassFinder allClassesFinder = new ClassFinder(classLoader); 212 String pkgName = makePackageName(pkgNameSuffix, counter); 213 Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false); 214 assertTrue(allClasses.size() > 0, "Classes in " + pkgName); 215 String classNameToFind = classNamePrefix + counter; 216 assertTrue(contains(allClasses, classNameToFind)); 217 } 218 219 private static boolean contains(final Set<Class<?>> classes, final String simpleName) { 220 for (Class<?> c : classes) { 221 if (c.getSimpleName().equals(simpleName)) { 222 return true; 223 } 224 } 225 return false; 226 } 227 228 @Test 229 public void testClassFinderFiltersByNameInDirs(TestInfo testInfo) throws Exception { 230 // Make some classes for us to find. Class naming and packaging is kinda cryptic. 231 // TODO: Fix. 232 final long counter = testCounter.incrementAndGet(); 233 final String classNamePrefix = testInfo.getTestMethod().get().getName(); 234 String pkgNameSuffix = testInfo.getTestMethod().get().getName(); 235 LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter)); 236 final String classNameToFilterOut = classNamePrefix + counter; 237 final ClassFinder.FileNameFilter notThisFilter = 238 (fileName, absFilePath) -> !fileName.equals(classNameToFilterOut + ".class"); 239 String pkgName = makePackageName(pkgNameSuffix, counter); 240 ClassFinder allClassesFinder = new ClassFinder(classLoader); 241 Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false); 242 assertTrue(allClasses.size() > 0, "Classes in " + pkgName); 243 ClassFinder notThisClassFinder = new ClassFinder(null, notThisFilter, null, classLoader); 244 Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(pkgName, false); 245 assertFalse(contains(notAllClasses, classNameToFilterOut)); 246 assertEquals(allClasses.size() - 1, notAllClasses.size()); 247 } 248 249 @Test 250 public void testClassFinderFiltersByClassInDirs(TestInfo testInfo) throws Exception { 251 // Make some classes for us to find. Class naming and packaging is kinda cryptic. 252 // TODO: Fix. 253 final long counter = testCounter.incrementAndGet(); 254 final String classNamePrefix = testInfo.getTestMethod().get().getName(); 255 String pkgNameSuffix = testInfo.getTestMethod().get().getName(); 256 LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter)); 257 final Class<?> clazz = makeClass(pkgNameSuffix, classNamePrefix, counter); 258 final ClassFinder.ClassFilter notThisFilter = c -> c != clazz; 259 String pkgName = makePackageName(pkgNameSuffix, counter); 260 ClassFinder allClassesFinder = new ClassFinder(classLoader); 261 Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false); 262 assertTrue(allClasses.size() > 0, "Classes in " + pkgName); 263 ClassFinder notThisClassFinder = new ClassFinder(null, null, notThisFilter, classLoader); 264 Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(pkgName, false); 265 assertFalse(contains(notAllClasses, clazz.getSimpleName())); 266 assertEquals(allClasses.size() - 1, notAllClasses.size()); 267 } 268 269 @Test 270 public void testClassFinderFiltersByPathInDirs() throws Exception { 271 final String hardcodedThisSubdir = "hbase-common"; 272 final ClassFinder.ResourcePathFilter notExcJarFilter = 273 (resourcePath, isJar) -> isJar || !resourcePath.contains(hardcodedThisSubdir); 274 String thisPackage = this.getClass().getPackage().getName(); 275 ClassFinder notThisClassFinder = new ClassFinder(notExcJarFilter, null, null, classLoader); 276 Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(thisPackage, false); 277 assertFalse(notAllClasses.contains(this.getClass())); 278 } 279 280 @Test 281 public void testClassFinderDefaultsToOwnPackage() throws Exception { 282 // Correct handling of nested packages is tested elsewhere, so here we just assume 283 // pkgClasses is the correct answer that we don't have to check. 284 ClassFinder allClassesFinder = new ClassFinder(classLoader); 285 Set<Class<?>> pkgClasses = 286 allClassesFinder.findClasses(ClassFinder.class.getPackage().getName(), false); 287 Set<Class<?>> defaultClasses = allClassesFinder.findClasses(false); 288 Object[] pkgClassesArray = pkgClasses.toArray(); 289 Object[] defaultClassesArray = defaultClasses.toArray(); 290 assertEquals(pkgClassesArray.length, defaultClassesArray.length); 291 assertThat(pkgClassesArray, arrayContainingInAnyOrder(defaultClassesArray)); 292 } 293 294 private static class FileAndPath { 295 String path; 296 File file; 297 298 public FileAndPath(String path, File file) { 299 this.file = file; 300 this.path = path; 301 } 302 } 303 304 private static Class<?> makeClass(String nestedPkgSuffix, String className, long counter) 305 throws ClassNotFoundException { 306 String name = makePackageName(nestedPkgSuffix, counter) + "." + className + counter; 307 return Class.forName(name, true, classLoader); 308 } 309 310 private static String makePackageName(String nestedSuffix, long counter) { 311 return BASEPKG + counter + nestedSuffix; 312 } 313 314 /** 315 * Compiles the test class with bogus code into a .class file. Unfortunately it's very tedious. 316 * @param counter Unique test counter. 317 * @param packageNameSuffix Package name suffix (e.g. ".suffix") for nesting, or "". 318 * @return The resulting .class file and the location in jar it is supposed to go to. 319 */ 320 private static FileAndPath compileTestClass(long counter, String packageNameSuffix, 321 String classNamePrefix) throws Exception { 322 classNamePrefix = classNamePrefix + counter; 323 String packageName = makePackageName(packageNameSuffix, counter); 324 String javaPath = basePath + classNamePrefix + ".java"; 325 String classPath = basePath + classNamePrefix + ".class"; 326 PrintStream source = new PrintStream(javaPath); 327 source.println("package " + packageName + ";"); 328 source.println( 329 "public class " + classNamePrefix + " { public static void main(String[] args) { } };"); 330 source.close(); 331 JavaCompiler jc = ToolProvider.getSystemJavaCompiler(); 332 int result = jc.run(null, null, null, javaPath); 333 assertEquals(0, result); 334 File classFile = new File(classPath); 335 assertTrue(classFile.exists()); 336 return new FileAndPath(packageName.replace('.', '/') + '/', classFile); 337 } 338 339 /** 340 * Makes a jar out of some class files. Unfortunately it's very tedious. 341 * @param filesInJar Files created via compileTestClass. 342 * @return path to the resulting jar file. 343 */ 344 private static String packageAndLoadJar(FileAndPath... filesInJar) throws Exception { 345 // First, write the bogus jar file. 346 String path = basePath + "jar" + jarCounter.incrementAndGet() + ".jar"; 347 Manifest manifest = new Manifest(); 348 manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); 349 FileOutputStream fos = new FileOutputStream(path); 350 JarOutputStream jarOutputStream = new JarOutputStream(fos, manifest); 351 // Directory entries for all packages have to be added explicitly for 352 // resources to be findable via ClassLoader. Directory entries must end 353 // with "/"; the initial one is expected to, also. 354 Set<String> pathsInJar = new HashSet<>(); 355 for (FileAndPath fileAndPath : filesInJar) { 356 String pathToAdd = fileAndPath.path; 357 while (pathsInJar.add(pathToAdd)) { 358 int ix = pathToAdd.lastIndexOf('/', pathToAdd.length() - 2); 359 if (ix < 0) { 360 break; 361 } 362 pathToAdd = pathToAdd.substring(0, ix); 363 } 364 } 365 for (String pathInJar : pathsInJar) { 366 jarOutputStream.putNextEntry(new JarEntry(pathInJar)); 367 jarOutputStream.closeEntry(); 368 } 369 for (FileAndPath fileAndPath : filesInJar) { 370 File file = fileAndPath.file; 371 jarOutputStream.putNextEntry(new JarEntry(fileAndPath.path + file.getName())); 372 byte[] allBytes = new byte[(int) file.length()]; 373 FileInputStream fis = new FileInputStream(file); 374 fis.read(allBytes); 375 fis.close(); 376 jarOutputStream.write(allBytes); 377 jarOutputStream.closeEntry(); 378 } 379 jarOutputStream.close(); 380 fos.close(); 381 382 // Add the file to classpath. 383 File jarFile = new File(path); 384 assertTrue(jarFile.exists()); 385 classLoader.addURL(jarFile.toURI().toURL()); 386 return jarFile.getAbsolutePath(); 387 } 388 389 // Java 11 workaround - Custom class loader to expose addUrl method of URLClassLoader 390 private static class CustomClassloader extends URLClassLoader { 391 public CustomClassloader(URL[] urls, ClassLoader parentLoader) { 392 super(urls, parentLoader); 393 } 394 395 @Override 396 public void addURL(URL url) { 397 super.addURL(url); 398 } 399 } 400}