blob: 169219055e57b813a3ae7277b73dfc9a3a767590 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.geode.test.compiler;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.ToolProvider;
import org.apache.commons.io.IOUtils;
/**
* Test framework utility class to programmatically create classes, JARs and ClassLoaders that
* include the classes.
*
* @since GemFire 7.0
*/
@SuppressWarnings("serial")
public class ClassBuilder implements Serializable {
private String classPath = System.getProperty("java.class.path");
/**
* Write a JAR with an empty class using the given class name. The className may have a package
* separated by /. For example: my/package/myclass
*
* @param className Name of the class to create
* @param outputFile Where to write the JAR file
* @throws IOException If there is a problem creating the output stream for the JAR file.
*/
public void writeJarFromName(final String className, final File outputFile) throws IOException {
writeJarFromContent(className, "public class " + className + "{}", outputFile);
}
/**
* Write a JAR with a class of the given name with the provided content. The className may have a
* package separated by /. For example: my/package/myclass
*
* @param className Name of the class to create
* @param content Content of the created class
* @param outputFile Where to write the JAR file
* @throws IOException If there is a problem writing the JAR file.
*/
public void writeJarFromContent(final String className, final String content,
final File outputFile) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
writeJarFromContent(className, content, fileOutputStream);
fileOutputStream.close();
}
/**
* Create a JAR with an empty class using the given class name. The className may have a package
* separated by /. For example: my/package/myclass
*
* @param className Name of the class to create
* @return The JAR file contents
* @throws IOException If there is a problem creating the output stream for the JAR file.
*/
public byte[] createJarFromName(final String className) throws IOException {
return createJarFromClassContent(className, "public class " + className + "{}");
}
/**
* Create a JAR using the given file contents and with the given file name.
*
* @param fileName Name of the file to create
* @param content Content of the created file
* @return The JAR file content
* @throws IOException If there is a problem creating the output stream for the JAR file.
*/
public byte[] createJarFromFileContent(final String fileName, final String content)
throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
JarOutputStream jarOutputStream = new JarOutputStream(byteArrayOutputStream);
JarEntry entry = new JarEntry(fileName);
entry.setTime(System.currentTimeMillis());
jarOutputStream.putNextEntry(entry);
jarOutputStream.write(content.getBytes());
jarOutputStream.closeEntry();
jarOutputStream.close();
return byteArrayOutputStream.toByteArray();
}
/**
* Create a JAR using the given class contents and with the given class name.
*
* @param className Name of the class to create
* @param content Content of the created class
* @return The JAR file content
* @throws IOException If there is a problem creating the output stream for the JAR file.
*/
public byte[] createJarFromClassContent(final String className, final String content)
throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
writeJarFromContent(className, content, byteArrayOutputStream);
return byteArrayOutputStream.toByteArray();
}
/**
* Write a JAR with a class of the given name with the provided content. The className may have a
* package separated by /. For example: my/package/myclass
*
* @param className Name of the class to create
* @param content Content of the created class
* @param outStream Stream to write the JAR to
* @throws IOException If there is a problem creating the output stream for the JAR file.
*/
public void writeJarFromContent(final String className, final String content,
final OutputStream outStream) throws IOException {
byte[] bytes = compileClass(className, content);
createJar(className, outStream, bytes);
return;
}
private void createJar(String className, OutputStream outStream, byte[] bytes)
throws IOException {
JarOutputStream jarOutputStream = new JarOutputStream(outStream);
// Add the class file to the JAR file
String formattedName = className;
if (!formattedName.endsWith(".class")) {
formattedName = formattedName.concat(".class");
}
JarEntry entry = new JarEntry(formattedName);
entry.setTime(System.currentTimeMillis());
jarOutputStream.putNextEntry(entry);
jarOutputStream.write(bytes);
jarOutputStream.closeEntry();
jarOutputStream.close();
}
public static void writeJarFromClasses(File jar, Class... types) throws IOException {
try (JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(jar))) {
for (Class type : types) {
String className = type.getName();
String classAsPath = className.replace('.', '/') + ".class";
InputStream stream = type.getClassLoader().getResourceAsStream(classAsPath);
byte[] bytes = IOUtils.toByteArray(stream);
JarEntry entry = new JarEntry(classAsPath);
entry.setTime(System.currentTimeMillis());
jarOutputStream.putNextEntry(entry);
jarOutputStream.write(bytes);
jarOutputStream.closeEntry();
}
}
}
/**
* Creates a ClassLoader that contains an empty class with the given name using the given content.
* The className may have a package separated by /. For example: my/package/myclass
*
* @param className Name of the class to create
* @param content Content of the created class
* @return The class loader
* @throws IOException If there's a problem creating the output stream used to generate the class
*/
public ClassLoader createClassLoaderFromContent(final String className, final String content)
throws IOException {
byte[] classDefinition = compileClass(className, content);
SingleClassClassLoader classLoader = new SingleClassClassLoader();
classLoader.addClass(className, classDefinition);
return classLoader;
}
/**
* Compile the provided class. The className may have a package separated by /. For example:
* my/package/myclass
*
* @param className Name of the class to compile.
* @param classCode Plain text contents of the class
* @return The byte contents of the compiled class.
*/
public byte[] compileClass(final String className, final String classCode) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
OutputStreamJavaFileManager<JavaFileManager> fileManager =
new OutputStreamJavaFileManager<JavaFileManager>(
javaCompiler.getStandardFileManager(null, null, null), byteArrayOutputStream);
List<JavaFileObject> fileObjects = new ArrayList<JavaFileObject>();
fileObjects.add(new JavaSourceFromString(className, classCode));
List<String> options = Arrays.asList("-classpath", this.classPath);
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
if (!javaCompiler.getTask(null, fileManager, diagnostics, options, null, fileObjects).call()) {
StringBuilder errorMsg = new StringBuilder();
for (Diagnostic d : diagnostics.getDiagnostics()) {
String err = String.format("Compilation error: Line %d - %s%n", d.getLineNumber(),
d.getMessage(null));
errorMsg.append(err);
System.err.print(err);
}
throw new IOException(errorMsg.toString());
}
return byteArrayOutputStream.toByteArray();
}
/***
* Add to the ClassPath used when compiling.
*
* @param path Path to add
* @return The complete, new ClassPath
*/
public String addToClassPath(final String path) {
this.classPath += (System.getProperty("path.separator") + path);
return this.classPath;
}
/**
* Get the current ClassPath used when compiling.
*
* @return The ClassPath used when compiling.
*/
public String getClassPath() {
return this.classPath;
}
private class JavaSourceFromString extends SimpleJavaFileObject {
final String code;
JavaSourceFromString(final String name, final String code) {
super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}
@Override
public CharSequence getCharContent(final boolean ignoreEncodingErrors) {
return this.code;
}
}
private class OutputStreamSimpleFileObject extends SimpleJavaFileObject {
private OutputStream outputStream;
protected OutputStreamSimpleFileObject(final URI uri, final JavaFileObject.Kind kind,
final OutputStream outputStream) {
super(uri, kind);
this.outputStream = outputStream;
}
@Override
public OutputStream openOutputStream() {
return this.outputStream;
}
}
private class OutputStreamJavaFileManager<M extends JavaFileManager>
extends ForwardingJavaFileManager<M> {
private OutputStream outputStream;
protected OutputStreamJavaFileManager(final M fileManager, final OutputStream outputStream) {
super(fileManager);
this.outputStream = outputStream;
}
@Override
public JavaFileObject getJavaFileForOutput(final JavaFileManager.Location location,
final String className, final JavaFileObject.Kind kind, final FileObject sibling) {
return new OutputStreamSimpleFileObject(new File(className).toURI(), kind, outputStream);
}
}
public class SingleClassClassLoader extends ClassLoader {
public Class addClass(final String className, final byte[] definition) {
return super.defineClass(className, definition, 0, definition.length);
}
}
}