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.*;
22  
23  import java.io.File;
24  import java.io.FileInputStream;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.PrintStream;
28  import java.lang.reflect.Method;
29  import java.net.URL;
30  import java.net.URLClassLoader;
31  import java.util.*;
32  import java.util.concurrent.atomic.AtomicLong;
33  import java.util.jar.*;
34  import javax.tools.*;
35  
36  import org.apache.hadoop.hbase.SmallTests;
37  
38  import org.junit.experimental.categories.Category;
39  import org.junit.AfterClass;
40  import org.junit.BeforeClass;
41  import org.junit.Test;
42  
43  import org.apache.commons.io.FileUtils;
44  
45  @Category(SmallTests.class)
46  public class TestClassFinder {
47    private static final HBaseTestingUtility testUtil = new HBaseTestingUtility();
48    private static final String BASEPKG = "tfcpkg";
49    // Use unique jar/class/package names in each test case with the help
50    // of these global counters; we are mucking with ClassLoader in this test
51    // and we don't want individual test cases to conflict via it.
52    private static AtomicLong testCounter = new AtomicLong(0);
53    private static AtomicLong jarCounter = new AtomicLong(0);
54  
55    private static String basePath = null;
56  
57    // Default name/class filters for testing.
58    private static final ClassFinder.FileNameFilter trueNameFilter =
59        new ClassFinder.FileNameFilter() {
60      @Override
61      public boolean isCandidateFile(String fileName, String absFilePath) {
62        return true;
63      }
64    };
65    private static final ClassFinder.ClassFilter trueClassFilter =
66        new ClassFinder.ClassFilter() {
67      @Override
68      public boolean isCandidateClass(Class<?> c) {
69        return true;
70      }
71    };
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    }
86  
87    @AfterClass
88    public static void deleteTestDir() throws IOException {
89      testUtil.cleanupTestDir(TestClassFinder.class.getSimpleName());
90    }
91  
92    @Test
93    public void testClassFinderCanFindClassesInJars() throws Exception {
94      long counter = testCounter.incrementAndGet();
95      FileAndPath c1 = compileTestClass(counter, "", "c1");
96      FileAndPath c2 = compileTestClass(counter, ".nested", "c2");
97      FileAndPath c3 = compileTestClass(counter, "", "c3");
98      packageAndLoadJar(c1, c3);
99      packageAndLoadJar(c2);
100 
101     ClassFinder allClassesFinder = new ClassFinder(trueNameFilter, trueClassFilter);
102     Set<Class<?>> allClasses = allClassesFinder.findClasses(
103         makePackageName("", counter), false);
104     assertEquals(3, allClasses.size());
105   }
106 
107   @Test
108   public void testClassFinderHandlesConflicts() throws Exception {
109     long counter = testCounter.incrementAndGet();
110     FileAndPath c1 = compileTestClass(counter, "", "c1");
111     FileAndPath c2 = compileTestClass(counter, "", "c2");
112     packageAndLoadJar(c1, c2);
113     packageAndLoadJar(c1);
114 
115     ClassFinder allClassesFinder = new ClassFinder(trueNameFilter, trueClassFilter);
116     Set<Class<?>> allClasses = allClassesFinder.findClasses(
117         makePackageName("", counter), false);
118     assertEquals(2, allClasses.size());
119   }
120 
121   @Test
122   public void testClassFinderHandlesNestedPackages() throws Exception {
123     final String NESTED = ".nested";
124     final String CLASSNAME1 = "c2";
125     final String CLASSNAME2 = "c3";
126     long counter = testCounter.incrementAndGet();
127     FileAndPath c1 = compileTestClass(counter, "", "c1");
128     FileAndPath c2 = compileTestClass(counter, NESTED, CLASSNAME1);
129     FileAndPath c3 = compileTestClass(counter, NESTED, CLASSNAME2);
130     packageAndLoadJar(c1, c2);
131     packageAndLoadJar(c3);
132 
133     ClassFinder allClassesFinder = new ClassFinder(trueNameFilter, trueClassFilter);
134     Set<Class<?>> nestedClasses = allClassesFinder.findClasses(
135         makePackageName(NESTED, counter), false);
136     assertEquals(2, nestedClasses.size());
137     Class<?> nestedClass1 = makeClass(NESTED, CLASSNAME1, counter);
138     assertTrue(nestedClasses.contains(nestedClass1));
139     Class<?> nestedClass2 = makeClass(NESTED, CLASSNAME2, counter);
140     assertTrue(nestedClasses.contains(nestedClass2));
141   }
142 
143   @Test
144   public void testClassFinderFiltersByNameInJar() throws Exception {
145     final String CLASSNAME = "c1";
146     final String CLASSNAMEEXCPREFIX = "c2";
147     long counter = testCounter.incrementAndGet();
148     FileAndPath c1 = compileTestClass(counter, "", CLASSNAME);
149     FileAndPath c2 = compileTestClass(counter, "", CLASSNAMEEXCPREFIX + "1");
150     FileAndPath c3 = compileTestClass(counter, "", CLASSNAMEEXCPREFIX + "2");
151     packageAndLoadJar(c1, c2, c3);
152 
153     ClassFinder.FileNameFilter notExcNameFilter = new ClassFinder.FileNameFilter() {
154       @Override
155       public boolean isCandidateFile(String fileName, String absFilePath) {
156         return !fileName.startsWith(CLASSNAMEEXCPREFIX);
157       }
158     };
159     ClassFinder incClassesFinder = new ClassFinder(notExcNameFilter, trueClassFilter);
160     Set<Class<?>> incClasses = incClassesFinder.findClasses(
161         makePackageName("", counter), false);
162     assertEquals(1, incClasses.size());
163     Class<?> incClass = makeClass("", CLASSNAME, counter);
164     assertTrue(incClasses.contains(incClass));
165   }
166 
167   @Test
168   public void testClassFinderFiltersByClassInJar() throws Exception {
169     final String CLASSNAME = "c1";
170     final String CLASSNAMEEXCPREFIX = "c2";
171     long counter = testCounter.incrementAndGet();
172     FileAndPath c1 = compileTestClass(counter, "", CLASSNAME);
173     FileAndPath c2 = compileTestClass(counter, "", CLASSNAMEEXCPREFIX + "1");
174     FileAndPath c3 = compileTestClass(counter, "", CLASSNAMEEXCPREFIX + "2");
175     packageAndLoadJar(c1, c2, c3);
176 
177     final ClassFinder.ClassFilter notExcClassFilter = new ClassFinder.ClassFilter() {
178       @Override
179       public boolean isCandidateClass(Class<?> c) {
180         return !c.getSimpleName().startsWith(CLASSNAMEEXCPREFIX);
181       }
182     };
183     ClassFinder incClassesFinder = new ClassFinder(trueNameFilter, notExcClassFilter);
184     Set<Class<?>> incClasses = incClassesFinder.findClasses(
185         makePackageName("", counter), false);
186     assertEquals(1, incClasses.size());
187     Class<?> incClass = makeClass("", CLASSNAME, counter);
188     assertTrue(incClasses.contains(incClass));
189   }
190 
191   @Test
192   public void testClassFinderCanFindClassesInDirs() throws Exception {
193     // Well, technically, we are not guaranteed that the classes will
194     // be in dirs, but during normal build they would be.
195     ClassFinder allClassesFinder = new ClassFinder(trueNameFilter, trueClassFilter);
196     Set<Class<?>> allClasses = allClassesFinder.findClasses(
197         this.getClass().getPackage().getName(), false);
198     assertTrue(allClasses.contains(this.getClass()));
199     assertTrue(allClasses.contains(ClassFinder.class));
200   }
201 
202   @Test
203   public void testClassFinderFiltersByNameInDirs() throws Exception {
204     final String thisName = this.getClass().getSimpleName();
205     ClassFinder.FileNameFilter notThisFilter = new ClassFinder.FileNameFilter() {
206       @Override
207       public boolean isCandidateFile(String fileName, String absFilePath) {
208         return !fileName.equals(thisName + ".class");
209       }
210     };
211     String thisPackage = this.getClass().getPackage().getName();
212     ClassFinder allClassesFinder = new ClassFinder(trueNameFilter, trueClassFilter);
213     Set<Class<?>> allClasses = allClassesFinder.findClasses(thisPackage, false);
214     ClassFinder notThisClassFinder = new ClassFinder(notThisFilter, trueClassFilter);
215     Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(thisPackage, false);
216     assertFalse(notAllClasses.contains(this.getClass()));
217     assertEquals(allClasses.size() - 1, notAllClasses.size());
218   }
219 
220   @Test
221   public void testClassFinderFiltersByClassInDirs() throws Exception {
222     ClassFinder.ClassFilter notThisFilter = new ClassFinder.ClassFilter() {
223       @Override
224       public boolean isCandidateClass(Class<?> c) {
225         return c != TestClassFinder.class;
226       }
227     };
228     String thisPackage = this.getClass().getPackage().getName();
229     ClassFinder allClassesFinder = new ClassFinder(trueNameFilter, trueClassFilter);
230     Set<Class<?>> allClasses = allClassesFinder.findClasses(thisPackage, false);
231     ClassFinder notThisClassFinder = new ClassFinder(trueNameFilter, notThisFilter);
232     Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(thisPackage, false);
233     assertFalse(notAllClasses.contains(this.getClass()));
234     assertEquals(allClasses.size() - 1, notAllClasses.size());
235   }
236 
237   @Test
238   public void testClassFinderDefaultsToOwnPackage() throws Exception {
239     // Correct handling of nested packages is tested elsewhere, so here we just assume
240     // pkgClasses is the correct answer that we don't have to check.
241     ClassFinder allClassesFinder = new ClassFinder(trueNameFilter, trueClassFilter);
242     Set<Class<?>> pkgClasses = allClassesFinder.findClasses(
243         ClassFinder.class.getPackage().getName(), false);
244     Set<Class<?>> defaultClasses = allClassesFinder.findClasses(false);
245     assertArrayEquals(pkgClasses.toArray(), defaultClasses.toArray());
246   }
247 
248   private static class FileAndPath {
249     String path;
250     File file;
251     public FileAndPath(String path, File file) {
252       this.file = file;
253       this.path = path;
254     }
255   }
256 
257   private static Class<?> makeClass(String nestedPkgSuffix,
258       String className, long counter) throws ClassNotFoundException {
259     return Class.forName(
260         makePackageName(nestedPkgSuffix, counter) + "." + className + counter);
261   }
262 
263   private static String makePackageName(String nestedSuffix, long counter) {
264     return BASEPKG + counter + nestedSuffix;
265   }
266 
267   /**
268    * Compiles the test class with bogus code into a .class file.
269    * Unfortunately it's very tedious.
270    * @param counter Unique test counter.
271    * @param packageNameSuffix Package name suffix (e.g. ".suffix") for nesting, or "".
272    * @return The resulting .class file and the location in jar it is supposed to go to.
273    */
274   private static FileAndPath compileTestClass(long counter,
275       String packageNameSuffix, String classNamePrefix) throws Exception {
276     classNamePrefix = classNamePrefix + counter;
277     String packageName = makePackageName(packageNameSuffix, counter);
278     String javaPath = basePath + classNamePrefix + ".java";
279     String classPath = basePath + classNamePrefix + ".class";
280     PrintStream source = new PrintStream(javaPath);
281     source.println("package " + packageName + ";");
282     source.println("public class " + classNamePrefix
283         + " { public static void main(String[] args) { } };");
284     source.close();
285     JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
286     int result = jc.run(null, null, null, javaPath);
287     assertEquals(0, result);
288     File classFile = new File(classPath);
289     assertTrue(classFile.exists());
290     return new FileAndPath(packageName.replace('.', '/') + '/', classFile);
291   }
292 
293   /**
294    * Makes a jar out of some class files. Unfortunately it's very tedious.
295    * @param filesInJar Files created via compileTestClass.
296    */
297   private static void packageAndLoadJar(FileAndPath... filesInJar) throws Exception {
298     // First, write the bogus jar file.
299     String path = basePath + "jar" + jarCounter.incrementAndGet() + ".jar";
300     Manifest manifest = new Manifest();
301     manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
302     FileOutputStream fos = new FileOutputStream(path);
303     JarOutputStream jarOutputStream = new JarOutputStream(fos, manifest);
304     // Directory entries for all packages have to be added explicitly for
305     // resources to be findable via ClassLoader. Directory entries must end
306     // with "/"; the initial one is expected to, also.
307     Set<String> pathsInJar = new HashSet<String>();
308     for (FileAndPath fileAndPath : filesInJar) {
309       String pathToAdd = fileAndPath.path;
310       while (pathsInJar.add(pathToAdd)) {
311         int ix = pathToAdd.lastIndexOf('/', pathToAdd.length() - 2);
312         if (ix < 0) {
313           break;
314         }
315         pathToAdd = pathToAdd.substring(0, ix);
316       }
317     }
318     for (String pathInJar : pathsInJar) {
319       jarOutputStream.putNextEntry(new JarEntry(pathInJar));
320       jarOutputStream.closeEntry();
321     }
322     for (FileAndPath fileAndPath : filesInJar) {
323       File file = fileAndPath.file;
324       jarOutputStream.putNextEntry(
325           new JarEntry(fileAndPath.path + file.getName()));
326       byte[] allBytes = new byte[(int)file.length()];
327       FileInputStream fis = new FileInputStream(file);
328       fis.read(allBytes);
329       fis.close();
330       jarOutputStream.write(allBytes);
331       jarOutputStream.closeEntry();
332     }
333     jarOutputStream.close();
334     fos.close();
335 
336     // Add the file to classpath.
337     File jarFile = new File(path);
338     assertTrue(jarFile.exists());
339     URLClassLoader urlClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
340     Method method = URLClassLoader.class
341         .getDeclaredMethod("addURL", new Class[] { URL.class });
342     method.setAccessible(true);
343     method.invoke(urlClassLoader, new Object[] { jarFile.toURI().toURL() });
344   }
345 };