org.codehaus.mojo.clirr.AbstractClirrMojo Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of clirr-maven-plugin Show documentation
Show all versions of clirr-maven-plugin Show documentation
Clirr is a tool that checks Java libraries for binary and source compatibility with older releases.
Basically you give it two sets of jar files and Clirr dumps out a list of changes in the public API.
The clirr-maven-plugin can be configured to break the build, if it detects incompatible api changes.
In a continuous integration process, the clirr-maven-plugin can automatically prevent accidental
introduction of binary or source compatibility problems. Additionally, the plugin can generate
a report as part of the generated site.
package org.codehaus.mojo.clirr;
/*
* Copyright 2006 The Apache Software Foundation.
*
* 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.
*/
import net.sf.clirr.core.Checker;
import net.sf.clirr.core.CheckerException;
import net.sf.clirr.core.ClassFilter;
import net.sf.clirr.core.DiffListener;
import net.sf.clirr.core.PlainDiffListener;
import net.sf.clirr.core.Severity;
import net.sf.clirr.core.internal.bcel.BcelJavaType;
import net.sf.clirr.core.internal.bcel.BcelTypeArrayBuilder;
import net.sf.clirr.core.spi.JavaType;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.util.ClassLoaderRepository;
import org.apache.bcel.util.Repository;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.metadata.ArtifactMetadataRetrievalException;
import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
import org.apache.maven.artifact.versioning.VersionRange;
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 org.apache.maven.project.MavenProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.artifact.InvalidDependencyVersionException;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.xml.XmlStreamReader;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* Base parameters for Clirr check and report.
*
* @author Brett Porter
* @todo i18n exceptions, log messages
* @requiresDependencyResolution compile
*/
public abstract class AbstractClirrMojo
extends AbstractMojo
{
/**
* Flag to easily skip execution.
*
* @parameter property="clirr.skip" default-value="false"
*/
protected boolean skip;
/**
* @parameter property="project"
* @required
* @readonly
*/
protected MavenProject project;
/**
* @component
*/
protected ArtifactResolver resolver;
/**
* @component
*/
protected ArtifactFactory factory;
/**
* @parameter default-value="${localRepository}"
* @required
* @readonly
*/
protected ArtifactRepository localRepository;
/**
* @component
*/
private ArtifactMetadataSource metadataSource;
/**
* @component
*/
private MavenProjectBuilder mavenProjectBuilder;
/**
* The classes of this project to compare the last release against.
*
* @parameter default-value="${project.build.outputDirectory}
*/
protected File classesDirectory;
/**
* Version to compare the current code against.
*
* @parameter property="comparisonVersion" default-value="(,${project.version})"
*/
protected String comparisonVersion;
/**
* List of artifacts to compare the current code against. This
* overrides comparisonVersion
, if present.
* Each comparisonArtifact
is made of a groupId
, an artifactId
and
* a version
number. Optionally it may have a classifier
* (default null) and a type
(default "jar").
*
* @parameter
*/
protected ArtifactSpecification[] comparisonArtifacts;
/**
* Show only messages of this severity or higher. Valid values are
* info
, warning
and error
.
*
* @parameter property="minSeverity" default-value="warning"
*/
protected String minSeverity;
/**
* A text output file to render to. If omitted, no output is rendered to a text file.
*
* @parameter property="textOutputFile"
*/
protected File textOutputFile;
/**
* An XML file to render to. If omitted, no output is rendered to an XML file.
*
* @parameter property="xmlOutputFile"
*/
protected File xmlOutputFile;
/**
* A list of classes to include. Anything not included is excluded. If omitted, all are assumed to be included.
* Values are specified in path pattern notation, e.g. org/codehaus/mojo/**
.
*
* @parameter
*/
protected String[] includes;
/**
* A list of classes to exclude. These classes are excluded from the list of classes that are included.
* Values are specified in path pattern notation, e.g. org/codehaus/mojo/**
.
*
* @parameter
*/
protected String[] excludes;
/**
* A list of differences reported by Clirr that should be ignored when producing the final report.
* Values specified here will be joined with the ones specified using the "ignoredDifferencesFile"
* parameter.
*
* @parameter
* @since 2.5
*/
protected Difference[] ignored;
/**
* A path to the XML file containing the ignored differences definitions.
* Values specified int the file will be joined with the ones specified using the "ignored"
* parameter.
*
* File Format
*
* <differences>
* ...
* <difference>
* <className>com.thoughtworks.qdox.parser.impl.Parser</className>
* <field>*</field>
* </difference>
* ...
* </differences>
*
*
* @parameter property="clirr.ignoredDifferencesFile"
* @since 2.5
*/
protected File ignoredDifferencesFile;
/**
* Whether to log the results to the console or not.
*
* @parameter property="logResults" default-value="false"
*/
protected boolean logResults;
/**
* @parameter property="clirr.skipArtifactTypeTest" default-value="false"
*/
private boolean skipArtifactTypeTest;
private static final URL[] EMPTY_URL_ARRAY = new URL[0];
public void execute()
throws MojoExecutionException, MojoFailureException
{
if ( skip )
{
getLog().info( "Skipping execution" );
}
else
{
doExecute();
}
}
protected abstract void doExecute()
throws MojoExecutionException, MojoFailureException;
public ClirrDiffListener executeClirr()
throws MojoExecutionException, MojoFailureException
{
return executeClirr( null );
}
protected ClirrDiffListener executeClirr( Severity minSeverity )
throws MojoExecutionException, MojoFailureException
{
ClirrDiffListener listener = new ClirrDiffListener();
ClassFilter classFilter = new ClirrClassFilter( includes, excludes );
JavaType[] origClasses = resolvePreviousReleaseClasses( classFilter );
JavaType[] currentClasses = resolveCurrentClasses( classFilter );
// Create a Clirr checker and execute
Checker checker = new Checker();
List listeners = new ArrayList();
listeners.add( listener );
if ( xmlOutputFile != null )
{
try
{
listeners.add( new TypeRevealingXmlDiffListener( xmlOutputFile.getAbsolutePath() ) );
}
catch ( IOException e )
{
throw new MojoExecutionException( "Error adding '" + xmlOutputFile + "' for output: " + e.getMessage(),
e );
}
}
if ( textOutputFile != null )
{
try
{
listeners.add( new PlainDiffListener( textOutputFile.getAbsolutePath() ) );
}
catch ( IOException e )
{
throw new MojoExecutionException( "Error adding '" + textOutputFile + "' for output: " + e.getMessage(),
e );
}
}
if ( logResults )
{
listeners.add( new LogDiffListener( getLog() ) );
}
checker.addDiffListener( new DelegatingListener( listeners, minSeverity, getAllIgnored() ) );
reportDiffs( checker, origClasses, currentClasses );
return listener;
}
protected List getAllIgnored()
{
Difference[] ret = ignored;
if ( ignoredDifferencesFile != null && ignoredDifferencesFile.exists() )
{
XmlStreamReader rdr = null;
try
{
rdr = ReaderFactory.newXmlReader( ignoredDifferencesFile );
Difference[] diffs = Difference.parseXml( rdr );
int ignoredLength = ignored == null ? 0 : ignored.length;
Difference[] tmp = new Difference[ignoredLength + diffs.length];
if ( ignored != null )
{
System.arraycopy( ignored, 0, tmp, 0, ignoredLength );
}
System.arraycopy( diffs, 0, tmp, ignoredLength, diffs.length );
ret = tmp;
}
catch ( IOException e )
{
getLog().error( "Could not read the ignored differences file.", e );
}
catch ( XmlPullParserException e )
{
getLog().error( "Could not read the ignored differences file.", e );
}
finally
{
IOUtil.close( rdr );
}
}
return ret == null ? Collections.emptyList() : Arrays.asList( ret );
}
private JavaType[] resolveCurrentClasses( ClassFilter classFilter )
throws MojoExecutionException
{
try
{
ClassLoader currentDepCL = createClassLoader( project.getArtifacts(), null );
return createClassSet( classesDirectory, currentDepCL, classFilter );
}
catch ( MalformedURLException e )
{
throw new MojoExecutionException( "Error creating classloader for current classes", e );
}
}
private JavaType[] resolvePreviousReleaseClasses( ClassFilter classFilter )
throws MojoFailureException, MojoExecutionException
{
final Set previousArtifacts;
final Artifact firstPreviousArtifact;
if ( comparisonArtifacts == null )
{
firstPreviousArtifact = getComparisonArtifact();
comparisonVersion = firstPreviousArtifact.getVersion();
getLog().info( "Comparing to version: " + comparisonVersion );
previousArtifacts = Collections.singleton( firstPreviousArtifact );
}
else
{
previousArtifacts = resolveArtifacts( comparisonArtifacts );
Artifact a = null;
for ( Iterator iter = previousArtifacts.iterator(); iter.hasNext(); )
{
Artifact artifact = (Artifact) iter.next();
if ( a == null )
{
a = artifact;
}
getLog().debug( "Comparing to " + artifact.getGroupId() + ":" + artifact.getArtifactId() + ":"
+ artifact.getVersion() + ":" + artifact.getClassifier() + ":"
+ artifact.getType() );
}
firstPreviousArtifact = a;
}
try
{
for ( Iterator iter = previousArtifacts.iterator(); iter.hasNext(); )
{
Artifact artifact = (Artifact) iter.next();
resolver.resolve( artifact, project.getRemoteArtifactRepositories(), localRepository );
}
final List dependencies = getTransitiveDependencies( previousArtifacts );
ClassLoader origDepCL = createClassLoader( dependencies, previousArtifacts );
final Set files = new HashSet();
for ( Iterator iter = previousArtifacts.iterator(); iter.hasNext(); )
{
Artifact artifact = (Artifact) iter.next();
// Clirr expects JAR files, so let's not pass other artifact files.
// MCLIRR-39 Support for Maven Plugins, which are also JARs
// MCLIRR-61: jenkins plugin so test if isAddedToClasspath
if ( "jar".equals( artifact.getType() ) || "maven-plugin".equals( artifact.getType() )
|| "bundle".equals( artifact.getType() ) || artifact.getArtifactHandler().isAddedToClasspath()
|| skipArtifactTypeTest )
{
files.add( new File( localRepository.getBasedir(), localRepository.pathOf( artifact ) ) );
}
}
return BcelTypeArrayBuilder.createClassSet( (File[]) files.toArray( new File[files.size()] ), origDepCL,
classFilter );
}
catch ( ProjectBuildingException e )
{
throw new MojoExecutionException( "Failed to build project for previous artifact: " + e.getMessage(), e );
}
catch ( InvalidDependencyVersionException e )
{
throw new MojoExecutionException( e.getMessage(), e );
}
catch ( ArtifactResolutionException e )
{
throw new MissingPreviousException( "Error resolving previous version: " + e.getMessage(), e );
}
catch ( ArtifactNotFoundException e )
{
getLog().warn( "Impossible to find previous version" );
return new JavaType[0];
//throw new MojoExecutionException( "Error finding previous version: " + e.getMessage(), e );
}
catch ( MalformedURLException e )
{
throw new MojoExecutionException( "Error creating classloader for previous version's classes", e );
}
}
protected List getTransitiveDependencies( final Set previousArtifacts )
throws ProjectBuildingException, InvalidDependencyVersionException, ArtifactResolutionException,
ArtifactNotFoundException
{
final List dependencies = new ArrayList();
for ( Iterator iter = previousArtifacts.iterator(); iter.hasNext(); )
{
final Artifact a = (Artifact) iter.next();
final Artifact pomArtifact =
factory.createArtifact( a.getGroupId(), a.getArtifactId(), a.getVersion(), a.getScope(), "pom" );
final MavenProject pomProject =
mavenProjectBuilder.buildFromRepository( pomArtifact, project.getRemoteArtifactRepositories(),
localRepository );
final Set pomProjectArtifacts = pomProject.createArtifacts( factory, null, null );
final ArtifactResolutionResult result =
resolver.resolveTransitively( pomProjectArtifacts, pomArtifact, localRepository,
project.getRemoteArtifactRepositories(), metadataSource, null );
dependencies.addAll( result.getArtifacts() );
}
return dependencies;
}
private Artifact resolveArtifact( ArtifactSpecification artifactSpec )
throws MojoFailureException, MojoExecutionException
{
final String groupId = artifactSpec.getGroupId();
if ( groupId == null )
{
throw new MojoFailureException( "An artifacts groupId is required." );
}
final String artifactId = artifactSpec.getArtifactId();
if ( artifactId == null )
{
throw new MojoFailureException( "An artifacts artifactId is required." );
}
final String version = artifactSpec.getVersion();
if ( version == null )
{
throw new MojoFailureException( "An artifacts version number is required." );
}
final VersionRange versionRange = VersionRange.createFromVersion( version );
String type = artifactSpec.getType();
if ( type == null )
{
type = "jar";
}
Artifact artifact =
factory.createDependencyArtifact( groupId, artifactId, versionRange, type, artifactSpec.getClassifier(),
Artifact.SCOPE_COMPILE );
return artifact;
}
protected Set resolveArtifacts( ArtifactSpecification[] artifacts )
throws MojoFailureException, MojoExecutionException
{
Set artifactSet = new HashSet();
Artifact[] result = new Artifact[artifacts.length];
for ( int i = 0; i < result.length; i++ )
{
artifactSet.add( resolveArtifact( artifacts[i] ) );
}
return artifactSet;
}
private Artifact getComparisonArtifact()
throws MojoFailureException, MojoExecutionException
{
// Find the previous version JAR and resolve it, and it's dependencies
VersionRange range;
try
{
range = VersionRange.createFromVersionSpec( comparisonVersion );
}
catch ( InvalidVersionSpecificationException e )
{
throw new MojoFailureException( "Invalid comparison version: " + e.getMessage() );
}
Artifact previousArtifact;
try
{
previousArtifact = factory.createDependencyArtifact( project.getGroupId(), project.getArtifactId(), range,
project.getPackaging(), null, Artifact.SCOPE_COMPILE );
if ( !previousArtifact.getVersionRange().isSelectedVersionKnown( previousArtifact ) )
{
getLog().debug( "Searching for versions in range: " + previousArtifact.getVersionRange() );
List availableVersions = metadataSource.retrieveAvailableVersions( previousArtifact, localRepository,
project.getRemoteArtifactRepositories() );
filterSnapshots( availableVersions );
ArtifactVersion version = range.matchVersion( availableVersions );
if ( version != null )
{
previousArtifact.selectVersion( version.toString() );
}
}
}
catch ( OverConstrainedVersionException e1 )
{
throw new MojoFailureException( "Invalid comparison version: " + e1.getMessage() );
}
catch ( ArtifactMetadataRetrievalException e11 )
{
throw new MojoExecutionException( "Error determining previous version: " + e11.getMessage(), e11 );
}
if ( previousArtifact.getVersion() == null )
{
getLog().info( "Unable to find a previous version of the project in the repository" );
}
return previousArtifact;
}
private void filterSnapshots( List versions )
{
for ( Iterator versionIterator = versions.iterator(); versionIterator.hasNext(); )
{
ArtifactVersion version = (ArtifactVersion) versionIterator.next();
if ( "SNAPSHOT".equals( version.getQualifier() ) )
{
versionIterator.remove();
}
}
}
public static JavaType[] createClassSet( File classes, ClassLoader thirdPartyClasses, ClassFilter classFilter )
throws MalformedURLException
{
ClassLoader classLoader = new URLClassLoader( new URL[]{ classes.toURI().toURL() }, thirdPartyClasses );
Repository repository = new ClassLoaderRepository( classLoader );
List selected = new ArrayList();
DirectoryScanner scanner = new DirectoryScanner();
scanner.setBasedir( classes );
scanner.setIncludes( new String[]{ "**/*.class" } );
scanner.scan();
String[] files = scanner.getIncludedFiles();
for ( int i = 0; i < files.length; i++ )
{
File f = new File( classes, files[i] );
JavaClass clazz = extractClass( f, repository );
if ( classFilter.isSelected( clazz ) )
{
selected.add( new BcelJavaType( clazz ) );
repository.storeClass( clazz );
}
}
JavaType[] ret = new JavaType[selected.size()];
selected.toArray( ret );
return ret;
}
private static JavaClass extractClass( File f, Repository repository )
throws CheckerException
{
InputStream is = null;
try
{
is = new FileInputStream( f );
ClassParser parser = new ClassParser( is, f.getName() );
JavaClass clazz = parser.parse();
clazz.setRepository( repository );
return clazz;
}
catch ( IOException ex )
{
throw new CheckerException( "Cannot read " + f, ex );
}
finally
{
IOUtil.close( is );
}
}
/**
* Create a ClassLoader, which includes the artifacts in artifacts
,
* but excludes the artifacts in previousArtifacts
. The intention is,
* that we let BCEL inspect the artifacts in the latter set, using a
* {@link ClassLoader}, which contains the dependencies. However, the
* {@link ClassLoader} must not contain the jar files, which are being inspected.
*
* @param artifacts The artifacts, from which to build a {@link ClassLoader}.
* @param previousArtifacts The artifacts being inspected, or null, if te
* returned {@link ClassLoader} should contain all the elements of
* artifacts
.
* @return A {@link ClassLoader} which may be used to inspect the classes in
* previousArtifacts.
* @throws MalformedURLException Failed to convert a file to an URL.
*/
protected static ClassLoader createClassLoader( Collection artifacts, Set previousArtifacts )
throws MalformedURLException
{
URLClassLoader cl = null;
if ( !artifacts.isEmpty() )
{
List urls = new ArrayList( artifacts.size() );
for ( Iterator i = artifacts.iterator(); i.hasNext(); )
{
Artifact artifact = (Artifact) i.next();
if ( previousArtifacts == null || !previousArtifacts.contains( artifact ) )
{
urls.add( artifact.getFile().toURI().toURL() );
}
}
if ( !urls.isEmpty() )
{
cl = new URLClassLoader( (URL[]) urls.toArray( EMPTY_URL_ARRAY ) );
}
}
return cl;
}
protected static Severity convertSeverity( String minSeverity )
{
Severity s;
if ( "info".equals( minSeverity ) )
{
s = Severity.INFO;
}
else if ( "warning".equals( minSeverity ) )
{
s = Severity.WARNING;
}
else if ( "error".equals( minSeverity ) )
{
s = Severity.ERROR;
}
else
{
s = null;
}
return s;
}
protected boolean canGenerate()
throws MojoFailureException, MojoExecutionException
{
boolean classes = false;
if ( classesDirectory.exists() )
{
classes = true;
}
else
{
getLog().debug( "Classes directory not found: " + classesDirectory );
}
if ( !classes )
{
getLog().info( "Not generating Clirr report as there are no classes generated by the project" );
return false;
}
if ( comparisonArtifacts == null || comparisonArtifacts.length == 0 )
{
Artifact previousArtifact = getComparisonArtifact();
if ( previousArtifact.getVersion() == null )
{
getLog().info(
"Not generating Clirr report as there is no previous version of the library to compare against" );
return false;
}
}
return true;
}
/**
* Calls {@link Checker#reportDiffs(JavaType[], JavaType[])} and take care of BCEL errors.
*
* @param checker not null
* @param origClasses not null
* @param currentClasses not null
* @see Checker#reportDiffs(JavaType[], JavaType[])
*/
private void reportDiffs( Checker checker, JavaType[] origClasses, JavaType[] currentClasses )
{
try
{
checker.reportDiffs( origClasses, currentClasses );
}
catch ( CheckerException e )
{
getLog().error( e.getMessage() );
// remove class with errors
int matchingClasses = 0;
int j = 0;
for ( int i = 0; i < origClasses.length; i++ )
{
if ( !e.getMessage().endsWith( origClasses[i].getName() ) )
{
matchingClasses++;
}
}
JavaType[] origClasses2 = new JavaType[matchingClasses];
for ( int i = 0; i < origClasses.length; i++ )
{
if ( !e.getMessage().endsWith( origClasses[i].getName() ) )
{
origClasses2[j++] = origClasses[i];
}
}
matchingClasses = 0;
j = 0;
for ( int i = 0; i < currentClasses.length; i++ )
{
if ( !e.getMessage().endsWith( currentClasses[i].getName() ) )
{
matchingClasses++;
}
}
JavaType[] currentClasses2 = new JavaType[matchingClasses];
for ( int i = 0; i < currentClasses.length; i++ )
{
if ( !e.getMessage().endsWith( currentClasses[i].getName() ) )
{
currentClasses2[j++] = currentClasses[i];
}
}
reportDiffs( checker, origClasses2, currentClasses2 );
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy