/* * 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; } }