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