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