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