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