JavassistMojo.java
/*
* Copyright 2012 http://github.com/drochetti/
*
* Licensed 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 de.icongmbh.oss.maven.plugin.javassist;
import static java.lang.Thread.currentThread;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import javassist.build.IClassTransformer;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Maven plugin that will apply <a
* href="http://www.javassist.org/">Javassist</a>
* class transformations on compiled classes (bytecode instrumentation).
*
* <p>
* The real modifications of the bytecode are performed by one or more instances of the configured
* {@link IClassTransformer}.
* </p>
* <p>
* Example plugin configuration :
* </p>
*
* <pre>
* {@code
* ...
* <configuration>
* <includeTestClasses>false</includeTestClasses>
* <buildDir>bin/classes</buildDir>
* <testBuildDir>bin/test-classes</testBuildDir>
* <transformerClasses>
* <transformerClass>
* <className>
* de.icongmbh.oss.maven.plugin.javassist.example.transformer.MethodCallClassTransformer
* </className>
* </transformerClass>
* </transformerClasses>
* </configuration>
* ...
* }
* </pre>
*
* @since 1.0.0
*/
// @formatter:off
@Mojo(name = "javassist", defaultPhase = LifecyclePhase.PROCESS_CLASSES,
requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
// @formatter:on
public class JavassistMojo extends AbstractMojo {
private static final Logger LOGGER = LoggerFactory.getLogger(JavassistMojo.class);
private static final Class<IClassTransformer> TRANSFORMER_TYPE = IClassTransformer.class;
// @formatter:off
@Parameter(defaultValue = "${project}", property = "javassist.project", required = true,
readonly = true)
// @formatter:on
private MavenProject project;
/**
* Skips all processing performed by this goal.
*
* <pre>
* {@code
* ...
* <configuration>
* <skip>false</skip>
* </configuration>
* ...
* }
* </pre>
*/
@Parameter(defaultValue = "false", property = "javassist.skip", required = false)
private boolean skip;
/**
* Whether or not to include test classes to be processed by declared transformers.
*
* <pre>
* {@code
* ...
* <configuration>
* <includeTestClasses>false</includeTestClasses>
* </configuration>
* ...
* }
* </pre>
*/
@Parameter(defaultValue = "true", property = "javassist.includeTestClasses", required = true)
private Boolean includeTestClasses;
/**
* Configure one or more class transformer.
*
* <pre>
* {@code
* ...
* <configuration>
* <transformerClasses>
* <transformerClass>
* <className>
* de.icongmbh.oss.maven.plugin.javassist.example.transformer.MethodCallClassTransformer
* </className>
* </transformerClass>
* </transformerClasses>
* </configuration>
* ...
* }
* </pre>
*/
@Parameter(property = "javassist.transformerClasses", required = true)
private ClassTransformerConfiguration[] transformerClasses;
/**
* Allows to customize the build directory of the project, used for both finding classes to
* transform and output them once transformed.
*
* <p>
* The path must be either absolute or relative to project base directory.
* </p>
*
* <pre>
* {@code
* ...
* <configuration>
* <buildDir>bin/classes</buildDir>
* </configuration>
* ...
* }
* </pre>
*/
@Parameter(defaultValue = "target/classes", property = "javassist.buildDir", required = false)
private String buildDir;
/**
* Allows to customize the build directory of the tests of the project, used for both finding
* classes to transform and output them once transformed.
*
* <p>
* The path must be either absolute or relative to project base directory.
* </p>
*
* <pre>
* {@code
* ...
* <configuration>
* <testBuildDir>bin/test-classes</testBuildDir>
* </configuration>
* ...
* }
* </pre>
*/
// @formatter:off
@Parameter(defaultValue = "target/test-classes", property = "javassist.testBuildDir",
required = false)
// @formatter:on
private String testBuildDir;
@Override
public void execute() throws MojoExecutionException {
if (skip) {
LOGGER.info("Skipping executing.");
return;
}
final ClassLoader originalContextClassLoader = currentThread().getContextClassLoader();
try {
final List<URL> classPath = new ArrayList<URL>();
for (final String runtimeResource : project.getRuntimeClasspathElements()) {
classPath.add(resolveUrl(runtimeResource));
}
final String inputDirectory = (null == buildDir) ? project.getBuild().getOutputDirectory()
: computeDir(buildDir);
classPath.add(resolveUrl(inputDirectory));
loadAdditionalClassPath(classPath);
final JavassistTransformerExecutor executor = new JavassistTransformerExecutor();
String testInputDirectory = (null == testBuildDir)
? project.getBuild().getTestOutputDirectory() : computeDir(testBuildDir);
executor.setTransformerClasses(instantiateTransformerClasses(
currentThread()
.getContextClassLoader(),
transformerClasses));
executor.setInputDirectory(inputDirectory);
executor.setOutputDirectory(inputDirectory);
executor.execute();
if (includeTestClasses) {
classPath.add(resolveUrl(testInputDirectory));
executor.setInputDirectory(testInputDirectory);
executor.setOutputDirectory(testInputDirectory);
executor.execute();
}
} catch (final Exception e) {
getLog().error(e.getMessage(), e);
throw new MojoExecutionException(e.getMessage(), e);
} finally {
currentThread().setContextClassLoader(originalContextClassLoader);
}
}
private void loadAdditionalClassPath(final List<URL> classPath) {
if (classPath.isEmpty()) {
return;
}
final ClassLoader contextClassLoader = currentThread().getContextClassLoader();
// @formatter:off
final URLClassLoader pluginClassLoader = URLClassLoader.newInstance(
classPath.toArray(new URL[classPath.size()]), contextClassLoader);
// @formatter:on
currentThread().setContextClassLoader(pluginClassLoader);
}
private String computeDir(String dir) {
File dirFile = new File(dir);
if (dirFile.isAbsolute()) {
return dirFile.getAbsolutePath();
} else {
return new File(project.getBasedir(), buildDir).getAbsolutePath();
}
}
/**
* Instantiates and configures the passed transformer classes.
*
* @param contextClassLoader maybe {@code null}
* @param transformerClasses maybe {@code null}
* @return array of passed transformer class name instances and never {@code null} but maybe
* empty.
* @throws Exception by
* {@link #instantiateTransformerClass(ClassLoader, ClassTransformerConfiguration)} and
* {@link #configureTransformerInstance(IClassTransformer, Properties)}
* @see #instantiateTransformerClass(ClassLoader, ClassTransformerConfiguration)
* @see #configureTransformerInstance(IClassTransformer, Properties)
*/
// @formatter:off
protected IClassTransformer[] instantiateTransformerClasses(
final ClassLoader contextClassLoader,
final ClassTransformerConfiguration... transformerClasses) throws Exception {
// @formatter:on
if (null == transformerClasses || transformerClasses.length <= 0) {
throw new MojoExecutionException("Invalid transformer classes passed");
}
final List<IClassTransformer> transformerInstances = new LinkedList<IClassTransformer>();
for (ClassTransformerConfiguration transformerClass : transformerClasses) {
final IClassTransformer transformerInstance = instantiateTransformerClass(contextClassLoader,
transformerClass);
configureTransformerInstance(transformerInstance, transformerClass.getProperties());
transformerInstances.add(transformerInstance);
}
return transformerInstances.toArray(new IClassTransformer[transformerInstances.size()]);
}
/**
* Instantiate the class passed by {@link ClassTransformerConfiguration} configuration object.
*
* @param contextClassLoader maybe {@code null}
* @param transformerClass must not be {@code null}
*
* @return new instance of passed transformer class name and never {@code null}
*
* @throws ClassNotFoundException by {@code transformerClass} {@link Class#forName(String)}.
* @throws InstantiationException by {@code transformerClass} {@link Class#forName(String)}.
* @throws IllegalAccessException by {@code transformerClass} {@link Class#forName(String)}.
* @throws MojoExecutionException if passed {@code transformerClass} is {@code null} or invalid
*
* @see Class#forName(String, boolean, ClassLoader)
*/
// @formatter:off
protected IClassTransformer instantiateTransformerClass(
final ClassLoader contextClassLoader,
final ClassTransformerConfiguration transformerClass) throws ClassNotFoundException,
InstantiationException,
IllegalAccessException,
MojoExecutionException {
// @formatter:on
if (null == transformerClass || null == transformerClass.getClassName()
|| transformerClass.getClassName().trim().isEmpty()) {
throw new MojoExecutionException("Invalid transformer class name passed");
}
final Class<?> transformerClassInstance = Class.forName(transformerClass.getClassName().trim(),
true,
contextClassLoader);
if (TRANSFORMER_TYPE.isAssignableFrom(transformerClassInstance)) {
return TRANSFORMER_TYPE.cast(transformerClassInstance.newInstance());
} else {
throw new MojoExecutionException("Transformer class must inherit from "
+ TRANSFORMER_TYPE.getName());
}
}
/**
* Configure the passed {@link ClassTransformer} instance using the passed {@link Properties}.
*
* @param transformerInstance maybe {@code null}
* @param properties maybe {@code null} or empty
*
* @throws Exception by {@link ClassTransformer#configure(Properties)}
*/
protected void configureTransformerInstance(final IClassTransformer transformerInstance,
final Properties properties) throws Exception {
if (null == transformerInstance || !(transformerInstance instanceof ClassTransformer)) {
return;
}
((ClassTransformer)transformerInstance).configure(properties);
}
private URL resolveUrl(final String resource) {
try {
return new File(resource).toURI().toURL();
} catch (final MalformedURLException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* Signals whether or not to skip the plugin execution.
*
* @return {@code true} if configuration option is set otherwise {@code false}
*/
public boolean isSkip() {
return skip;
}
/**
* Whether or not to include test classes in class transformation.
*
* @return {@code true} if configuration option is set otherwise {@code false} and never
* {@code null}
*/
public Boolean getIncludeTestClasses() {
return null == includeTestClasses ? Boolean.FALSE : includeTestClasses;
}
/**
* The configured transformer classes.
*
* @return all configured transformer classes and never {@code null} but maybe empty.
*/
public ClassTransformerConfiguration[] getTransformerClasses() {
return (null == this.transformerClasses) ? new ClassTransformerConfiguration[0]
: this.transformerClasses.clone();
}
/**
* The build directory of the project, used for both finding classes to
* transform and output them once transformed.
*
* @return never {@code null}
*/
public String getBuildDir() {
return buildDir;
}
/**
* The build directory of the tests of the project, used for both finding
* classes to transform and output them once transformed.
*
* @return never {@code null}
*/
public String getTestBuildDir() {
return testBuildDir;
}
}