View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase;
19  
20  import static org.junit.Assert.assertArrayEquals;
21  import static org.junit.Assert.assertEquals;
22  import static org.junit.Assert.assertFalse;
23  import static org.junit.Assert.assertTrue;
24  
25  import java.io.File;
26  import java.io.FileInputStream;
27  import java.io.FileOutputStream;
28  import java.io.IOException;
29  import java.io.PrintStream;
30  import java.net.URL;
31  import java.net.URLClassLoader;
32  import java.util.HashSet;
33  import java.util.Set;
34  import java.util.concurrent.atomic.AtomicLong;
35  import java.util.jar.Attributes;
36  import java.util.jar.JarEntry;
37  import java.util.jar.JarOutputStream;
38  import java.util.jar.Manifest;
39  
40  import javax.tools.JavaCompiler;
41  import javax.tools.ToolProvider;
42  
43  import org.apache.commons.logging.Log;
44  import org.apache.commons.logging.LogFactory;
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  
53  @Category(SmallTests.class)
54  public class TestClassFinder {
55    private static final Log LOG = LogFactory.getLog(TestClassFinder.class);
56  
57    @Rule public TestName name = new TestName();
58    private static final HBaseCommonTestingUtility testUtil = new HBaseCommonTestingUtility();
59    private static final String BASEPKG = "tfcpkg";
60    private static final String PREFIX = "Prefix";
61  
62    // Use unique jar/class/package names in each test case with the help
63    // of these global counters; we are mucking with ClassLoader in this test
64    // and we don't want individual test cases to conflict via it.
65    private static AtomicLong testCounter = new AtomicLong(0);
66    private static AtomicLong jarCounter = new AtomicLong(0);
67  
68    private static String basePath = null;
69  
70    private static CustomClassloader classLoader;
71  
72    @BeforeClass
73    public static void createTestDir() throws IOException {
74      basePath = testUtil.getDataTestDir(TestClassFinder.class.getSimpleName()).toString();
75      if (!basePath.endsWith("/")) {
76        basePath += "/";
77      }
78      // Make sure we get a brand new directory.
79      File testDir = new File(basePath);
80      if (testDir.exists()) {
81        deleteTestDir();
82      }
83      assertTrue(testDir.mkdirs());
84      LOG.info("Using new, clean directory=" + testDir);
85  
86      classLoader = new CustomClassloader(new URL[0], ClassLoader.getSystemClassLoader());
87    }
88  
89    @AfterClass
90    public static void deleteTestDir() {
91      testUtil.cleanupTestDir(TestClassFinder.class.getSimpleName());
92    }
93  
94    @Test
95    public void testClassFinderCanFindClassesInJars() throws Exception {
96      long counter = testCounter.incrementAndGet();
97      FileAndPath c1 = compileTestClass(counter, "", "c1");
98      FileAndPath c2 = compileTestClass(counter, ".nested", "c2");
99      FileAndPath c3 = compileTestClass(counter, "", "c3");
100     packageAndLoadJar(c1, c3);
101     packageAndLoadJar(c2);
102 
103     ClassFinder allClassesFinder = new ClassFinder(classLoader);
104     Set<Class<?>> allClasses = allClassesFinder.findClasses(
105         makePackageName("", counter), false);
106     assertEquals(3, allClasses.size());
107   }
108 
109   @Test
110   public void testClassFinderHandlesConflicts() throws Exception {
111     long counter = testCounter.incrementAndGet();
112     FileAndPath c1 = compileTestClass(counter, "", "c1");
113     FileAndPath c2 = compileTestClass(counter, "", "c2");
114     packageAndLoadJar(c1, c2);
115     packageAndLoadJar(c1);
116 
117     ClassFinder allClassesFinder = new ClassFinder(classLoader);
118     Set<Class<?>> allClasses = allClassesFinder.findClasses(
119         makePackageName("", counter), false);
120     assertEquals(2, allClasses.size());
121   }
122 
123   @Test
124   public void testClassFinderHandlesNestedPackages() throws Exception {
125     final String NESTED = ".nested";
126     final String CLASSNAME1 = name.getMethodName() + "1";
127     final String CLASSNAME2 = name.getMethodName() + "2";
128     long counter = testCounter.incrementAndGet();
129     FileAndPath c1 = compileTestClass(counter, "", "c1");
130     FileAndPath c2 = compileTestClass(counter, NESTED, CLASSNAME1);
131     FileAndPath c3 = compileTestClass(counter, NESTED, CLASSNAME2);
132     packageAndLoadJar(c1, c2);
133     packageAndLoadJar(c3);
134 
135     ClassFinder allClassesFinder = new ClassFinder(classLoader);
136     Set<Class<?>> nestedClasses = allClassesFinder.findClasses(
137         makePackageName(NESTED, counter), false);
138     assertEquals(2, nestedClasses.size());
139     Class<?> nestedClass1 = makeClass(NESTED, CLASSNAME1, counter);
140     assertTrue(nestedClasses.contains(nestedClass1));
141     Class<?> nestedClass2 = makeClass(NESTED, CLASSNAME2, counter);
142     assertTrue(nestedClasses.contains(nestedClass2));
143   }
144 
145   @Test
146   public void testClassFinderFiltersByNameInJar() throws Exception {
147     final long counter = testCounter.incrementAndGet();
148     final String classNamePrefix = name.getMethodName();
149     LOG.info("Created jar " + createAndLoadJar("", classNamePrefix, counter));
150 
151     ClassFinder.FileNameFilter notExcNameFilter = new ClassFinder.FileNameFilter() {
152       @Override
153       public boolean isCandidateFile(String fileName, String absFilePath) {
154         return !fileName.startsWith(PREFIX);
155       }
156     };
157     ClassFinder incClassesFinder = new ClassFinder(null, notExcNameFilter, null, classLoader);
158     Set<Class<?>> incClasses = incClassesFinder.findClasses(
159         makePackageName("", counter), false);
160     assertEquals(1, incClasses.size());
161     Class<?> incClass = makeClass("", classNamePrefix, counter);
162     assertTrue(incClasses.contains(incClass));
163   }
164 
165   @Test
166   public void testClassFinderFiltersByClassInJar() throws Exception {
167     final long counter = testCounter.incrementAndGet();
168     final String classNamePrefix = name.getMethodName();
169     LOG.info("Created jar " + createAndLoadJar("", classNamePrefix, counter));
170 
171     final ClassFinder.ClassFilter notExcClassFilter = new ClassFinder.ClassFilter() {
172       @Override
173       public boolean isCandidateClass(Class<?> c) {
174         return !c.getSimpleName().startsWith(PREFIX);
175       }
176     };
177     ClassFinder incClassesFinder = new ClassFinder(null, null, notExcClassFilter, classLoader);
178     Set<Class<?>> incClasses = incClassesFinder.findClasses(
179         makePackageName("", counter), false);
180     assertEquals(1, incClasses.size());
181     Class<?> incClass = makeClass("", classNamePrefix, counter);
182     assertTrue(incClasses.contains(incClass));
183   }
184 
185   private static String createAndLoadJar(final String packageNameSuffix,
186       final String classNamePrefix, final long counter) 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, classLoader);
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(classLoader);
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)) {
242         return true;
243       }
244     }
245     return false;
246   }
247 
248   @Test
249   public void testClassFinderFiltersByNameInDirs() throws Exception {
250     // Make some classes for us to find.  Class naming and packaging is kinda cryptic.
251     // TODO: Fix.
252     final long counter = testCounter.incrementAndGet();
253     final String classNamePrefix = name.getMethodName();
254     String pkgNameSuffix = name.getMethodName();
255     LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter));
256     final String classNameToFilterOut = classNamePrefix + counter;
257     final ClassFinder.FileNameFilter notThisFilter = new ClassFinder.FileNameFilter() {
258       @Override
259       public boolean isCandidateFile(String fileName, String absFilePath) {
260         return !fileName.equals(classNameToFilterOut + ".class");
261       }
262     };
263     String pkgName = makePackageName(pkgNameSuffix, counter);
264     ClassFinder allClassesFinder = new ClassFinder(classLoader);
265     Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false);
266     assertTrue("Classes in " + pkgName, allClasses.size() > 0);
267     ClassFinder notThisClassFinder = new ClassFinder(null, notThisFilter, null, classLoader);
268     Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(pkgName, false);
269     assertFalse(contains(notAllClasses, classNameToFilterOut));
270     assertEquals(allClasses.size() - 1, notAllClasses.size());
271   }
272 
273   @Test
274   public void testClassFinderFiltersByClassInDirs() throws Exception {
275     // Make some classes for us to find.  Class naming and packaging is kinda cryptic.
276     // TODO: Fix.
277     final long counter = testCounter.incrementAndGet();
278     final String classNamePrefix = name.getMethodName();
279     String pkgNameSuffix = name.getMethodName();
280     LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter));
281     final Class<?> clazz = makeClass(pkgNameSuffix, classNamePrefix, counter);
282     final ClassFinder.ClassFilter notThisFilter = new ClassFinder.ClassFilter() {
283       @Override
284       public boolean isCandidateClass(Class<?> c) {
285         return c != clazz;
286       }
287     };
288     String pkgName = makePackageName(pkgNameSuffix, counter);
289     ClassFinder allClassesFinder = new ClassFinder(classLoader);
290     Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false);
291     assertTrue("Classes in " + pkgName, allClasses.size() > 0);
292     ClassFinder notThisClassFinder = new ClassFinder(null, null, notThisFilter, classLoader);
293     Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(pkgName, false);
294     assertFalse(contains(notAllClasses, clazz.getSimpleName()));
295     assertEquals(allClasses.size() - 1, notAllClasses.size());
296   }
297 
298   @Test
299   public void testClassFinderFiltersByPathInDirs() throws Exception {
300     final String hardcodedThisSubdir = "hbase-common";
301     final ClassFinder.ResourcePathFilter notExcJarFilter = new ClassFinder.ResourcePathFilter() {
302       @Override
303       public boolean isCandidatePath(String resourcePath, boolean isJar) {
304         return isJar || !resourcePath.contains(hardcodedThisSubdir);
305       }
306     };
307     String thisPackage = this.getClass().getPackage().getName();
308     ClassFinder notThisClassFinder = new ClassFinder(notExcJarFilter, null, null, classLoader);
309     Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(thisPackage, false);
310     assertFalse(notAllClasses.contains(this.getClass()));
311   }
312 
313   @Test
314   public void testClassFinderDefaultsToOwnPackage() throws Exception {
315     // Correct handling of nested packages is tested elsewhere, so here we just assume
316     // pkgClasses is the correct answer that we don't have to check.
317     ClassFinder allClassesFinder = new ClassFinder(classLoader);
318     Set<Class<?>> pkgClasses = allClassesFinder.findClasses(
319         ClassFinder.class.getPackage().getName(), false);
320     Set<Class<?>> defaultClasses = allClassesFinder.findClasses(false);
321     assertArrayEquals(pkgClasses.toArray(), defaultClasses.toArray());
322   }
323 
324   private static class FileAndPath {
325     String path;
326     File file;
327     public FileAndPath(String path, File file) {
328       this.file = file;
329       this.path = path;
330     }
331   }
332 
333   private static Class<?> makeClass(String nestedPkgSuffix,
334       String className, long counter) throws ClassNotFoundException {
335     String name = makePackageName(nestedPkgSuffix, counter) + "." + className + counter;
336     return Class.forName(name, true, classLoader);
337   }
338 
339   private static String makePackageName(String nestedSuffix, long counter) {
340     return BASEPKG + counter + nestedSuffix;
341   }
342 
343   /**
344    * Compiles the test class with bogus code into a .class file.
345    * Unfortunately it's very tedious.
346    * @param counter Unique test counter.
347    * @param packageNameSuffix Package name suffix (e.g. ".suffix") for nesting, or "".
348    * @return The resulting .class file and the location in jar it is supposed to go to.
349    */
350   private static FileAndPath compileTestClass(long counter,
351       String packageNameSuffix, String classNamePrefix) throws Exception {
352     classNamePrefix = classNamePrefix + counter;
353     String packageName = makePackageName(packageNameSuffix, counter);
354     String javaPath = basePath + classNamePrefix + ".java";
355     String classPath = basePath + classNamePrefix + ".class";
356     PrintStream source = new PrintStream(javaPath);
357     source.println("package " + packageName + ";");
358     source.println("public class " + classNamePrefix
359         + " { public static void main(String[] args) { } };");
360     source.close();
361     JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
362     int result = jc.run(null, null, null, javaPath);
363     assertEquals(0, result);
364     File classFile = new File(classPath);
365     assertTrue(classFile.exists());
366     return new FileAndPath(packageName.replace('.', '/') + '/', classFile);
367   }
368 
369   /**
370    * Makes a jar out of some class files. Unfortunately it's very tedious.
371    * @param filesInJar Files created via compileTestClass.
372    * @return path to the resulting jar file.
373    */
374   private static String packageAndLoadJar(FileAndPath... filesInJar) throws Exception {
375     // First, write the bogus jar file.
376     String path = basePath + "jar" + jarCounter.incrementAndGet() + ".jar";
377     Manifest manifest = new Manifest();
378     manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
379     FileOutputStream fos = new FileOutputStream(path);
380     JarOutputStream jarOutputStream = new JarOutputStream(fos, manifest);
381     // Directory entries for all packages have to be added explicitly for
382     // resources to be findable via ClassLoader. Directory entries must end
383     // with "/"; the initial one is expected to, also.
384     Set<String> pathsInJar = new HashSet<>();
385     for (FileAndPath fileAndPath : filesInJar) {
386       String pathToAdd = fileAndPath.path;
387       while (pathsInJar.add(pathToAdd)) {
388         int ix = pathToAdd.lastIndexOf('/', pathToAdd.length() - 2);
389         if (ix < 0) {
390           break;
391         }
392         pathToAdd = pathToAdd.substring(0, ix);
393       }
394     }
395     for (String pathInJar : pathsInJar) {
396       jarOutputStream.putNextEntry(new JarEntry(pathInJar));
397       jarOutputStream.closeEntry();
398     }
399     for (FileAndPath fileAndPath : filesInJar) {
400       File file = fileAndPath.file;
401       jarOutputStream.putNextEntry(
402           new JarEntry(fileAndPath.path + file.getName()));
403       byte[] allBytes = new byte[(int)file.length()];
404       FileInputStream fis = new FileInputStream(file);
405       fis.read(allBytes);
406       fis.close();
407       jarOutputStream.write(allBytes);
408       jarOutputStream.closeEntry();
409     }
410     jarOutputStream.close();
411     fos.close();
412 
413     // Add the file to classpath.
414     File jarFile = new File(path);
415     assertTrue(jarFile.exists());
416     classLoader.addURL(jarFile.toURI().toURL());
417     return jarFile.getAbsolutePath();
418   }
419 
420   // Java 11 workaround - Custom class loader to expose addUrl method of URLClassLoader
421   private static class CustomClassloader extends URLClassLoader {
422     public CustomClassloader(URL[] urls, ClassLoader parentLoader) {
423       super(urls, parentLoader);
424     }
425 
426     public void addURL(URL url) {
427       super.addURL(url);
428     }
429   }
430 }