/** * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.hbase; import static org.junit.Assert.*; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.*; import java.util.concurrent.atomic.AtomicLong; import java.util.jar.*; import javax.tools.*; import org.apache.hadoop.hbase.SmallTests; import org.junit.experimental.categories.Category; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.apache.commons.io.FileUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @Category(SmallTests.class) public class TestClassFinder { private static final Log LOG = LogFactory.getLog(TestClassFinder.class); private static final HBaseCommonTestingUtility testUtil = new HBaseCommonTestingUtility(); private static final String BASEPKG = "tfcpkg"; // Use unique jar/class/package names in each test case with the help // of these global counters; we are mucking with ClassLoader in this test // and we don't want individual test cases to conflict via it. private static AtomicLong testCounter = new AtomicLong(0); private static AtomicLong jarCounter = new AtomicLong(0); private static String basePath = null; @BeforeClass public static void createTestDir() throws IOException { basePath = testUtil.getDataTestDir(TestClassFinder.class.getSimpleName()).toString(); if (!basePath.endsWith("/")) { basePath += "/"; } // Make sure we get a brand new directory. File testDir = new File(basePath); if (testDir.exists()) { deleteTestDir(); } assertTrue(testDir.mkdirs()); } @AfterClass public static void deleteTestDir() throws IOException { testUtil.cleanupTestDir(TestClassFinder.class.getSimpleName()); } @Test public void testClassFinderCanFindClassesInJars() throws Exception { long counter = testCounter.incrementAndGet(); FileAndPath c1 = compileTestClass(counter, "", "c1"); FileAndPath c2 = compileTestClass(counter, ".nested", "c2"); FileAndPath c3 = compileTestClass(counter, "", "c3"); packageAndLoadJar(c1, c3); packageAndLoadJar(c2); ClassFinder allClassesFinder = new ClassFinder(); Set<Class<?>> allClasses = allClassesFinder.findClasses( makePackageName("", counter), false); assertEquals(3, allClasses.size()); } @Test public void testClassFinderHandlesConflicts() throws Exception { long counter = testCounter.incrementAndGet(); FileAndPath c1 = compileTestClass(counter, "", "c1"); FileAndPath c2 = compileTestClass(counter, "", "c2"); packageAndLoadJar(c1, c2); packageAndLoadJar(c1); ClassFinder allClassesFinder = new ClassFinder(); Set<Class<?>> allClasses = allClassesFinder.findClasses( makePackageName("", counter), false); assertEquals(2, allClasses.size()); } @Test public void testClassFinderHandlesNestedPackages() throws Exception { final String NESTED = ".nested"; final String CLASSNAME1 = "c2"; final String CLASSNAME2 = "c3"; long counter = testCounter.incrementAndGet(); FileAndPath c1 = compileTestClass(counter, "", "c1"); FileAndPath c2 = compileTestClass(counter, NESTED, CLASSNAME1); FileAndPath c3 = compileTestClass(counter, NESTED, CLASSNAME2); packageAndLoadJar(c1, c2); packageAndLoadJar(c3); ClassFinder allClassesFinder = new ClassFinder(); Set<Class<?>> nestedClasses = allClassesFinder.findClasses( makePackageName(NESTED, counter), false); assertEquals(2, nestedClasses.size()); Class<?> nestedClass1 = makeClass(NESTED, CLASSNAME1, counter); assertTrue(nestedClasses.contains(nestedClass1)); Class<?> nestedClass2 = makeClass(NESTED, CLASSNAME2, counter); assertTrue(nestedClasses.contains(nestedClass2)); } @Test public void testClassFinderFiltersByNameInJar() throws Exception { final String CLASSNAME = "c1"; final String CLASSNAMEEXCPREFIX = "c2"; long counter = testCounter.incrementAndGet(); FileAndPath c1 = compileTestClass(counter, "", CLASSNAME); FileAndPath c2 = compileTestClass(counter, "", CLASSNAMEEXCPREFIX + "1"); FileAndPath c3 = compileTestClass(counter, "", CLASSNAMEEXCPREFIX + "2"); packageAndLoadJar(c1, c2, c3); ClassFinder.FileNameFilter notExcNameFilter = new ClassFinder.FileNameFilter() { @Override public boolean isCandidateFile(String fileName, String absFilePath) { return !fileName.startsWith(CLASSNAMEEXCPREFIX); } }; ClassFinder incClassesFinder = new ClassFinder(null, notExcNameFilter, null); Set<Class<?>> incClasses = incClassesFinder.findClasses( makePackageName("", counter), false); assertEquals(1, incClasses.size()); Class<?> incClass = makeClass("", CLASSNAME, counter); assertTrue(incClasses.contains(incClass)); } @Test public void testClassFinderFiltersByClassInJar() throws Exception { final String CLASSNAME = "c1"; final String CLASSNAMEEXCPREFIX = "c2"; long counter = testCounter.incrementAndGet(); FileAndPath c1 = compileTestClass(counter, "", CLASSNAME); FileAndPath c2 = compileTestClass(counter, "", CLASSNAMEEXCPREFIX + "1"); FileAndPath c3 = compileTestClass(counter, "", CLASSNAMEEXCPREFIX + "2"); packageAndLoadJar(c1, c2, c3); final ClassFinder.ClassFilter notExcClassFilter = new ClassFinder.ClassFilter() { @Override public boolean isCandidateClass(Class<?> c) { return !c.getSimpleName().startsWith(CLASSNAMEEXCPREFIX); } }; ClassFinder incClassesFinder = new ClassFinder(null, null, notExcClassFilter); Set<Class<?>> incClasses = incClassesFinder.findClasses( makePackageName("", counter), false); assertEquals(1, incClasses.size()); Class<?> incClass = makeClass("", CLASSNAME, counter); assertTrue(incClasses.contains(incClass)); } @Test public void testClassFinderFiltersByPathInJar() throws Exception { final String CLASSNAME = "c1"; long counter = testCounter.incrementAndGet(); FileAndPath c1 = compileTestClass(counter, "", CLASSNAME); FileAndPath c2 = compileTestClass(counter, "", "c2"); packageAndLoadJar(c1); final String excludedJar = packageAndLoadJar(c2); final ClassFinder.ResourcePathFilter notExcJarFilter = new ClassFinder.ResourcePathFilter() { @Override public boolean isCandidatePath(String resourcePath, boolean isJar) { return !isJar || !resourcePath.equals(excludedJar); } }; ClassFinder incClassesFinder = new ClassFinder(notExcJarFilter, null, null); Set<Class<?>> incClasses = incClassesFinder.findClasses( makePackageName("", counter), false); assertEquals(1, incClasses.size()); Class<?> incClass = makeClass("", CLASSNAME, counter); assertTrue(incClasses.contains(incClass)); } @Test public void testClassFinderCanFindClassesInDirs() throws Exception { // Well, technically, we are not guaranteed that the classes will // be in dirs, but during normal build they would be. ClassFinder allClassesFinder = new ClassFinder(); Set<Class<?>> allClasses = allClassesFinder.findClasses( this.getClass().getPackage().getName(), false); assertTrue(allClasses.contains(this.getClass())); assertTrue(allClasses.contains(ClassFinder.class)); } @Test public void testClassFinderFiltersByNameInDirs() throws Exception { final String thisName = this.getClass().getSimpleName(); final ClassFinder.FileNameFilter notThisFilter = new ClassFinder.FileNameFilter() { @Override public boolean isCandidateFile(String fileName, String absFilePath) { return !fileName.equals(thisName + ".class"); } }; String thisPackage = this.getClass().getPackage().getName(); ClassFinder allClassesFinder = new ClassFinder(); Set<Class<?>> allClasses = allClassesFinder.findClasses(thisPackage, false); ClassFinder notThisClassFinder = new ClassFinder(null, notThisFilter, null); Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(thisPackage, false); assertFalse(notAllClasses.contains(this.getClass())); assertEquals(allClasses.size() - 1, notAllClasses.size()); } @Test public void testClassFinderFiltersByClassInDirs() throws Exception { final ClassFinder.ClassFilter notThisFilter = new ClassFinder.ClassFilter() { @Override public boolean isCandidateClass(Class<?> c) { return c != TestClassFinder.class; } }; String thisPackage = this.getClass().getPackage().getName(); ClassFinder allClassesFinder = new ClassFinder(); Set<Class<?>> allClasses = allClassesFinder.findClasses(thisPackage, false); ClassFinder notThisClassFinder = new ClassFinder(null, null, notThisFilter); Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(thisPackage, false); assertFalse(notAllClasses.contains(this.getClass())); assertEquals(allClasses.size() - 1, notAllClasses.size()); } @Test public void testClassFinderFiltersByPathInDirs() throws Exception { final String hardcodedThisSubdir = "hbase-common"; final ClassFinder.ResourcePathFilter notExcJarFilter = new ClassFinder.ResourcePathFilter() { @Override public boolean isCandidatePath(String resourcePath, boolean isJar) { return isJar || !resourcePath.contains(hardcodedThisSubdir); } }; String thisPackage = this.getClass().getPackage().getName(); ClassFinder notThisClassFinder = new ClassFinder(notExcJarFilter, null, null); Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(thisPackage, false); assertFalse(notAllClasses.contains(this.getClass())); } @Test public void testClassFinderDefaultsToOwnPackage() throws Exception { // Correct handling of nested packages is tested elsewhere, so here we just assume // pkgClasses is the correct answer that we don't have to check. ClassFinder allClassesFinder = new ClassFinder(); Set<Class<?>> pkgClasses = allClassesFinder.findClasses( ClassFinder.class.getPackage().getName(), false); Set<Class<?>> defaultClasses = allClassesFinder.findClasses(false); assertArrayEquals(pkgClasses.toArray(), defaultClasses.toArray()); } private static class FileAndPath { String path; File file; public FileAndPath(String path, File file) { this.file = file; this.path = path; } } private static Class<?> makeClass(String nestedPkgSuffix, String className, long counter) throws ClassNotFoundException { return Class.forName( makePackageName(nestedPkgSuffix, counter) + "." + className + counter); } private static String makePackageName(String nestedSuffix, long counter) { return BASEPKG + counter + nestedSuffix; } /** * Compiles the test class with bogus code into a .class file. * Unfortunately it's very tedious. * @param counter Unique test counter. * @param packageNameSuffix Package name suffix (e.g. ".suffix") for nesting, or "". * @return The resulting .class file and the location in jar it is supposed to go to. */ private static FileAndPath compileTestClass(long counter, String packageNameSuffix, String classNamePrefix) throws Exception { classNamePrefix = classNamePrefix + counter; String packageName = makePackageName(packageNameSuffix, counter); String javaPath = basePath + classNamePrefix + ".java"; String classPath = basePath + classNamePrefix + ".class"; PrintStream source = new PrintStream(javaPath); source.println("package " + packageName + ";"); source.println("public class " + classNamePrefix + " { public static void main(String[] args) { } };"); source.close(); JavaCompiler jc = ToolProvider.getSystemJavaCompiler(); int result = jc.run(null, null, null, javaPath); assertEquals(0, result); File classFile = new File(classPath); assertTrue(classFile.exists()); return new FileAndPath(packageName.replace('.', '/') + '/', classFile); } /** * Makes a jar out of some class files. Unfortunately it's very tedious. * @param filesInJar Files created via compileTestClass. * @return path to the resulting jar file. */ private static String packageAndLoadJar(FileAndPath... filesInJar) throws Exception { // First, write the bogus jar file. String path = basePath + "jar" + jarCounter.incrementAndGet() + ".jar"; Manifest manifest = new Manifest(); manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); FileOutputStream fos = new FileOutputStream(path); JarOutputStream jarOutputStream = new JarOutputStream(fos, manifest); // Directory entries for all packages have to be added explicitly for // resources to be findable via ClassLoader. Directory entries must end // with "/"; the initial one is expected to, also. Set<String> pathsInJar = new HashSet<String>(); for (FileAndPath fileAndPath : filesInJar) { String pathToAdd = fileAndPath.path; while (pathsInJar.add(pathToAdd)) { int ix = pathToAdd.lastIndexOf('/', pathToAdd.length() - 2); if (ix < 0) { break; } pathToAdd = pathToAdd.substring(0, ix); } } for (String pathInJar : pathsInJar) { jarOutputStream.putNextEntry(new JarEntry(pathInJar)); jarOutputStream.closeEntry(); } for (FileAndPath fileAndPath : filesInJar) { File file = fileAndPath.file; jarOutputStream.putNextEntry( new JarEntry(fileAndPath.path + file.getName())); byte[] allBytes = new byte[(int)file.length()]; FileInputStream fis = new FileInputStream(file); fis.read(allBytes); fis.close(); jarOutputStream.write(allBytes); jarOutputStream.closeEntry(); } jarOutputStream.close(); fos.close(); // Add the file to classpath. File jarFile = new File(path); assertTrue(jarFile.exists()); URLClassLoader urlClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader(); Method method = URLClassLoader.class .getDeclaredMethod("addURL", new Class[] { URL.class }); method.setAccessible(true); method.invoke(urlClassLoader, new Object[] { jarFile.toURI().toURL() }); return jarFile.getAbsolutePath(); } };