package org.codehaus.mojo.macker;
/*
* 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.
*/
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.AbstractArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.codehaus.plexus.resource.ResourceManager;
import org.codehaus.plexus.resource.loader.FileResourceCreationException;
import org.codehaus.plexus.resource.loader.FileResourceLoader;
import org.codehaus.plexus.resource.loader.ResourceNotFoundException;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;
/**
* Runs Macker against the compiled classes of the project.
*
* @goal macker
* @execute phase="compile"
* @requiresDependencyResolution compile
* @requiresProject
* @author <a href="http://www.codehaus.org/~wfay/">Wayne Fay</a>
* @author <a href="http://people.apache.org/~bellingard/">Fabrice Bellingard</a>
* @author <a href="http://www.code-cop.org/">Peter Kofler</a>
*/
public class MackerMojo
extends AbstractMojo
{
/**
* Directory containing the class files for Macker to analyze.
*
* @parameter expression="${project.build.outputDirectory}"
* @required
*/
private File classesDirectory;
/**
* The directories containing the test-classes to be analyzed.
*
* @parameter expression="${project.build.testOutputDirectory}"
* @required
*/
private File testClassesDirectory;
/**
* A list of files to exclude from checking. Can contain Ant-style wildcards and double wildcards. Note that these
* exclusion patterns only operate on the path of a source file relative to its source root directory. In other
* words, files are excluded based on their package and/or class name. If you want to exclude entire root
* directories, use the parameter <code>excludeRoots</code> instead.
*
* @parameter
*/
private String[] excludes;
/**
* A list of files to include from checking. Can contain Ant-style wildcards and double wildcards.
* Defaults to **\/*.class.
*
* @parameter
*/
private String[] includes;
/**
* Run Macker on the tests.
*
* @parameter default-value="false"
*/
private boolean includeTests;
/**
* Directory containing the rules files for Macker.
*
* @parameter expression="${basedir}/src/main/config"
* @required
*/
private File rulesDirectory;
/**
* Directory where the Macker output file will be generated.
*
* @parameter default-value="${project.build.directory}"
* @required
*/
private File outputDirectory;
/**
* Name of the Macker output file.
*
* @parameter expression="${outputName}" default-value="macker-out.xml"
* @required
*/
private String outputName;
/**
* Print max messages.
*
* @parameter expression="${maxmsg}" default-value="0"
*/
private int maxmsg;
/**
* Print threshold. Valid options are error, warning, info, and debug.
*
* @parameter expression="${print}"
*/
private String print;
/**
* Anger threshold. Valid options are error, warning, info, and debug.
*
* @parameter expression="${anger}"
*/
private String anger;
/**
* Name of the Macker rules file.
*
* @parameter expression="${rule}" default-value="macker-rules.xml"
*/
private String rule;
/**
* Name of the Macker rules files.
*
* @parameter expression="${rules}"
*/
private String[] rules = new String[0];
/**
* @component
* @required
* @readonly
*/
private ResourceManager locator;
/**
* Variables map that will be passed to Macker.
*
* @parameter expression="${variables}"
*/
private Map/*<String, String>*/variables = new HashMap/*<String, String>*/();
/**
* Verbose setting for Macker tool execution.
*
* @parameter expression="${verbose}" default-value="false"
*/
private boolean verbose;
/**
* Fail the build on an error.
*
* @parameter default-value="true"
*/
private boolean failOnError;
/**
* Skip the checks. Most useful on the command line
* via "-Dmacker.skip=true".
*
* @parameter expression="${macker.skip}" default-value="false"
*/
private boolean skip;
/**
* <i>Maven Internal</i>: Project to interact with.
*
* @parameter expression="${project}"
* @required
* @readonly
*/
private MavenProject project;
/**
* Maximum memory to pass JVM of Macker processes.
*
* @parameter expression="${macker.maxmem}" default-value="64m"
*/
private String maxmem;
/**
* <i>Maven Internal</i>: List of artifacts for the plugin.
*
* @parameter expression="${plugin.artifacts}"
* @required
* @readonly
*/
private List/*<Artifact>*/ pluginClasspathList;
/**
* Only output Macker errors, avoid info messages.
*
* @parameter expression="${quiet}" default-value="false"
*/
private boolean quiet;
/**
* @parameter default-value="${localRepository}"
* @required
* @readonly
*/
private ArtifactRepository localRepository;
/**
* @parameter default-value="${project.remoteArtifactRepositories}"
* @required
* @readonly
*/
private List remoteRepositories;
/**
* @component
* @required
* @readonly
*/
private ArtifactResolver resolver;
/**
* @throws MojoExecutionException if a error occurs during Macker execution
* @throws MojoFailureException if Macker detects a failure.
* @see org.apache.maven.plugin.Mojo#execute()
*/
public void execute()
throws MojoExecutionException, MojoFailureException
{
if ( skip )
{
return;
}
ArtifactHandler artifactHandler = project.getArtifact().getArtifactHandler();
if ( !"java".equals( artifactHandler.getLanguage() ) )
{
if ( !quiet )
{
getLog().info( "Not executing macker as the project is not a Java classpath-capable package" );
}
return;
}
//configure ResourceManager
locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() );
locator.addSearchPath( "url", "" );
locator.setOutputDirectory( new File( project.getBuild().getDirectory() ) );
// check if rules were specified
if ( null == rules || 0 == rules.length )
{
rules = new String[1]; // at least the default name
rules[0] = rule;
}
// check if there are class files to analyze
List/*<File>*/files;
try
{
files = getFilesToProcess();
}
catch ( IOException e )
{
throw new MojoExecutionException( "Error during Macker execution: error in file selection", e );
}
if ( files == null || files.size() == 0 )
{
// no class file, we can't do anything
if ( !quiet )
{
if ( includeTests )
{
getLog().info( "No class files in directories " + classesDirectory + ", " + testClassesDirectory );
}
else
{
getLog().info( "No class files in specified directory " + classesDirectory );
}
}
}
else
{
if ( !outputDirectory.exists() )
{
if ( !outputDirectory.mkdirs() )
{
throw new MojoExecutionException( "Error during Macker execution: Could not create directory "
+ outputDirectory.getAbsolutePath() );
}
}
// let's go!
File outputFile = new File( outputDirectory, outputName );
launchMacker( outputFile, files );
}
}
/**
* Executes Macker as requested.
*
* @param outputFile the result file that will should produced by macker
* @param files classes files that should be analysed
* @throws MojoExecutionException if a error occurs during Macker execution
* @throws MojoFailureException if Macker detects a failure.
*/
private void launchMacker( File outputFile, List/*<File>*/files )
throws MojoExecutionException, MojoFailureException
{
try
{
Macker macker = createMacker( outputFile );
configureRules( macker );
initMackerVariables( macker );
specifyClassFilesToAnalyse( files, macker );
// we're OK with configuration, let's run Macker
macker.check();
// if we're here, then everything went fine
if ( !quiet )
{
getLog().info( "Macker has not found any violation." );
}
}
catch ( MojoExecutionException ex )
{
throw ex;
}
catch ( MojoFailureException ex )
{
getLog().warn( "Macker has detected violations. Please refer to the XML report for more information." );
if ( failOnError )
{
throw ex;
}
}
catch ( Exception ex )
{
throw new MojoExecutionException( "Error during Macker execution: " + ex.getMessage(), ex );
}
}
/**
* Tell Macker where to look for Class files to analyze.
*
* @param files the ".class" files to analyze
* @param macker the Macker instance
* @throws IOException if there's a problem reading a file
* @throws MojoExecutionException if there's a problem parsing a class
*/
private void specifyClassFilesToAnalyse( List/*<File>*/files, Macker macker )
throws IOException, MojoExecutionException
{
for ( Iterator/*<File>*/i = files.iterator(); i.hasNext(); )
{
macker.addClass( (File) i.next() );
}
}
/**
* If specific variables are set in the POM, give them to Macker.
*
* @param macker the Macker isntance
*/
private void initMackerVariables( Macker macker )
{
if ( variables != null && variables.size() > 0 )
{
Iterator/*<String>*/it = variables.keySet().iterator();
while ( it.hasNext() )
{
String key = (String) it.next();
macker.setVariable( key, (String) variables.get( key ) );
}
}
}
/**
* Configure Macker with the rule files specified in the POM.
*
* @param macker the Macker instance
* @throws IOException if there's a problem reading a file
* @throws MojoExecutionException if there's a problem parsing a rule file
*/
private void configureRules( Macker macker )
throws IOException, MojoExecutionException
{
try
{
for ( int i = 0; i < rules.length; i++ )
{
String set = rules[i];
File ruleFile = new File( rulesDirectory, set );
if ( ruleFile.exists() )
{
getLog().debug( "Add rules file: " + rulesDirectory + File.separator + rules[i] );
}
else
{
getLog().debug( "Preparing ruleset: " + set );
ruleFile = locator.getResourceAsFile( set, getLocationTemp( set ) );
if ( null == ruleFile )
{
throw new MojoExecutionException( "Could not resolve rules file: " + set );
}
}
macker.addRulesFile( ruleFile );
}
}
catch ( ResourceNotFoundException e )
{
throw new MojoExecutionException( e.getMessage(), e );
}
catch ( FileResourceCreationException e )
{
throw new MojoExecutionException( e.getMessage(), e );
}
}
/**
* Convenience method to get the location of the specified file name.
*
* @param name the name of the file whose location is to be resolved
* @return a String that contains the absolute file name of the file
*/
private String getLocationTemp( String name )
{
String loc = name;
if ( loc.indexOf( '/' ) != -1 )
{
loc = loc.substring( loc.lastIndexOf( '/' ) + 1 );
}
if ( loc.indexOf( '\\' ) != -1 )
{
loc = loc.substring( loc.lastIndexOf( '\\' ) + 1 );
}
getLog().debug( "Before: " + name + " After: " + loc );
return loc;
}
/**
* Prepares Macker for the analysis.
*
* @param outputFile the result file that will should produced by Macker
* @return the new instance of Macker
* @throws IOException if there's a problem with the report file
* @throws MojoExecutionException if there's a creating the classpath for forking
*/
private Macker createMacker( File outputFile )
throws IOException, MojoExecutionException
{
// Macker macker = new LinkedMacker();
ForkedMacker macker = new ForkedMacker( );
macker.setLog( getLog() );
macker.setMaxmem( maxmem );
macker.setPluginClasspathList( collectArtifactList() );
macker.setQuiet( quiet );
macker.setVerbose( verbose );
macker.setXmlReportFile( outputFile );
if ( maxmsg > 0 )
{
macker.setPrintMaxMessages( maxmsg );
}
if ( print != null )
{
macker.setPrintThreshold( print );
}
if ( anger != null )
{
macker.setAngerThreshold( anger );
}
return macker;
}
/**
* Get the full classpath of this plugin including the plugin itself.
* @throws MojoExecutionException if there's a creating the classpath for forking
*/
private List collectArtifactList()
throws MojoExecutionException
{
// look up myself, it must be here
Artifact myself = (Artifact) getProject().getPluginArtifactMap().get( "org.codehaus.mojo:macker-maven-plugin" );
try
{
resolver.resolve( myself, remoteRepositories, localRepository );
}
catch ( AbstractArtifactResolutionException e )
{
throw new MojoExecutionException( e.getMessage(), e );
}
List/*<Artifact>*/ classpath = new ArrayList/*<Artifact>*/();
classpath.add( myself );
classpath.addAll( pluginClasspathList );
return classpath;
}
/**
* Returns the MavenProject object.
*
* @return MavenProject
*/
public MavenProject getProject()
{
return this.project;
}
/**
* Convenience method to get the list of files where the PMD tool will be executed
*
* @return a List of the files where the MACKER tool will be executed
* @throws IOException if there's a problem scanning the directories
*/
private List/*<File>*/getFilesToProcess()
throws IOException
{
List/*<File>*/directories = new ArrayList/*<File>*/();
if ( classesDirectory != null && classesDirectory.isDirectory() )
{
directories.add( classesDirectory );
}
if ( includeTests )
{
if ( testClassesDirectory != null && testClassesDirectory.isDirectory() )
{
directories.add( testClassesDirectory );
}
else
{
getLog().info( "No class files in test directory: " + testClassesDirectory );
}
}
String excluding = getExcludes();
getLog().debug( "Exclusions: " + excluding );
String including = getIncludes();
getLog().debug( "Inclusions: " + including );
List/*<File>*/files = new LinkedList/*<File>*/();
for ( Iterator/*<File>*/i = directories.iterator(); i.hasNext(); )
{
File sourceDirectory = (File) i.next();
if ( sourceDirectory.isDirectory() )
{
List/*<File>*/newfiles = FileUtils.getFiles( sourceDirectory, including, excluding );
files.addAll( newfiles );
}
}
return files;
}
/**
* Gets the comma separated list of effective include patterns.
*
* @return The comma separated list of effective include patterns, never <code>null</code>.
*/
private String getIncludes()
{
Collection/*<String>*/patterns = new LinkedHashSet/*<String>*/();
if ( includes != null )
{
patterns.addAll( Arrays.asList( includes ) );
}
if ( patterns.isEmpty() )
{
patterns.add( "**/*.class" );
}
return StringUtils.join( patterns.iterator(), "," );
}
/**
* Gets the comma separated list of effective exclude patterns.
*
* @return The comma separated list of effective exclude patterns, never <code>null</code>.
*/
private String getExcludes()
{
Collection/*<String>*/patterns = new LinkedHashSet/*<String>*/( FileUtils.getDefaultExcludesAsList() );
if ( excludes != null )
{
patterns.addAll( Arrays.asList( excludes ) );
}
return StringUtils.join( patterns.iterator(), "," );
}
/**
* For test purposes only.
*/
void setRules( String[] ruleSets )
{
rules = (String[]) ruleSets.clone();
}
}