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.util;
019
020import static org.junit.Assert.assertFalse;
021import static org.junit.Assert.assertTrue;
022import static org.junit.Assert.fail;
023
024import java.io.BufferedWriter;
025import java.io.File;
026import java.io.FileInputStream;
027import java.io.FileOutputStream;
028import java.nio.charset.StandardCharsets;
029import java.nio.file.Files;
030import java.util.ArrayList;
031import java.util.List;
032import java.util.jar.JarEntry;
033import java.util.jar.JarOutputStream;
034import java.util.jar.Manifest;
035import javax.tools.JavaCompiler;
036import javax.tools.JavaFileObject;
037import javax.tools.StandardJavaFileManager;
038import javax.tools.ToolProvider;
039import org.apache.hadoop.conf.Configuration;
040import org.apache.hadoop.fs.Path;
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043
044/**
045 * Some utilities to help class loader testing
046 */
047public final class ClassLoaderTestHelper {
048  private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderTestHelper.class);
049
050  private static final int BUFFER_SIZE = 4096;
051
052  private ClassLoaderTestHelper() {
053  }
054
055  /**
056   * Jar a list of files into a jar archive.
057   * @param archiveFile the target jar archive
058   * @param tobeJared   a list of files to be jared
059   * @return true if a jar archive is build, false otherwise
060   */
061  private static boolean createJarArchive(File archiveFile, File[] tobeJared) {
062    try {
063      byte[] buffer = new byte[BUFFER_SIZE];
064      // Open archive file
065      FileOutputStream stream = new FileOutputStream(archiveFile);
066      JarOutputStream out = new JarOutputStream(stream, new Manifest());
067
068      for (File file : tobeJared) {
069        if (file == null || !file.exists() || file.isDirectory()) {
070          continue;
071        }
072
073        // Add archive entry
074        JarEntry jarAdd = new JarEntry(file.getName());
075        jarAdd.setTime(file.lastModified());
076        out.putNextEntry(jarAdd);
077
078        // Write file to archive
079        FileInputStream in = new FileInputStream(file);
080        while (true) {
081          int nRead = in.read(buffer, 0, buffer.length);
082          if (nRead <= 0) {
083            break;
084          }
085
086          out.write(buffer, 0, nRead);
087        }
088        in.close();
089      }
090      out.close();
091      stream.close();
092      LOG.info("Adding classes to jar file completed");
093      return true;
094    } catch (Exception ex) {
095      LOG.error("Error: " + ex.getMessage());
096      return false;
097    }
098  }
099
100  /**
101   * Create a test jar for testing purpose for a given class name with specified code string: save
102   * the class to a file, compile it, and jar it up. If the code string passed in is null, a bare
103   * empty class will be created and used.
104   * @param testDir   the folder under which to store the test class and jar
105   * @param className the test class name
106   * @param code      the optional test class code, which can be null. If null, a bare empty class
107   *                  will be used
108   * @return the test jar file generated
109   */
110  public static File buildJar(String testDir, String className, String code) throws Exception {
111    return buildJar(testDir, className, code, testDir);
112  }
113
114  /**
115   * Create a test jar for testing purpose for a given class name with specified code string.
116   * @param testDir   the folder under which to store the test class
117   * @param className the test class name
118   * @param code      the optional test class code, which can be null. If null, an empty class will
119   *                  be used
120   * @param folder    the folder under which to store the generated jar
121   * @return the test jar file generated
122   */
123  public static File buildJar(String testDir, String className, String code, String folder)
124    throws Exception {
125    String javaCode = code != null ? code : "public class " + className + " {}";
126    Path srcDir = new Path(testDir, "src");
127    File srcDirPath = new File(srcDir.toString());
128    srcDirPath.mkdirs();
129    File sourceCodeFile = new File(srcDir.toString(), className + ".java");
130    BufferedWriter bw = Files.newBufferedWriter(sourceCodeFile.toPath(), StandardCharsets.UTF_8);
131    bw.write(javaCode);
132    bw.close();
133
134    // compile it by JavaCompiler
135    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
136    StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null);
137    Iterable<? extends JavaFileObject> cu = fm.getJavaFileObjects(sourceCodeFile);
138    List<String> options = new ArrayList<>(2);
139    options.add("-classpath");
140    // only add hbase classes to classpath. This is a little bit tricky: assume
141    // the classpath is {hbaseSrc}/target/classes.
142    String currentDir = new File(".").getAbsolutePath();
143    String classpath = currentDir + File.separator + "target" + File.separator + "classes"
144      + System.getProperty("path.separator") + System.getProperty("java.class.path")
145      + System.getProperty("path.separator") + System.getProperty("surefire.test.class.path");
146
147    options.add(classpath);
148    LOG.debug("Setting classpath to: " + classpath);
149
150    JavaCompiler.CompilationTask task = compiler.getTask(null, fm, null, options, null, cu);
151    assertTrue("Compile file " + sourceCodeFile + " failed.", task.call());
152
153    // build a jar file by the classes files
154    String jarFileName = className + ".jar";
155    File jarFile = new File(folder, jarFileName);
156    jarFile.getParentFile().mkdirs();
157    if (
158      !createJarArchive(jarFile, new File[] { new File(srcDir.toString(), className + ".class") })
159    ) {
160      fail("Build jar file failed.");
161    }
162    return jarFile;
163  }
164
165  /**
166   * Add a list of jar files to another jar file under a specific folder. It is used to generated
167   * coprocessor jar files which can be loaded by the coprocessor class loader. It is for testing
168   * usage only so we don't be so careful about stream closing in case any exception.
169   * @param targetJar the target jar file
170   * @param libPrefix the folder where to put inner jar files
171   * @param srcJars   the source inner jar files to be added
172   * @throws Exception if anything doesn't work as expected
173   */
174  public static void addJarFilesToJar(File targetJar, String libPrefix, File... srcJars)
175    throws Exception {
176    FileOutputStream stream = new FileOutputStream(targetJar);
177    JarOutputStream out = new JarOutputStream(stream, new Manifest());
178    byte[] buffer = new byte[BUFFER_SIZE];
179
180    for (File jarFile : srcJars) {
181      // Add archive entry
182      JarEntry jarAdd = new JarEntry(libPrefix + jarFile.getName());
183      jarAdd.setTime(jarFile.lastModified());
184      out.putNextEntry(jarAdd);
185
186      // Write file to archive
187      FileInputStream in = new FileInputStream(jarFile);
188      while (true) {
189        int nRead = in.read(buffer, 0, buffer.length);
190        if (nRead <= 0) {
191          break;
192        }
193
194        out.write(buffer, 0, nRead);
195      }
196      in.close();
197    }
198    out.close();
199    stream.close();
200    LOG.info("Adding jar file to outer jar file completed");
201  }
202
203  public static String localDirPath(Configuration conf) {
204    return conf.get(ClassLoaderBase.LOCAL_DIR_KEY) + File.separator + "jars" + File.separator;
205  }
206
207  public static void deleteClass(String className, String testDir, Configuration conf)
208    throws Exception {
209    String jarFileName = className + ".jar";
210    File file = new File(testDir, jarFileName);
211    file.delete();
212    assertFalse("Should be deleted: " + file.getPath(), file.exists());
213
214    file = new File(conf.get("hbase.dynamic.jars.dir"), jarFileName);
215    file.delete();
216    assertFalse("Should be deleted: " + file.getPath(), file.exists());
217
218    file = new File(ClassLoaderTestHelper.localDirPath(conf), jarFileName);
219    file.delete();
220    assertFalse("Should be deleted: " + file.getPath(), file.exists());
221  }
222}