/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* 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.kie.maven.plugin;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Execute;
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.drools.core.phreak.ReactiveObject;
import javassist.ClassPool;
import javassist.CtClass;
@Mojo(name = "injectreactive",
requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
requiresProject = true,
defaultPhase = LifecyclePhase.COMPILE)
@Execute(goal = "injectreactive",
phase = LifecyclePhase.COMPILE)
public class InjectReactiveMojo extends AbstractKieMojo {
private List<File> sourceSet = new ArrayList<File>();
@Parameter(required = true, defaultValue = "${project.build.outputDirectory}")
private File outputDirectory;
@Parameter(alias = "instrument-enabled", property = "kie.instrument.enabled", defaultValue = "false")
private boolean enabled;
@Parameter(alias = "instrument-failOnError", property = "kie.instrument.failOnError", defaultValue = "true")
private boolean failOnError;
/*
* DO NOT add a default to @Parameter annotation as it buggy to assign it regardless
*/
@Parameter(alias = "instrument-packages", property = "kie.instrument.packages")
private String[] instrumentPackages;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (!enabled) {
getLog().debug("Configuration for instrument-enabled is false, skipping goal 'injectreactive' execute() end.");
return;
}
if (instrumentPackages.length == 0) {
getLog().debug("No configuration passed for instrument-packages, default to '*' .");
instrumentPackages = new String[]{"*"};
}
getLog().debug("Configured with resolved instrument-packages: "+Arrays.asList(instrumentPackages));
List<String> packageRegExps = convertAllToPkgRegExps(instrumentPackages);
for (String prefix : packageRegExps) {
getLog().debug(" "+prefix);
}
// Perform a depth first search for sourceSet
File root = outputDirectory;
if ( !root.exists() ) {
getLog().info( "Skipping InjectReactive enhancement plugin execution since there is no classes dir " + outputDirectory );
return;
}
walkDir( root );
if ( sourceSet.isEmpty() ) {
getLog().info( "Skipping InjectReactive enhancement plugin execution since there are no classes to enhance on " + outputDirectory );
return;
}
getLog().info( "Starting InjectReactive enhancement for classes on " + outputDirectory );
final ClassLoader classLoader = toClassLoader( Collections.singletonList( root ) );
final ClassPool classPool = new ClassPool( true ); // 'true' will append classpath for Object.class.
// Need to append classpath for the project itself output directory for dependencies betweek Pojos of the project itself.
try {
getLog().info("Adding to ClassPool the classpath: " + outputDirectory.getAbsolutePath());
classPool.appendClassPath(outputDirectory.getAbsolutePath());
} catch (Exception e) {
getLog().error( "Unable to append path for outputDirectory : "+outputDirectory );
if (failOnError) {
throw new MojoExecutionException("Unable to append path for outputDirectory : "+outputDirectory, e);
} else {
return;
}
}
// Append classpath for ReactiveObject.class by using the JAR of the kie-maven-plugin
try {
String aname = ReactiveObject.class.getPackage().getName().replaceAll("\\.", "/") + "/" + ReactiveObject.class.getSimpleName()+".class";
getLog().info("Resolving ReactiveObject from : "+aname);
// The ReactiveObject shall be resolved by using the JAR of the kie-maven-plugin hence asking the ClassLoader of the kie-maven-plugin to resolve it
String apath = Thread.currentThread().getContextClassLoader().getResource( aname).getPath();
getLog().info(".. as in resource: " + apath );
String path = null;
if (apath.contains("!")) {
path = apath.substring(0, apath.indexOf("!"));
} else {
path = "file:"+apath.substring(0, apath.indexOf(aname));
}
getLog().info(".. as in file path: " + path );
File f = new File(new URI(path));
getLog().info("Adding to ClassPool the classpath: " + f.getAbsolutePath());
classPool.appendClassPath(f.getAbsolutePath());
} catch (Exception e) {
getLog().error( "Unable to locate path for ReactiveObject." );
e.printStackTrace();
if (failOnError) {
throw new MojoExecutionException("Unable to locate path for ReactiveObject.", e);
} else {
return;
}
}
// Append classpath for the project dependencies
for ( URL url : dependenciesURLs() ) {
try {
getLog().info("Adding to ClassPool the classpath: " + url.getPath());
classPool.appendClassPath(url.getPath());
} catch (Exception e) {
getLog().error( "Unable to append path for project dependency : "+url.getPath() );
if (failOnError) {
throw new MojoExecutionException( "Unable to append path for project dependency : "+url.getPath(), e);
} else {
return;
}
}
}
final BytecodeInjectReactive enhancer = BytecodeInjectReactive.newInstance(classPool);
for ( File file : sourceSet ) {
final CtClass ctClass = toCtClass( file, classPool );
if ( ctClass == null ) {
continue;
}
getLog().info( "Evaluating class [" + ctClass.getName() + "]" );
getLog().info( ctClass.getPackageName() );
getLog().info( ""+Arrays.asList( packageRegExps ) );
if ( !isPackageNameIncluded(ctClass.getPackageName(), packageRegExps) ) {
continue;
}
byte[] enhancedBytecode;
try {
enhancedBytecode = enhancer.injectReactive(ctClass.getName());
writeOutEnhancedClass( enhancedBytecode, ctClass, file );
getLog().info( "Successfully enhanced class [" + ctClass.getName() + "]" );
} catch (Exception e) {
getLog().error( "ERROR while trying to enhanced class [" + ctClass.getName() + "]" );
e.printStackTrace();
if (failOnError) {
throw new MojoExecutionException("ERROR while trying to enhanced class [" + ctClass.getName() + "]", e);
} else {
return;
}
}
}
}
private CtClass toCtClass(File file, ClassPool classPool) throws MojoExecutionException {
try {
final InputStream is = new FileInputStream( file.getAbsolutePath() );
try {
return classPool.makeClass( is );
}
catch (IOException e) {
String msg = "Javassist unable to load class in preparation for enhancing: " + file.getAbsolutePath();
if ( failOnError ) {
throw new MojoExecutionException( msg, e );
}
getLog().warn( msg );
return null;
}
finally {
try {
is.close();
}
catch (IOException e) {
getLog().info( "Was unable to close InputStream : " + file.getAbsolutePath(), e );
}
}
}
catch (FileNotFoundException e) {
// should never happen, but...
String msg = "Unable to locate class file for InputStream: " + file.getAbsolutePath();
if ( failOnError ) {
throw new MojoExecutionException( msg, e );
}
getLog().warn( msg );
return null;
}
}
private ClassLoader toClassLoader(List<File> runtimeClasspath) throws MojoExecutionException {
List<URL> urls = new ArrayList<URL>();
for ( File file : runtimeClasspath ) {
try {
urls.add( file.toURI().toURL() );
getLog().debug( "Adding classpath entry for classes root " + file.getAbsolutePath() );
}
catch (MalformedURLException e) {
String msg = "Unable to resolve classpath entry to URL: " + file.getAbsolutePath();
if ( failOnError ) {
throw new MojoExecutionException( msg, e );
}
getLog().warn( msg );
}
}
urls.addAll( dependenciesURLs() );
return new URLClassLoader( urls.toArray( new URL[urls.size()] ), null );
}
private List<URL> dependenciesURLs() throws MojoExecutionException {
List<URL> urls = new ArrayList<>();
// HHH-10145 Add dependencies to classpath as well - all but the ones used for testing purposes
Set<Artifact> artifacts = null;
MavenProject project = ( (MavenProject) getPluginContext().get( "project" ) );
if ( project != null ) {
// Prefer execution project when available (it includes transient dependencies)
MavenProject executionProject = project.getExecutionProject();
artifacts = ( executionProject != null ? executionProject.getArtifacts() : project.getArtifacts() );
}
if ( artifacts != null) {
for ( Artifact a : artifacts ) {
if ( !Artifact.SCOPE_TEST.equals( a.getScope() ) ) {
try {
urls.add( a.getFile().toURI().toURL() );
getLog().debug( "Adding classpath entry for dependency " + a.getId() );
}
catch (MalformedURLException e) {
String msg = "Unable to resolve URL for dependency " + a.getId() + " at " + a.getFile().getAbsolutePath();
if ( failOnError ) {
throw new MojoExecutionException( msg, e );
}
getLog().warn( msg );
}
}
}
}
return urls;
}
/**
* Expects a directory.
*/
private void walkDir(File dir) {
walkDir(
dir,
new FileFilter() {
@Override
public boolean accept(File pathname) {
return ( pathname.isFile() && pathname.getName().endsWith( ".class" ) );
}
},
new FileFilter() {
@Override
public boolean accept(File pathname) {
return ( pathname.isDirectory() );
}
}
);
}
private void walkDir(File dir, FileFilter classesFilter, FileFilter dirFilter) {
File[] dirs = dir.listFiles( dirFilter );
for ( File dir1 : dirs ) {
walkDir( dir1, classesFilter, dirFilter );
}
File[] files = dir.listFiles( classesFilter );
Collections.addAll( this.sourceSet, files );
}
private void writeOutEnhancedClass(byte[] enhancedBytecode, CtClass ctClass, File file) throws MojoExecutionException{
if ( enhancedBytecode == null ) {
return;
}
try {
if ( file.delete() ) {
if ( !file.createNewFile() ) {
getLog().error( "Unable to recreate class file [" + ctClass.getName() + "]" );
}
}
else {
getLog().error( "Unable to delete class file [" + ctClass.getName() + "]" );
}
}
catch (IOException e) {
getLog().warn( "Problem preparing class file for writing out enhancements [" + ctClass.getName() + "]" );
}
try {
FileOutputStream outputStream = new FileOutputStream( file, false );
try {
outputStream.write( enhancedBytecode );
outputStream.flush();
}
catch (IOException e) {
String msg = String.format( "Error writing to enhanced class [%s] to file [%s]", ctClass.getName(), file.getAbsolutePath() );
if ( failOnError ) {
throw new MojoExecutionException( msg, e );
}
getLog().warn( msg );
}
finally {
try {
outputStream.close();
ctClass.detach();
}
catch (IOException ignore) {
}
}
}
catch (FileNotFoundException e) {
String msg = "Error opening class file for writing: " + file.getAbsolutePath();
if ( failOnError ) {
throw new MojoExecutionException( msg, e );
}
getLog().warn( msg );
}
}
public static List<String> convertAllToPkgRegExps(String[] patterns) {
List<String> result = new ArrayList<>();
for (String p : patterns) {
if ( p.equals("*") ) {
result.add("^.*$");
} else if ( !p.endsWith(".*") ) {
// a pattern like com.acme should match for com.acme only (not the subpackages).
result.add( "^" + p.replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*") + "$" );
} else if ( p.endsWith(".*") ) {
// a pattern like com.acme.* should match for com.acme and all subpackages of com.acme.*
result.add( "^" + p.substring(0, p.length()-2).replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*") + "$" );
result.add( "^" + p.replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*") + "$" );
} else {
// unexpected input will be passed as-is.
result.add(p);
}
}
return result;
}
public static boolean isPackageNameIncluded(String packageName, List<String> regexps) {
for (String r : regexps) {
if (packageName.matches(r)) {
return true;
}
}
return false;
}
}