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