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