View Javadoc

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
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
89    public static void deleteTestDir() throws IOException {
90      testUtil.cleanupTestDir(TestClassFinder.class.getSimpleName());
91    }
92  
93    @Test
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
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
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
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
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 
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
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
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 
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
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
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
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
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;
326     public FileAndPath(String path, File file) {
327       this.file = file;
328       this.path = path;
329     }
330   }
331 
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 
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    */
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    */
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 };