1 /** 2 * 3 * Licensed to the Apache Software Foundation (ASF) under one 4 * or more contributor license agreements. See the NOTICE file 5 * distributed with this work for additional information 6 * regarding copyright ownership. The ASF licenses this file 7 * to you under the Apache License, Version 2.0 (the 8 * "License"); you may not use this file except in compliance 9 * with the License. You may obtain a copy of the License at 10 * 11 * http://www.apache.org/licenses/LICENSE-2.0 12 * 13 * Unless required by applicable law or agreed to in writing, software 14 * distributed under the License is distributed on an "AS IS" BASIS, 15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 * See the License for the specific language governing permissions and 17 * limitations under the License. 18 */ 19 package org.apache.hadoop.hbase; 20 21 import static org.junit.Assert.assertArrayEquals; 22 import static org.junit.Assert.assertEquals; 23 import static org.junit.Assert.assertFalse; 24 import static org.junit.Assert.assertTrue; 25 26 import java.io.File; 27 import java.io.FileInputStream; 28 import java.io.FileOutputStream; 29 import java.io.IOException; 30 import java.io.PrintStream; 31 import java.lang.reflect.Method; 32 import java.net.URL; 33 import java.net.URLClassLoader; 34 import java.util.HashSet; 35 import java.util.Set; 36 import java.util.concurrent.atomic.AtomicLong; 37 import java.util.jar.Attributes; 38 import java.util.jar.JarEntry; 39 import java.util.jar.JarOutputStream; 40 import java.util.jar.Manifest; 41 42 import javax.tools.JavaCompiler; 43 import javax.tools.ToolProvider; 44 45 import org.apache.commons.logging.Log; 46 import org.apache.commons.logging.LogFactory; 47 import org.apache.hadoop.hbase.testclassification.SmallTests; 48 import org.junit.AfterClass; 49 import org.junit.BeforeClass; 50 import org.junit.Rule; 51 import org.junit.Test; 52 import org.junit.experimental.categories.Category; 53 import org.junit.rules.TestName; 54 55 @Category(SmallTests.class) 56 public class TestClassFinder { 57 58 private static final Log LOG = LogFactory.getLog(TestClassFinder.class); 59 60 @Rule public TestName name = new TestName(); 61 private static final HBaseCommonTestingUtility testUtil = new HBaseCommonTestingUtility(); 62 private static final String BASEPKG = "tfcpkg"; 63 private static final String PREFIX = "Prefix"; 64 65 // Use unique jar/class/package names in each test case with the help 66 // of these global counters; we are mucking with ClassLoader in this test 67 // and we don't want individual test cases to conflict via it. 68 private static AtomicLong testCounter = new AtomicLong(0); 69 private static AtomicLong jarCounter = new AtomicLong(0); 70 71 private static String basePath = null; 72 73 @BeforeClass createTestDir()74 public static void createTestDir() throws IOException { 75 basePath = testUtil.getDataTestDir(TestClassFinder.class.getSimpleName()).toString(); 76 if (!basePath.endsWith("/")) { 77 basePath += "/"; 78 } 79 // Make sure we get a brand new directory. 80 File testDir = new File(basePath); 81 if (testDir.exists()) { 82 deleteTestDir(); 83 } 84 assertTrue(testDir.mkdirs()); 85 LOG.info("Using new, clean directory=" + testDir); 86 } 87 88 @AfterClass deleteTestDir()89 public static void deleteTestDir() throws IOException { 90 testUtil.cleanupTestDir(TestClassFinder.class.getSimpleName()); 91 } 92 93 @Test testClassFinderCanFindClassesInJars()94 public void testClassFinderCanFindClassesInJars() throws Exception { 95 long counter = testCounter.incrementAndGet(); 96 FileAndPath c1 = compileTestClass(counter, "", "c1"); 97 FileAndPath c2 = compileTestClass(counter, ".nested", "c2"); 98 FileAndPath c3 = compileTestClass(counter, "", "c3"); 99 packageAndLoadJar(c1, c3); 100 packageAndLoadJar(c2); 101 102 ClassFinder allClassesFinder = new ClassFinder(); 103 Set<Class<?>> allClasses = allClassesFinder.findClasses( 104 makePackageName("", counter), false); 105 assertEquals(3, allClasses.size()); 106 } 107 108 @Test testClassFinderHandlesConflicts()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(); 117 Set<Class<?>> allClasses = allClassesFinder.findClasses( 118 makePackageName("", counter), false); 119 assertEquals(2, allClasses.size()); 120 } 121 122 @Test testClassFinderHandlesNestedPackages()123 public void testClassFinderHandlesNestedPackages() throws Exception { 124 final String NESTED = ".nested"; 125 final String CLASSNAME1 = name.getMethodName() + "1"; 126 final String CLASSNAME2 = name.getMethodName() + "2"; 127 long counter = testCounter.incrementAndGet(); 128 FileAndPath c1 = compileTestClass(counter, "", "c1"); 129 FileAndPath c2 = compileTestClass(counter, NESTED, CLASSNAME1); 130 FileAndPath c3 = compileTestClass(counter, NESTED, CLASSNAME2); 131 packageAndLoadJar(c1, c2); 132 packageAndLoadJar(c3); 133 134 ClassFinder allClassesFinder = new ClassFinder(); 135 Set<Class<?>> nestedClasses = allClassesFinder.findClasses( 136 makePackageName(NESTED, counter), false); 137 assertEquals(2, nestedClasses.size()); 138 Class<?> nestedClass1 = makeClass(NESTED, CLASSNAME1, counter); 139 assertTrue(nestedClasses.contains(nestedClass1)); 140 Class<?> nestedClass2 = makeClass(NESTED, CLASSNAME2, counter); 141 assertTrue(nestedClasses.contains(nestedClass2)); 142 } 143 144 @Test testClassFinderFiltersByNameInJar()145 public void testClassFinderFiltersByNameInJar() throws Exception { 146 final long counter = testCounter.incrementAndGet(); 147 final String classNamePrefix = name.getMethodName(); 148 LOG.info("Created jar " + createAndLoadJar("", classNamePrefix, counter)); 149 150 ClassFinder.FileNameFilter notExcNameFilter = new ClassFinder.FileNameFilter() { 151 @Override 152 public boolean isCandidateFile(String fileName, String absFilePath) { 153 return !fileName.startsWith(PREFIX); 154 } 155 }; 156 ClassFinder incClassesFinder = new ClassFinder(null, notExcNameFilter, null); 157 Set<Class<?>> incClasses = incClassesFinder.findClasses( 158 makePackageName("", counter), false); 159 assertEquals(1, incClasses.size()); 160 Class<?> incClass = makeClass("", classNamePrefix, counter); 161 assertTrue(incClasses.contains(incClass)); 162 } 163 164 @Test testClassFinderFiltersByClassInJar()165 public void testClassFinderFiltersByClassInJar() throws Exception { 166 final long counter = testCounter.incrementAndGet(); 167 final String classNamePrefix = name.getMethodName(); 168 LOG.info("Created jar " + createAndLoadJar("", classNamePrefix, counter)); 169 170 final ClassFinder.ClassFilter notExcClassFilter = new ClassFinder.ClassFilter() { 171 @Override 172 public boolean isCandidateClass(Class<?> c) { 173 return !c.getSimpleName().startsWith(PREFIX); 174 } 175 }; 176 ClassFinder incClassesFinder = new ClassFinder(null, null, notExcClassFilter); 177 Set<Class<?>> incClasses = incClassesFinder.findClasses( 178 makePackageName("", counter), false); 179 assertEquals(1, incClasses.size()); 180 Class<?> incClass = makeClass("", classNamePrefix, counter); 181 assertTrue(incClasses.contains(incClass)); 182 } 183 createAndLoadJar(final String packageNameSuffix, final String classNamePrefix, final long counter)184 private static String createAndLoadJar(final String packageNameSuffix, 185 final String classNamePrefix, final long counter) 186 throws Exception { 187 FileAndPath c1 = compileTestClass(counter, packageNameSuffix, classNamePrefix); 188 FileAndPath c2 = compileTestClass(counter, packageNameSuffix, PREFIX + "1"); 189 FileAndPath c3 = compileTestClass(counter, packageNameSuffix, PREFIX + classNamePrefix + "2"); 190 return packageAndLoadJar(c1, c2, c3); 191 } 192 193 @Test testClassFinderFiltersByPathInJar()194 public void testClassFinderFiltersByPathInJar() throws Exception { 195 final String CLASSNAME = name.getMethodName(); 196 long counter = testCounter.incrementAndGet(); 197 FileAndPath c1 = compileTestClass(counter, "", CLASSNAME); 198 FileAndPath c2 = compileTestClass(counter, "", "c2"); 199 packageAndLoadJar(c1); 200 final String excludedJar = packageAndLoadJar(c2); 201 /* ResourcePathFilter will pass us the resourcePath as a path of a 202 * URL from the classloader. For Windows, the ablosute path and the 203 * one from the URL have different file separators. 204 */ 205 final String excludedJarResource = 206 new File(excludedJar).toURI().getRawSchemeSpecificPart(); 207 208 final ClassFinder.ResourcePathFilter notExcJarFilter = 209 new ClassFinder.ResourcePathFilter() { 210 @Override 211 public boolean isCandidatePath(String resourcePath, boolean isJar) { 212 return !isJar || !resourcePath.equals(excludedJarResource); 213 } 214 }; 215 ClassFinder incClassesFinder = new ClassFinder(notExcJarFilter, null, null); 216 Set<Class<?>> incClasses = incClassesFinder.findClasses( 217 makePackageName("", counter), false); 218 assertEquals(1, incClasses.size()); 219 Class<?> incClass = makeClass("", CLASSNAME, counter); 220 assertTrue(incClasses.contains(incClass)); 221 } 222 223 @Test testClassFinderCanFindClassesInDirs()224 public void testClassFinderCanFindClassesInDirs() throws Exception { 225 // Make some classes for us to find. Class naming and packaging is kinda cryptic. 226 // TODO: Fix. 227 final long counter = testCounter.incrementAndGet(); 228 final String classNamePrefix = name.getMethodName(); 229 String pkgNameSuffix = name.getMethodName(); 230 LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter)); 231 ClassFinder allClassesFinder = new ClassFinder(); 232 String pkgName = makePackageName(pkgNameSuffix, counter); 233 Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false); 234 assertTrue("Classes in " + pkgName, allClasses.size() > 0); 235 String classNameToFind = classNamePrefix + counter; 236 assertTrue(contains(allClasses, classNameToFind)); 237 } 238 contains(final Set<Class<?>> classes, final String simpleName)239 private static boolean contains(final Set<Class<?>> classes, final String simpleName) { 240 for (Class<?> c: classes) { 241 if (c.getSimpleName().equals(simpleName)) return true; 242 } 243 return false; 244 } 245 246 @Test testClassFinderFiltersByNameInDirs()247 public void testClassFinderFiltersByNameInDirs() throws Exception { 248 // Make some classes for us to find. Class naming and packaging is kinda cryptic. 249 // TODO: Fix. 250 final long counter = testCounter.incrementAndGet(); 251 final String classNamePrefix = name.getMethodName(); 252 String pkgNameSuffix = name.getMethodName(); 253 LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter)); 254 final String classNameToFilterOut = classNamePrefix + counter; 255 final ClassFinder.FileNameFilter notThisFilter = new ClassFinder.FileNameFilter() { 256 @Override 257 public boolean isCandidateFile(String fileName, String absFilePath) { 258 return !fileName.equals(classNameToFilterOut + ".class"); 259 } 260 }; 261 String pkgName = makePackageName(pkgNameSuffix, counter); 262 ClassFinder allClassesFinder = new ClassFinder(); 263 Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false); 264 assertTrue("Classes in " + pkgName, allClasses.size() > 0); 265 ClassFinder notThisClassFinder = new ClassFinder(null, notThisFilter, null); 266 Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(pkgName, false); 267 assertFalse(contains(notAllClasses, classNameToFilterOut)); 268 assertEquals(allClasses.size() - 1, notAllClasses.size()); 269 } 270 271 @Test testClassFinderFiltersByClassInDirs()272 public void testClassFinderFiltersByClassInDirs() throws Exception { 273 // Make some classes for us to find. Class naming and packaging is kinda cryptic. 274 // TODO: Fix. 275 final long counter = testCounter.incrementAndGet(); 276 final String classNamePrefix = name.getMethodName(); 277 String pkgNameSuffix = name.getMethodName(); 278 LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter)); 279 final Class<?> clazz = makeClass(pkgNameSuffix, classNamePrefix, counter); 280 final ClassFinder.ClassFilter notThisFilter = new ClassFinder.ClassFilter() { 281 @Override 282 public boolean isCandidateClass(Class<?> c) { 283 return c != clazz; 284 } 285 }; 286 String pkgName = makePackageName(pkgNameSuffix, counter); 287 ClassFinder allClassesFinder = new ClassFinder(); 288 Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false); 289 assertTrue("Classes in " + pkgName, allClasses.size() > 0); 290 ClassFinder notThisClassFinder = new ClassFinder(null, null, notThisFilter); 291 Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(pkgName, false); 292 assertFalse(contains(notAllClasses, clazz.getSimpleName())); 293 assertEquals(allClasses.size() - 1, notAllClasses.size()); 294 } 295 296 @Test testClassFinderFiltersByPathInDirs()297 public void testClassFinderFiltersByPathInDirs() throws Exception { 298 final String hardcodedThisSubdir = "hbase-common"; 299 final ClassFinder.ResourcePathFilter notExcJarFilter = 300 new ClassFinder.ResourcePathFilter() { 301 @Override 302 public boolean isCandidatePath(String resourcePath, boolean isJar) { 303 return isJar || !resourcePath.contains(hardcodedThisSubdir); 304 } 305 }; 306 String thisPackage = this.getClass().getPackage().getName(); 307 ClassFinder notThisClassFinder = new ClassFinder(notExcJarFilter, null, null); 308 Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(thisPackage, false); 309 assertFalse(notAllClasses.contains(this.getClass())); 310 } 311 312 @Test testClassFinderDefaultsToOwnPackage()313 public void testClassFinderDefaultsToOwnPackage() throws Exception { 314 // Correct handling of nested packages is tested elsewhere, so here we just assume 315 // pkgClasses is the correct answer that we don't have to check. 316 ClassFinder allClassesFinder = new ClassFinder(); 317 Set<Class<?>> pkgClasses = allClassesFinder.findClasses( 318 ClassFinder.class.getPackage().getName(), false); 319 Set<Class<?>> defaultClasses = allClassesFinder.findClasses(false); 320 assertArrayEquals(pkgClasses.toArray(), defaultClasses.toArray()); 321 } 322 323 private static class FileAndPath { 324 String path; 325 File file; FileAndPath(String path, File file)326 public FileAndPath(String path, File file) { 327 this.file = file; 328 this.path = path; 329 } 330 } 331 makeClass(String nestedPkgSuffix, String className, long counter)332 private static Class<?> makeClass(String nestedPkgSuffix, 333 String className, long counter) throws ClassNotFoundException { 334 return Class.forName( 335 makePackageName(nestedPkgSuffix, counter) + "." + className + counter); 336 } 337 makePackageName(String nestedSuffix, long counter)338 private static String makePackageName(String nestedSuffix, long counter) { 339 return BASEPKG + counter + nestedSuffix; 340 } 341 342 /** 343 * Compiles the test class with bogus code into a .class file. 344 * Unfortunately it's very tedious. 345 * @param counter Unique test counter. 346 * @param packageNameSuffix Package name suffix (e.g. ".suffix") for nesting, or "". 347 * @return The resulting .class file and the location in jar it is supposed to go to. 348 */ compileTestClass(long counter, String packageNameSuffix, String classNamePrefix)349 private static FileAndPath compileTestClass(long counter, 350 String packageNameSuffix, String classNamePrefix) throws Exception { 351 classNamePrefix = classNamePrefix + counter; 352 String packageName = makePackageName(packageNameSuffix, counter); 353 String javaPath = basePath + classNamePrefix + ".java"; 354 String classPath = basePath + classNamePrefix + ".class"; 355 PrintStream source = new PrintStream(javaPath); 356 source.println("package " + packageName + ";"); 357 source.println("public class " + classNamePrefix 358 + " { public static void main(String[] args) { } };"); 359 source.close(); 360 JavaCompiler jc = ToolProvider.getSystemJavaCompiler(); 361 int result = jc.run(null, null, null, javaPath); 362 assertEquals(0, result); 363 File classFile = new File(classPath); 364 assertTrue(classFile.exists()); 365 return new FileAndPath(packageName.replace('.', '/') + '/', classFile); 366 } 367 368 /** 369 * Makes a jar out of some class files. Unfortunately it's very tedious. 370 * @param filesInJar Files created via compileTestClass. 371 * @return path to the resulting jar file. 372 */ packageAndLoadJar(FileAndPath... filesInJar)373 private static String packageAndLoadJar(FileAndPath... filesInJar) throws Exception { 374 // First, write the bogus jar file. 375 String path = basePath + "jar" + jarCounter.incrementAndGet() + ".jar"; 376 Manifest manifest = new Manifest(); 377 manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); 378 FileOutputStream fos = new FileOutputStream(path); 379 JarOutputStream jarOutputStream = new JarOutputStream(fos, manifest); 380 // Directory entries for all packages have to be added explicitly for 381 // resources to be findable via ClassLoader. Directory entries must end 382 // with "/"; the initial one is expected to, also. 383 Set<String> pathsInJar = new HashSet<String>(); 384 for (FileAndPath fileAndPath : filesInJar) { 385 String pathToAdd = fileAndPath.path; 386 while (pathsInJar.add(pathToAdd)) { 387 int ix = pathToAdd.lastIndexOf('/', pathToAdd.length() - 2); 388 if (ix < 0) { 389 break; 390 } 391 pathToAdd = pathToAdd.substring(0, ix); 392 } 393 } 394 for (String pathInJar : pathsInJar) { 395 jarOutputStream.putNextEntry(new JarEntry(pathInJar)); 396 jarOutputStream.closeEntry(); 397 } 398 for (FileAndPath fileAndPath : filesInJar) { 399 File file = fileAndPath.file; 400 jarOutputStream.putNextEntry( 401 new JarEntry(fileAndPath.path + file.getName())); 402 byte[] allBytes = new byte[(int)file.length()]; 403 FileInputStream fis = new FileInputStream(file); 404 fis.read(allBytes); 405 fis.close(); 406 jarOutputStream.write(allBytes); 407 jarOutputStream.closeEntry(); 408 } 409 jarOutputStream.close(); 410 fos.close(); 411 412 // Add the file to classpath. 413 File jarFile = new File(path); 414 assertTrue(jarFile.exists()); 415 URLClassLoader urlClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader(); 416 Method method = URLClassLoader.class 417 .getDeclaredMethod("addURL", new Class[] { URL.class }); 418 method.setAccessible(true); 419 method.invoke(urlClassLoader, new Object[] { jarFile.toURI().toURL() }); 420 return jarFile.getAbsolutePath(); 421 } 422 }; 423