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 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.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import net.sf.clirr.core.Checker; import net.sf.clirr.core.CheckerException; import net.sf.clirr.core.ClassFilter; import net.sf.clirr.core.PlainDiffListener; import net.sf.clirr.core.Severity; import net.sf.clirr.core.XmlDiffListener; 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; /** * Base parameters for Clirr check and report. * * @author <a href="mailto:brett@apache.org">Brett Porter</a> * @todo i18n exceptions, log messages * @requiresDependencyResolution compile */ public abstract class AbstractClirrMojo extends AbstractMojo { /** * Flag to easily skip execution. * * @parameter expression="${clirr.skip}" default-value="false" */ protected boolean skip; /** * @parameter expression="${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 expression="${comparisonVersion}" default-value="(,${project.version})" */ protected String comparisonVersion; /** * List of artifacts to compare the current code against. This * overrides <code>comparisonVersion</code>, if present. * Each <code>comparisonArtifact</code> is made of a <code>groupId</code>, an <code>artifactId</code> and * a <code>version</code> number. Optionally it may have a <code>classifier</code> * (default null) and a <code>type</code> (default "jar"). * @parameter */ protected ArtifactSpecification[] comparisonArtifacts; /** * Show only messages of this severity or higher. Valid values are * <code>info</code>, <code>warning</code> and <code>error</code>. * * @parameter expression="${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 expression="${textOutputFile}" */ protected File textOutputFile; /** * An XML file to render to. If omitted, no output is rendered to an XML file. * * @parameter expression="${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. <code>org/codehaus/mojo/**</code>. * * @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. <code>org/codehaus/mojo/**</code>. * * @parameter */ protected String[] excludes; /** * Whether to log the results to the console or not. * * @parameter expression="${logResults}" default-value="false" */ protected boolean logResults; 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 XmlDiffListener( 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 ) ); reportDiffs( checker, origClasses, currentClasses ); return listener; } 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. if ("jar".equals(artifact.getType())) { 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 <code>artifacts</code>, * but excludes the artifacts in <code>previousArtifacts</code>. 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 * <code>artifacts</code>. * @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 JavaType[] origClasses2 = new JavaType[origClasses.length - 1]; int j = 0; for ( int i = 0; i < origClasses.length; i++ ) { if ( !e.getMessage().endsWith( origClasses[i].getName() ) ) { origClasses2[j++] = origClasses[i]; } } JavaType[] currentClasses2 = new JavaType[currentClasses.length - 1]; j = 0; for ( int i = 0; i < currentClasses.length; i++ ) { if ( !e.getMessage().endsWith( currentClasses[i].getName() ) ) { currentClasses2[j++] = currentClasses[i]; } } reportDiffs( checker, origClasses2, currentClasses2 ); } } }