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