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.hamcrest.MatcherAssert.assertThat;
021import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
022import static org.junit.Assert.assertEquals;
023import static org.junit.Assert.assertFalse;
024import static org.junit.Assert.assertTrue;
025
026import java.io.File;
027import java.io.FileInputStream;
028import java.io.FileOutputStream;
029import java.io.IOException;
030import java.io.PrintStream;
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  @ClassRule
057  public static final HBaseClassTestRule CLASS_RULE =
058    HBaseClassTestRule.forClass(TestClassFinder.class);
059
060  private static final Logger LOG = LoggerFactory.getLogger(TestClassFinder.class);
061
062  @Rule
063  public TestName name = new TestName();
064  private static final HBaseCommonTestingUtil testUtil = new HBaseCommonTestingUtil();
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  private static CustomClassloader classLoader;
077
078  @BeforeClass
079  public static void createTestDir() throws IOException {
080    basePath = testUtil.getDataTestDir(TestClassFinder.class.getSimpleName()).toString();
081    if (!basePath.endsWith("/")) {
082      basePath += "/";
083    }
084    // Make sure we get a brand new directory.
085    File testDir = new File(basePath);
086    if (testDir.exists()) {
087      deleteTestDir();
088    }
089    assertTrue(testDir.mkdirs());
090    LOG.info("Using new, clean directory=" + testDir);
091
092    classLoader = new CustomClassloader(new URL[0], ClassLoader.getSystemClassLoader());
093  }
094
095  @AfterClass
096  public static void deleteTestDir() {
097    testUtil.cleanupTestDir(TestClassFinder.class.getSimpleName());
098  }
099
100  @Test
101  public void testClassFinderCanFindClassesInJars() throws Exception {
102    long counter = testCounter.incrementAndGet();
103    FileAndPath c1 = compileTestClass(counter, "", "c1");
104    FileAndPath c2 = compileTestClass(counter, ".nested", "c2");
105    FileAndPath c3 = compileTestClass(counter, "", "c3");
106    packageAndLoadJar(c1, c3);
107    packageAndLoadJar(c2);
108
109    ClassFinder allClassesFinder = new ClassFinder(classLoader);
110    Set<Class<?>> allClasses = allClassesFinder.findClasses(makePackageName("", counter), false);
111    assertEquals(3, allClasses.size());
112  }
113
114  @Test
115  public void testClassFinderHandlesConflicts() throws Exception {
116    long counter = testCounter.incrementAndGet();
117    FileAndPath c1 = compileTestClass(counter, "", "c1");
118    FileAndPath c2 = compileTestClass(counter, "", "c2");
119    packageAndLoadJar(c1, c2);
120    packageAndLoadJar(c1);
121
122    ClassFinder allClassesFinder = new ClassFinder(classLoader);
123    Set<Class<?>> allClasses = allClassesFinder.findClasses(makePackageName("", counter), false);
124    assertEquals(2, allClasses.size());
125  }
126
127  @Test
128  public void testClassFinderHandlesNestedPackages() throws Exception {
129    final String NESTED = ".nested";
130    final String CLASSNAME1 = name.getMethodName() + "1";
131    final String CLASSNAME2 = name.getMethodName() + "2";
132    long counter = testCounter.incrementAndGet();
133    FileAndPath c1 = compileTestClass(counter, "", "c1");
134    FileAndPath c2 = compileTestClass(counter, NESTED, CLASSNAME1);
135    FileAndPath c3 = compileTestClass(counter, NESTED, CLASSNAME2);
136    packageAndLoadJar(c1, c2);
137    packageAndLoadJar(c3);
138
139    ClassFinder allClassesFinder = new ClassFinder(classLoader);
140    Set<Class<?>> nestedClasses =
141      allClassesFinder.findClasses(makePackageName(NESTED, counter), false);
142    assertEquals(2, nestedClasses.size());
143    Class<?> nestedClass1 = makeClass(NESTED, CLASSNAME1, counter);
144    assertTrue(nestedClasses.contains(nestedClass1));
145    Class<?> nestedClass2 = makeClass(NESTED, CLASSNAME2, counter);
146    assertTrue(nestedClasses.contains(nestedClass2));
147  }
148
149  @Test
150  public void testClassFinderFiltersByNameInJar() throws Exception {
151    final long counter = testCounter.incrementAndGet();
152    final String classNamePrefix = name.getMethodName();
153    LOG.info("Created jar " + createAndLoadJar("", classNamePrefix, counter));
154
155    ClassFinder.FileNameFilter notExcNameFilter =
156      (fileName, absFilePath) -> !fileName.startsWith(PREFIX);
157    ClassFinder incClassesFinder = new ClassFinder(null, notExcNameFilter, null, classLoader);
158    Set<Class<?>> incClasses = incClassesFinder.findClasses(makePackageName("", counter), false);
159    assertEquals(1, incClasses.size());
160    Class<?> incClass = makeClass("", classNamePrefix, counter);
161    assertTrue(incClasses.contains(incClass));
162  }
163
164  @Test
165  public void testClassFinderFiltersByClassInJar() throws Exception {
166    final long counter = testCounter.incrementAndGet();
167    final String classNamePrefix = name.getMethodName();
168    LOG.info("Created jar " + createAndLoadJar("", classNamePrefix, counter));
169
170    final ClassFinder.ClassFilter notExcClassFilter = c -> !c.getSimpleName().startsWith(PREFIX);
171    ClassFinder incClassesFinder = new ClassFinder(null, null, notExcClassFilter, classLoader);
172    Set<Class<?>> incClasses = incClassesFinder.findClasses(makePackageName("", counter), false);
173    assertEquals(1, incClasses.size());
174    Class<?> incClass = makeClass("", classNamePrefix, counter);
175    assertTrue(incClasses.contains(incClass));
176  }
177
178  private static String createAndLoadJar(final String packageNameSuffix,
179    final String classNamePrefix, final long counter) throws Exception {
180    FileAndPath c1 = compileTestClass(counter, packageNameSuffix, classNamePrefix);
181    FileAndPath c2 = compileTestClass(counter, packageNameSuffix, PREFIX + "1");
182    FileAndPath c3 = compileTestClass(counter, packageNameSuffix, PREFIX + classNamePrefix + "2");
183    return packageAndLoadJar(c1, c2, c3);
184  }
185
186  @Test
187  public void testClassFinderFiltersByPathInJar() throws Exception {
188    final String CLASSNAME = name.getMethodName();
189    long counter = testCounter.incrementAndGet();
190    FileAndPath c1 = compileTestClass(counter, "", CLASSNAME);
191    FileAndPath c2 = compileTestClass(counter, "", "c2");
192    packageAndLoadJar(c1);
193    final String excludedJar = packageAndLoadJar(c2);
194    /*
195     * ResourcePathFilter will pass us the resourcePath as a path of a URL from the classloader. For
196     * Windows, the ablosute path and the one from the URL have different file separators.
197     */
198    final String excludedJarResource = new File(excludedJar).toURI().getRawSchemeSpecificPart();
199
200    final ClassFinder.ResourcePathFilter notExcJarFilter =
201      (resourcePath, isJar) -> !isJar || !resourcePath.equals(excludedJarResource);
202    ClassFinder incClassesFinder = new ClassFinder(notExcJarFilter, null, null, classLoader);
203    Set<Class<?>> incClasses = incClassesFinder.findClasses(makePackageName("", counter), false);
204    assertEquals(1, incClasses.size());
205    Class<?> incClass = makeClass("", CLASSNAME, counter);
206    assertTrue(incClasses.contains(incClass));
207  }
208
209  @Test
210  public void testClassFinderCanFindClassesInDirs() throws Exception {
211    // Make some classes for us to find. Class naming and packaging is kinda cryptic.
212    // TODO: Fix.
213    final long counter = testCounter.incrementAndGet();
214    final String classNamePrefix = name.getMethodName();
215    String pkgNameSuffix = name.getMethodName();
216    LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter));
217    ClassFinder allClassesFinder = new ClassFinder(classLoader);
218    String pkgName = makePackageName(pkgNameSuffix, counter);
219    Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false);
220    assertTrue("Classes in " + pkgName, allClasses.size() > 0);
221    String classNameToFind = classNamePrefix + counter;
222    assertTrue(contains(allClasses, classNameToFind));
223  }
224
225  private static boolean contains(final Set<Class<?>> classes, final String simpleName) {
226    for (Class<?> c : classes) {
227      if (c.getSimpleName().equals(simpleName)) {
228        return true;
229      }
230    }
231    return false;
232  }
233
234  @Test
235  public void testClassFinderFiltersByNameInDirs() throws Exception {
236    // Make some classes for us to find. Class naming and packaging is kinda cryptic.
237    // TODO: Fix.
238    final long counter = testCounter.incrementAndGet();
239    final String classNamePrefix = name.getMethodName();
240    String pkgNameSuffix = name.getMethodName();
241    LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter));
242    final String classNameToFilterOut = classNamePrefix + counter;
243    final ClassFinder.FileNameFilter notThisFilter =
244      (fileName, absFilePath) -> !fileName.equals(classNameToFilterOut + ".class");
245    String pkgName = makePackageName(pkgNameSuffix, counter);
246    ClassFinder allClassesFinder = new ClassFinder(classLoader);
247    Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false);
248    assertTrue("Classes in " + pkgName, allClasses.size() > 0);
249    ClassFinder notThisClassFinder = new ClassFinder(null, notThisFilter, null, classLoader);
250    Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(pkgName, false);
251    assertFalse(contains(notAllClasses, classNameToFilterOut));
252    assertEquals(allClasses.size() - 1, notAllClasses.size());
253  }
254
255  @Test
256  public void testClassFinderFiltersByClassInDirs() throws Exception {
257    // Make some classes for us to find. Class naming and packaging is kinda cryptic.
258    // TODO: Fix.
259    final long counter = testCounter.incrementAndGet();
260    final String classNamePrefix = name.getMethodName();
261    String pkgNameSuffix = name.getMethodName();
262    LOG.info("Created jar " + createAndLoadJar(pkgNameSuffix, classNamePrefix, counter));
263    final Class<?> clazz = makeClass(pkgNameSuffix, classNamePrefix, counter);
264    final ClassFinder.ClassFilter notThisFilter = c -> c != clazz;
265    String pkgName = makePackageName(pkgNameSuffix, counter);
266    ClassFinder allClassesFinder = new ClassFinder(classLoader);
267    Set<Class<?>> allClasses = allClassesFinder.findClasses(pkgName, false);
268    assertTrue("Classes in " + pkgName, allClasses.size() > 0);
269    ClassFinder notThisClassFinder = new ClassFinder(null, null, notThisFilter, classLoader);
270    Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(pkgName, false);
271    assertFalse(contains(notAllClasses, clazz.getSimpleName()));
272    assertEquals(allClasses.size() - 1, notAllClasses.size());
273  }
274
275  @Test
276  public void testClassFinderFiltersByPathInDirs() throws Exception {
277    final String hardcodedThisSubdir = "hbase-common";
278    final ClassFinder.ResourcePathFilter notExcJarFilter =
279      (resourcePath, isJar) -> isJar || !resourcePath.contains(hardcodedThisSubdir);
280    String thisPackage = this.getClass().getPackage().getName();
281    ClassFinder notThisClassFinder = new ClassFinder(notExcJarFilter, null, null, classLoader);
282    Set<Class<?>> notAllClasses = notThisClassFinder.findClasses(thisPackage, false);
283    assertFalse(notAllClasses.contains(this.getClass()));
284  }
285
286  @Test
287  public void testClassFinderDefaultsToOwnPackage() throws Exception {
288    // Correct handling of nested packages is tested elsewhere, so here we just assume
289    // pkgClasses is the correct answer that we don't have to check.
290    ClassFinder allClassesFinder = new ClassFinder(classLoader);
291    Set<Class<?>> pkgClasses =
292      allClassesFinder.findClasses(ClassFinder.class.getPackage().getName(), false);
293    Set<Class<?>> defaultClasses = allClassesFinder.findClasses(false);
294    Object[] pkgClassesArray = pkgClasses.toArray();
295    Object[] defaultClassesArray = defaultClasses.toArray();
296    assertEquals(pkgClassesArray.length, defaultClassesArray.length);
297    assertThat(pkgClassesArray, arrayContainingInAnyOrder(defaultClassesArray));
298  }
299
300  private static class FileAndPath {
301    String path;
302    File file;
303
304    public FileAndPath(String path, File file) {
305      this.file = file;
306      this.path = path;
307    }
308  }
309
310  private static Class<?> makeClass(String nestedPkgSuffix, String className, long counter)
311    throws ClassNotFoundException {
312    String name = makePackageName(nestedPkgSuffix, counter) + "." + className + counter;
313    return Class.forName(name, true, classLoader);
314  }
315
316  private static String makePackageName(String nestedSuffix, long counter) {
317    return BASEPKG + counter + nestedSuffix;
318  }
319
320  /**
321   * Compiles the test class with bogus code into a .class file. Unfortunately it's very tedious.
322   * @param counter           Unique test counter.
323   * @param packageNameSuffix Package name suffix (e.g. ".suffix") for nesting, or "".
324   * @return The resulting .class file and the location in jar it is supposed to go to.
325   */
326  private static FileAndPath compileTestClass(long counter, String packageNameSuffix,
327    String classNamePrefix) throws Exception {
328    classNamePrefix = classNamePrefix + counter;
329    String packageName = makePackageName(packageNameSuffix, counter);
330    String javaPath = basePath + classNamePrefix + ".java";
331    String classPath = basePath + classNamePrefix + ".class";
332    PrintStream source = new PrintStream(javaPath);
333    source.println("package " + packageName + ";");
334    source.println(
335      "public class " + classNamePrefix + " { public static void main(String[] args) { } };");
336    source.close();
337    JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
338    int result = jc.run(null, null, null, javaPath);
339    assertEquals(0, result);
340    File classFile = new File(classPath);
341    assertTrue(classFile.exists());
342    return new FileAndPath(packageName.replace('.', '/') + '/', classFile);
343  }
344
345  /**
346   * Makes a jar out of some class files. Unfortunately it's very tedious.
347   * @param filesInJar Files created via compileTestClass.
348   * @return path to the resulting jar file.
349   */
350  private static String packageAndLoadJar(FileAndPath... filesInJar) throws Exception {
351    // First, write the bogus jar file.
352    String path = basePath + "jar" + jarCounter.incrementAndGet() + ".jar";
353    Manifest manifest = new Manifest();
354    manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
355    FileOutputStream fos = new FileOutputStream(path);
356    JarOutputStream jarOutputStream = new JarOutputStream(fos, manifest);
357    // Directory entries for all packages have to be added explicitly for
358    // resources to be findable via ClassLoader. Directory entries must end
359    // with "/"; the initial one is expected to, also.
360    Set<String> pathsInJar = new HashSet<>();
361    for (FileAndPath fileAndPath : filesInJar) {
362      String pathToAdd = fileAndPath.path;
363      while (pathsInJar.add(pathToAdd)) {
364        int ix = pathToAdd.lastIndexOf('/', pathToAdd.length() - 2);
365        if (ix < 0) {
366          break;
367        }
368        pathToAdd = pathToAdd.substring(0, ix);
369      }
370    }
371    for (String pathInJar : pathsInJar) {
372      jarOutputStream.putNextEntry(new JarEntry(pathInJar));
373      jarOutputStream.closeEntry();
374    }
375    for (FileAndPath fileAndPath : filesInJar) {
376      File file = fileAndPath.file;
377      jarOutputStream.putNextEntry(new JarEntry(fileAndPath.path + file.getName()));
378      byte[] allBytes = new byte[(int) file.length()];
379      FileInputStream fis = new FileInputStream(file);
380      fis.read(allBytes);
381      fis.close();
382      jarOutputStream.write(allBytes);
383      jarOutputStream.closeEntry();
384    }
385    jarOutputStream.close();
386    fos.close();
387
388    // Add the file to classpath.
389    File jarFile = new File(path);
390    assertTrue(jarFile.exists());
391    classLoader.addURL(jarFile.toURI().toURL());
392    return jarFile.getAbsolutePath();
393  }
394
395  // Java 11 workaround - Custom class loader to expose addUrl method of URLClassLoader
396  private static class CustomClassloader extends URLClassLoader {
397    public CustomClassloader(URL[] urls, ClassLoader parentLoader) {
398      super(urls, parentLoader);
399    }
400
401    @Override
402    public void addURL(URL url) {
403      super.addURL(url);
404    }
405  }
406}