001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase;
019
020import static org.junit.Assert.assertArrayEquals;
021import static org.junit.Assert.assertEquals;
022import static org.junit.Assert.assertFalse;
023import static org.junit.Assert.assertTrue;
024
025import java.io.File;
026import java.io.FileInputStream;
027import java.io.FileOutputStream;
028import java.io.IOException;
029import java.io.PrintStream;
030import java.lang.reflect.Method;
031import java.net.URL;
032import java.net.URLClassLoader;
033import java.util.HashSet;
034import java.util.Set;
035import java.util.concurrent.atomic.AtomicLong;
036import java.util.jar.Attributes;
037import java.util.jar.JarEntry;
038import java.util.jar.JarOutputStream;
039import java.util.jar.Manifest;
040import javax.tools.JavaCompiler;
041import javax.tools.ToolProvider;
042import org.apache.hadoop.hbase.testclassification.MiscTests;
043import org.apache.hadoop.hbase.testclassification.SmallTests;
044import org.junit.AfterClass;
045import org.junit.BeforeClass;
046import org.junit.ClassRule;
047import org.junit.Rule;
048import org.junit.Test;
049import org.junit.experimental.categories.Category;
050import org.junit.rules.TestName;
051import org.slf4j.Logger;
052import org.slf4j.LoggerFactory;
053
054@Category({MiscTests.class, SmallTests.class})
055public class TestClassFinder {
056
057  @ClassRule
058  public static final HBaseClassTestRule CLASS_RULE =
059      HBaseClassTestRule.forClass(TestClassFinder.class);
060
061  private static final Logger LOG = LoggerFactory.getLogger(TestClassFinder.class);
062
063  @Rule public TestName name = new TestName();
064  private static final HBaseCommonTestingUtility testUtil = new HBaseCommonTestingUtility();
065  private static final String BASEPKG = "tfcpkg";
066  private static final String PREFIX = "Prefix";
067
068  // Use unique jar/class/package names in each test case with the help
069  // of these global counters; we are mucking with ClassLoader in this test
070  // and we don't want individual test cases to conflict via it.
071  private static AtomicLong testCounter = new AtomicLong(0);
072  private static AtomicLong jarCounter = new AtomicLong(0);
073
074  private static String basePath = null;
075
076  @BeforeClass
077  public static void createTestDir() throws IOException {
078    basePath = testUtil.getDataTestDir(TestClassFinder.class.getSimpleName()).toString();
079    if (!basePath.endsWith("/")) {
080      basePath += "/";
081    }
082    // Make sure we get a brand new directory.
083    File testDir = new File(basePath);
084    if (testDir.exists()) {
085      deleteTestDir();
086    }
087    assertTrue(testDir.mkdirs());
088    LOG.info("Using new, clean directory=" + testDir);
089  }
090
091  @AfterClass
092  public static void deleteTestDir() throws IOException {
093    testUtil.cleanupTestDir(TestClassFinder.class.getSimpleName());
094  }
095
096  @Test
097  public void testClassFinderCanFindClassesInJars() throws Exception {
098    long counter = testCounter.incrementAndGet();
099    FileAndPath c1 = compileTestClass(counter, "", "c1");
100    FileAndPath c2 = compileTestClass(counter, ".nested", "c2");
101    FileAndPath c3 = compileTestClass(counter, "", "c3");
102    packageAndLoadJar(c1, c3);
103    packageAndLoadJar(c2);
104
105    ClassFinder allClassesFinder = new ClassFinder();
106    Set<Class<?>> allClasses = allClassesFinder.findClasses(
107        makePackageName("", counter), false);
108    assertEquals(3, allClasses.size());
109  }
110
111  @Test
112  public void testClassFinderHandlesConflicts() throws Exception {
113    long counter = testCounter.incrementAndGet();
114    FileAndPath c1 = compileTestClass(counter, "", "c1");
115    FileAndPath c2 = compileTestClass(counter, "", "c2");
116    packageAndLoadJar(c1, c2);
117    packageAndLoadJar(c1);
118
119    ClassFinder allClassesFinder = new ClassFinder();
120    Set<Class<?>> allClasses = allClassesFinder.findClasses(
121        makePackageName("", counter), false);
122    assertEquals(2, allClasses.size());
123  }
124
125  @Test
126  public void testClassFinderHandlesNestedPackages() throws Exception {
127    final String NESTED = ".nested";
128    final String CLASSNAME1 = name.getMethodName() + "1";
129    final String CLASSNAME2 = name.getMethodName() + "2";
130    long counter = testCounter.incrementAndGet();
131    FileAndPath c1 = compileTestClass(counter, "", "c1");
132    FileAndPath c2 = compileTestClass(counter, NESTED, CLASSNAME1);
133    FileAndPath c3 = compileTestClass(counter, NESTED, CLASSNAME2);
134    packageAndLoadJar(c1, c2);
135    packageAndLoadJar(c3);
136
137    ClassFinder allClassesFinder = new ClassFinder();
138    Set<Class<?>> nestedClasses = allClassesFinder.findClasses(
139        makePackageName(NESTED, counter), false);
140    assertEquals(2, nestedClasses.size());
141    Class<?> nestedClass1 = makeClass(NESTED, CLASSNAME1, counter);
142    assertTrue(nestedClasses.contains(nestedClass1));
143    Class<?> nestedClass2 = makeClass(NESTED, CLASSNAME2, counter);
144    assertTrue(nestedClasses.contains(nestedClass2));
145  }
146
147  @Test
148  public void testClassFinderFiltersByNameInJar() throws Exception {
149    final long counter = testCounter.incrementAndGet();
150    final String classNamePrefix = name.getMethodName();
151    LOG.info("Created jar " + createAndLoadJar("", classNamePrefix, counter));
152
153    ClassFinder.FileNameFilter notExcNameFilter = new ClassFinder.FileNameFilter() {
154      @Override
155      public boolean isCandidateFile(String fileName, String absFilePath) {
156        return !fileName.startsWith(PREFIX);
157      }
158    };
159    ClassFinder incClassesFinder = new ClassFinder(null, notExcNameFilter, null);
160    Set<Class<?>> incClasses = incClassesFinder.findClasses(
161        makePackageName("", counter), false);
162    assertEquals(1, incClasses.size());
163    Class<?> incClass = makeClass("", classNamePrefix, counter);
164    assertTrue(incClasses.contains(incClass));
165  }
166
167  @Test
168  public void testClassFinderFiltersByClassInJar() throws Exception {
169    final long counter = testCounter.incrementAndGet();
170    final String classNamePrefix = name.getMethodName();
171    LOG.info("Created jar " + createAndLoadJar("", classNamePrefix, counter));
172
173    final ClassFinder.ClassFilter notExcClassFilter = new ClassFinder.ClassFilter() {
174      @Override
175      public boolean isCandidateClass(Class<?> c) {
176        return !c.getSimpleName().startsWith(PREFIX);
177      }
178    };
179    ClassFinder incClassesFinder = new ClassFinder(null, null, notExcClassFilter);
180    Set<Class<?>> incClasses = incClassesFinder.findClasses(
181        makePackageName("", counter), false);
182    assertEquals(1, incClasses.size());
183    Class<?> incClass = makeClass("", classNamePrefix, counter);
184    assertTrue(incClasses.contains(incClass));
185  }
186
187  private static String createAndLoadJar(final String packageNameSuffix,
188      final String classNamePrefix, final long counter)
189  throws Exception {
190    FileAndPath c1 = compileTestClass(counter, packageNameSuffix, classNamePrefix);
191    FileAndPath c2 = compileTestClass(counter, packageNameSuffix, PREFIX + "1");
192    FileAndPath c3 = compileTestClass(counter, packageNameSuffix, PREFIX + classNamePrefix + "2");
193    return packageAndLoadJar(c1, c2, c3);
194  }
195
196  @Test
197  public void testClassFinderFiltersByPathInJar() throws Exception {
198    final String CLASSNAME = name.getMethodName();
199    long counter = testCounter.incrementAndGet();
200    FileAndPath c1 = compileTestClass(counter, "", CLASSNAME);
201    FileAndPath c2 = compileTestClass(counter, "", "c2");
202    packageAndLoadJar(c1);
203    final String excludedJar = packageAndLoadJar(c2);
204    /* ResourcePathFilter will pass us the resourcePath as a path of a
205     * URL from the classloader. For Windows, the ablosute path and the
206     * one from the URL have different file separators.
207     */
208    final String excludedJarResource =
209      new File(excludedJar).toURI().getRawSchemeSpecificPart();
210
211    final ClassFinder.ResourcePathFilter notExcJarFilter =
212        new ClassFinder.ResourcePathFilter() {
213      @Override
214      public boolean isCandidatePath(String resourcePath, boolean isJar) {
215        return !isJar || !resourcePath.equals(excludedJarResource);
216      }
217    };
218    ClassFinder incClassesFinder = new ClassFinder(notExcJarFilter, null, null);
219    Set<Class<?>> incClasses = incClassesFinder.findClasses(
220        makePackageName("", counter), false);
221    assertEquals(1, incClasses.size());
222    Class<?> incClass = makeClass("", CLASSNAME, counter);
223    assertTrue(incClasses.contains(incClass));
224  }
225
226  @Test
227  public void testClassFinderCanFindClassesInDirs() throws Exception {
228    // Make some classes for us to find.  Class naming and packaging is kinda cryptic.
229    // TODO: Fix.
230    final long counter = testCounter.incrementAndGet();
231    final String classNamePrefix = name.getMethodName();
232    String pkgNameSuffix = name.getMethodName();
233    LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter));
234    ClassFinder allClassesFinder = new ClassFinder();
235    String pkgName = makePackageName(pkgNameSuffix, counter);
236    Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false);
237    assertTrue("Classes in " + pkgName, allClasses.size() > 0);
238    String classNameToFind = classNamePrefix + counter;
239    assertTrue(contains(allClasses, classNameToFind));
240  }
241
242  private static boolean contains(final Set<Class<?>> classes, final String simpleName) {
243    for (Class<?> c: classes) {
244      if (c.getSimpleName().equals(simpleName)) return true;
245    }
246    return false;
247  }
248
249  @Test
250  public void testClassFinderFiltersByNameInDirs() throws Exception {
251    // Make some classes for us to find.  Class naming and packaging is kinda cryptic.
252    // TODO: Fix.
253    final long counter = testCounter.incrementAndGet();
254    final String classNamePrefix = name.getMethodName();
255    String pkgNameSuffix = name.getMethodName();
256    LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter));
257    final String classNameToFilterOut = classNamePrefix + counter;
258    final ClassFinder.FileNameFilter notThisFilter = new ClassFinder.FileNameFilter() {
259      @Override
260      public boolean isCandidateFile(String fileName, String absFilePath) {
261        return !fileName.equals(classNameToFilterOut + ".class");
262      }
263    };
264    String pkgName = makePackageName(pkgNameSuffix, counter);
265    ClassFinder allClassesFinder = new ClassFinder();
266    Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false);
267    assertTrue("Classes in " + pkgName, allClasses.size() > 0);
268    ClassFinder notThisClassFinder = new ClassFinder(null, notThisFilter, null);
269    Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(pkgName, false);
270    assertFalse(contains(notAllClasses, classNameToFilterOut));
271    assertEquals(allClasses.size() - 1, notAllClasses.size());
272  }
273
274  @Test
275  public void testClassFinderFiltersByClassInDirs() throws Exception {
276    // Make some classes for us to find.  Class naming and packaging is kinda cryptic.
277    // TODO: Fix.
278    final long counter = testCounter.incrementAndGet();
279    final String classNamePrefix = name.getMethodName();
280    String pkgNameSuffix = name.getMethodName();
281    LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter));
282    final Class<?> clazz = makeClass(pkgNameSuffix, classNamePrefix, counter);
283    final ClassFinder.ClassFilter notThisFilter = new ClassFinder.ClassFilter() {
284      @Override
285      public boolean isCandidateClass(Class<?> c) {
286        return c != clazz;
287      }
288    };
289    String pkgName = makePackageName(pkgNameSuffix, counter);
290    ClassFinder allClassesFinder = new ClassFinder();
291    Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false);
292    assertTrue("Classes in " + pkgName, allClasses.size() > 0);
293    ClassFinder notThisClassFinder = new ClassFinder(null, null, notThisFilter);
294    Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(pkgName, false);
295    assertFalse(contains(notAllClasses, clazz.getSimpleName()));
296    assertEquals(allClasses.size() - 1, notAllClasses.size());
297  }
298
299  @Test
300  public void testClassFinderFiltersByPathInDirs() throws Exception {
301    final String hardcodedThisSubdir = "hbase-common";
302    final ClassFinder.ResourcePathFilter notExcJarFilter =
303        new ClassFinder.ResourcePathFilter() {
304      @Override
305      public boolean isCandidatePath(String resourcePath, boolean isJar) {
306        return isJar || !resourcePath.contains(hardcodedThisSubdir);
307      }
308    };
309    String thisPackage = this.getClass().getPackage().getName();
310    ClassFinder notThisClassFinder = new ClassFinder(notExcJarFilter, null, null);
311    Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(thisPackage, false);
312    assertFalse(notAllClasses.contains(this.getClass()));
313  }
314
315  @Test
316  public void testClassFinderDefaultsToOwnPackage() throws Exception {
317    // Correct handling of nested packages is tested elsewhere, so here we just assume
318    // pkgClasses is the correct answer that we don't have to check.
319    ClassFinder allClassesFinder = new ClassFinder();
320    Set<Class<?>> pkgClasses = allClassesFinder.findClasses(
321        ClassFinder.class.getPackage().getName(), false);
322    Set<Class<?>> defaultClasses = allClassesFinder.findClasses(false);
323    assertArrayEquals(pkgClasses.toArray(), defaultClasses.toArray());
324  }
325
326  private static class FileAndPath {
327    String path;
328    File file;
329    public FileAndPath(String path, File file) {
330      this.file = file;
331      this.path = path;
332    }
333  }
334
335  private static Class<?> makeClass(String nestedPkgSuffix,
336      String className, long counter) throws ClassNotFoundException {
337    return Class.forName(
338        makePackageName(nestedPkgSuffix, counter) + "." + className + counter);
339  }
340
341  private static String makePackageName(String nestedSuffix, long counter) {
342    return BASEPKG + counter + nestedSuffix;
343  }
344
345  /**
346   * Compiles the test class with bogus code into a .class file.
347   * Unfortunately it's very tedious.
348   * @param counter Unique test counter.
349   * @param packageNameSuffix Package name suffix (e.g. ".suffix") for nesting, or "".
350   * @return The resulting .class file and the location in jar it is supposed to go to.
351   */
352  private static FileAndPath compileTestClass(long counter,
353      String packageNameSuffix, String classNamePrefix) throws Exception {
354    classNamePrefix = classNamePrefix + counter;
355    String packageName = makePackageName(packageNameSuffix, counter);
356    String javaPath = basePath + classNamePrefix + ".java";
357    String classPath = basePath + classNamePrefix + ".class";
358    PrintStream source = new PrintStream(javaPath);
359    source.println("package " + packageName + ";");
360    source.println("public class " + classNamePrefix
361        + " { public static void main(String[] args) { } };");
362    source.close();
363    JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
364    int result = jc.run(null, null, null, javaPath);
365    assertEquals(0, result);
366    File classFile = new File(classPath);
367    assertTrue(classFile.exists());
368    return new FileAndPath(packageName.replace('.', '/') + '/', classFile);
369  }
370
371  /**
372   * Makes a jar out of some class files. Unfortunately it's very tedious.
373   * @param filesInJar Files created via compileTestClass.
374   * @return path to the resulting jar file.
375   */
376  private static String packageAndLoadJar(FileAndPath... filesInJar) throws Exception {
377    // First, write the bogus jar file.
378    String path = basePath + "jar" + jarCounter.incrementAndGet() + ".jar";
379    Manifest manifest = new Manifest();
380    manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
381    FileOutputStream fos = new FileOutputStream(path);
382    JarOutputStream jarOutputStream = new JarOutputStream(fos, manifest);
383    // Directory entries for all packages have to be added explicitly for
384    // resources to be findable via ClassLoader. Directory entries must end
385    // with "/"; the initial one is expected to, also.
386    Set<String> pathsInJar = new HashSet<>();
387    for (FileAndPath fileAndPath : filesInJar) {
388      String pathToAdd = fileAndPath.path;
389      while (pathsInJar.add(pathToAdd)) {
390        int ix = pathToAdd.lastIndexOf('/', pathToAdd.length() - 2);
391        if (ix < 0) {
392          break;
393        }
394        pathToAdd = pathToAdd.substring(0, ix);
395      }
396    }
397    for (String pathInJar : pathsInJar) {
398      jarOutputStream.putNextEntry(new JarEntry(pathInJar));
399      jarOutputStream.closeEntry();
400    }
401    for (FileAndPath fileAndPath : filesInJar) {
402      File file = fileAndPath.file;
403      jarOutputStream.putNextEntry(
404          new JarEntry(fileAndPath.path + file.getName()));
405      byte[] allBytes = new byte[(int)file.length()];
406      FileInputStream fis = new FileInputStream(file);
407      fis.read(allBytes);
408      fis.close();
409      jarOutputStream.write(allBytes);
410      jarOutputStream.closeEntry();
411    }
412    jarOutputStream.close();
413    fos.close();
414
415    // Add the file to classpath.
416    File jarFile = new File(path);
417    assertTrue(jarFile.exists());
418    URLClassLoader urlClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
419    Method method = URLClassLoader.class
420        .getDeclaredMethod("addURL", new Class[] { URL.class });
421    method.setAccessible(true);
422    method.invoke(urlClassLoader, new Object[] { jarFile.toURI().toURL() });
423    return jarFile.getAbsolutePath();
424  }
425}