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