blob: 7be84d4d5c6fbcefcfdcff760ba5d4c61a8a16fc [file] [log] [blame]
/*
* 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.
*/
package org.apache.felix.bundleplugin.baseline;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import aQute.bnd.differ.Baseline;
import aQute.bnd.differ.Baseline.Info;
import aQute.bnd.differ.DiffPluginImpl;
import aQute.bnd.osgi.Instructions;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Processor;
import aQute.bnd.service.diff.Delta;
import aQute.bnd.service.diff.Diff;
import aQute.bnd.version.Version;
import aQute.service.reporter.Reporter;
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.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
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.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.StringUtils;
/**
* Abstract BND Baseline check between two bundles.
*/
abstract class AbstractBaselinePlugin
extends AbstractMojo
{
/**
* Flag to easily skip execution.
*/
@Parameter( property = "baseline.skip", defaultValue = "false" )
protected boolean skip;
/**
* Whether to fail on errors.
*/
@Parameter( property = "baseline.failOnError", defaultValue = "true" )
protected boolean failOnError;
/**
* Whether to fail on warnings.
*/
@Parameter( property = "baseline.failOnWarning", defaultValue = "false" )
protected boolean failOnWarning;
@Parameter( defaultValue = "${project}", readonly = true, required = true )
protected MavenProject project;
@Parameter( defaultValue = "${session}", readonly = true, required = true )
protected MavenSession session;
@Parameter( defaultValue = "${project.build.directory}", readonly = true, required = true )
private File buildDirectory;
@Parameter( defaultValue = "${project.build.finalName}", readonly = true, required = true )
private String finalName;
@Component
protected ArtifactResolver resolver;
@Component
protected ArtifactFactory factory;
@Component
private ArtifactMetadataSource metadataSource;
/**
* Group id to compare the current code against.
*/
@Parameter( defaultValue = "${project.groupId}", property="comparisonGroupId" )
protected String comparisonGroupId;
/**
* Artifact to compare the current code against.
*/
@Parameter( defaultValue = "${project.artifactId}", property="comparisonArtifactId" )
protected String comparisonArtifactId;
/**
* Version to compare the current code against.
*/
@Parameter( defaultValue = "(,${project.version})", property="comparisonVersion" )
protected String comparisonVersion;
/**
* Artifact to compare the current code against.
*/
@Parameter( defaultValue = "${project.packaging}", property="comparisonPackaging" )
protected String comparisonPackaging;
/**
* Classifier for the artifact to compare the current code against.
*/
@Parameter( property="comparisonClassifier" )
protected String comparisonClassifier;
/**
* A list of packages filter, if empty the whole bundle will be traversed. Values are specified in OSGi package
* instructions notation, e.g. <code>!org.apache.felix.bundleplugin</code>.
*/
@Parameter
private String[] filters;
/**
* Project types which this plugin supports.
*/
@Parameter
protected List<String> supportedProjectTypes = Arrays.asList( new String[] { "jar", "bundle" } );
public final void execute()
throws MojoExecutionException, MojoFailureException
{
this.execute(null);
}
protected void execute( Object context)
throws MojoExecutionException, MojoFailureException
{
if ( skip )
{
getLog().info( "Skipping Baseline execution" );
return;
}
if ( !supportedProjectTypes.contains( project.getArtifact().getType() ) )
{
getLog().info("Skipping Baseline (project type " + project.getArtifact().getType() + " not supported)");
return;
}
// get the bundles that have to be compared
final Jar currentBundle = getCurrentBundle();
if ( currentBundle == null )
{
getLog().info( "Not generating Baseline report as there is no bundle generated by the project" );
return;
}
final Artifact previousArtifact = getPreviousArtifact();
final Jar previousBundle;
if (previousArtifact != null)
{
previousBundle = openJar(previousArtifact.getFile());
}
else
{
previousBundle = null;
}
if ( previousBundle == null )
{
getLog().info( "Not generating Baseline report as there is no previous version of the library to compare against" );
return;
}
// preparing the filters
final Instructions packageFilters;
if ( filters == null || filters.length == 0 )
{
packageFilters = new Instructions();
}
else
{
packageFilters = new Instructions( Arrays.asList( filters ) );
}
String generationDate = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm'Z'" ).format( new Date() );
final Reporter reporter = new Processor();
final Info[] infos;
try
{
final Set<Info> infoSet = new Baseline( reporter, new DiffPluginImpl() )
.baseline( currentBundle, previousBundle, packageFilters );
infos = infoSet.toArray( new Info[infoSet.size()] );
Arrays.sort( infos, new InfoComparator() );
}
catch ( final Exception e )
{
throw new MojoExecutionException( "Impossible to calculate the baseline", e );
}
finally
{
closeJars( currentBundle, previousBundle );
}
try
{
// go!
context = this.init(context);
startBaseline( context, generationDate, project.getArtifactId(), project.getVersion(), previousArtifact.getVersion() );
for ( final Info info : infos )
{
DiffMessage diffMessage = null;
if ( info.suggestedVersion != null )
{
if ( info.newerVersion.compareTo( info.suggestedVersion ) > 0 )
{
diffMessage = new DiffMessage( "Excessive version increase", DiffMessage.Type.warning );
reporter.warning( "%s: %s; detected %s, suggested %s",
info.packageName, diffMessage, info.newerVersion, info.suggestedVersion );
}
else if ( info.newerVersion.compareTo( info.suggestedVersion ) < 0 )
{
diffMessage = new DiffMessage( "Version increase required", DiffMessage.Type.error );
reporter.error( "%s: %s; detected %s, suggested %s",
info.packageName, diffMessage, info.newerVersion, info.suggestedVersion );
}
}
switch ( info.packageDiff.getDelta() )
{
case UNCHANGED:
if ( info.newerVersion.compareTo( info.suggestedVersion ) != 0 )
{
diffMessage = new DiffMessage( "Version has been increased but analysis detected no changes", DiffMessage.Type.warning );
reporter.warning( "%s: %s; detected %s, suggested %s",
info.packageName, diffMessage, info.newerVersion, info.suggestedVersion );
}
break;
case REMOVED:
diffMessage = new DiffMessage( "Package removed", DiffMessage.Type.info );
reporter.trace( "%s: %s ", info.packageName, diffMessage );
break;
case CHANGED:
case MICRO:
case MINOR:
case MAJOR:
case ADDED:
default:
// ok
break;
}
startPackage( context,
info.mismatch,
info.packageName,
getShortDelta( info.packageDiff.getDelta() ),
StringUtils.lowerCase( String.valueOf( info.packageDiff.getDelta() ) ),
info.newerVersion,
info.olderVersion,
info.suggestedVersion,
diffMessage,
info.attributes );
if ( Delta.REMOVED != info.packageDiff.getDelta() )
{
doPackageDiff( context, info.packageDiff );
}
endPackage(context);
}
endBaseline(context);
}
finally
{
this.close(context);
}
// check if it has to fail if some error has been detected
boolean fail = false;
if ( !reporter.isOk() )
{
for ( String errorMessage : reporter.getErrors() )
{
getLog().error( errorMessage );
}
if ( failOnError )
{
fail = true;
}
}
// check if it has to fail if some warning has been detected
if ( !reporter.getWarnings().isEmpty() )
{
for ( String warningMessage : reporter.getWarnings() )
{
getLog().warn( warningMessage );
}
if ( failOnWarning )
{
fail = true;
}
}
getLog().info( String.format( "Baseline analysis complete, %s error(s), %s warning(s)",
reporter.getErrors().size(),
reporter.getWarnings().size() ) );
if ( fail )
{
throw new MojoFailureException( "Baseline failed, see generated report" );
}
}
private void doPackageDiff( Object context, Diff diff )
{
int depth = 1;
for ( Diff curDiff : diff.getChildren() )
{
if ( Delta.UNCHANGED != curDiff.getDelta() )
{
doDiff( context, curDiff, depth );
}
}
}
private void doDiff( Object context, Diff diff, int depth )
{
String type = StringUtils.lowerCase( String.valueOf( diff.getType() ) );
String shortDelta = getShortDelta( diff.getDelta() );
String delta = StringUtils.lowerCase( String.valueOf( diff.getDelta() ) );
String name = diff.getName();
startDiff( context, depth, type, name, delta, shortDelta );
for ( Diff curDiff : diff.getChildren() )
{
if ( Delta.UNCHANGED != curDiff.getDelta() )
{
doDiff( context, curDiff, depth + 1 );
}
}
endDiff( context, depth );
}
// extensions APIs
protected abstract Object init(final Object initialContext);
protected abstract void close(final Object context);
protected abstract void startBaseline( final Object context, String generationDate, String bundleName, String currentVersion, String previousVersion );
protected abstract void startPackage( final Object context,
boolean mismatch,
String name,
String shortDelta,
String delta,
Version newerVersion,
Version olderVersion,
Version suggestedVersion,
DiffMessage diffMessage,
Map<String,String> attributes );
protected abstract void startDiff( final Object context,
int depth,
String type,
String name,
String delta,
String shortDelta );
protected abstract void endDiff( final Object context, int depth );
protected abstract void endPackage(final Object context);
protected abstract void endBaseline(final Object context);
// internals
private Jar getCurrentBundle()
throws MojoExecutionException
{
/*
* Resolving the aQute.bnd.osgi.Jar via the produced artifact rather than what is produced in the target/classes
* directory would make the Mojo working also in projects where the bundle-plugin is used just to generate the
* manifest file and the final jar is assembled via the jar-plugin
*/
File currentBundle = new File( buildDirectory, getBundleName() );
if ( !currentBundle.exists() )
{
getLog().debug( "Produced bundle not found: " + currentBundle );
return null;
}
return openJar( currentBundle );
}
private Artifact getPreviousArtifact()
throws MojoFailureException, MojoExecutionException
{
// Find the previous version JAR and resolve it, and it's dependencies
final VersionRange range;
try
{
range = VersionRange.createFromVersionSpec( comparisonVersion );
}
catch ( InvalidVersionSpecificationException e )
{
throw new MojoFailureException( "Invalid comparison version: " + e.getMessage() );
}
final Artifact previousArtifact;
try
{
previousArtifact =
factory.createDependencyArtifact( comparisonGroupId,
comparisonArtifactId,
range,
comparisonPackaging,
comparisonClassifier,
Artifact.SCOPE_COMPILE );
if ( !previousArtifact.getVersionRange().isSelectedVersionKnown( previousArtifact ) )
{
getLog().debug( "Searching for versions in range: " + previousArtifact.getVersionRange() );
@SuppressWarnings( "unchecked" )
// type is konwn
List<ArtifactVersion> availableVersions =
metadataSource.retrieveAvailableVersions( previousArtifact, session.getLocalRepository(),
project.getRemoteArtifactRepositories() );
filterSnapshots( availableVersions );
ArtifactVersion version = range.matchVersion( availableVersions );
if ( version != null )
{
previousArtifact.selectVersion( version.toString() );
}
}
}
catch ( OverConstrainedVersionException ocve )
{
throw new MojoFailureException( "Invalid comparison version: " + ocve.getMessage() );
}
catch ( ArtifactMetadataRetrievalException amre )
{
throw new MojoExecutionException( "Error determining previous version: " + amre.getMessage(), amre );
}
if ( previousArtifact.getVersion() == null )
{
getLog().info( "Unable to find a previous version of the project in the repository" );
return null;
}
try
{
resolver.resolve( previousArtifact, project.getRemoteArtifactRepositories(), session.getLocalRepository() );
}
catch ( ArtifactResolutionException are )
{
throw new MojoExecutionException( "Artifact " + previousArtifact + " cannot be resolved : " + are.getMessage(), are );
}
catch ( ArtifactNotFoundException anfe )
{
throw new MojoExecutionException( "Artifact " + previousArtifact
+ " does not exist on local/remote repositories", anfe );
}
return previousArtifact;
}
private void filterSnapshots( List<ArtifactVersion> versions )
{
for ( Iterator<ArtifactVersion> versionIterator = versions.iterator(); versionIterator.hasNext(); )
{
ArtifactVersion version = versionIterator.next();
if ( version.getQualifier().endsWith( "SNAPSHOT" ) )
{
versionIterator.remove();
}
}
}
private static Jar openJar( final File file )
throws MojoExecutionException
{
try
{
return new Jar( file );
}
catch ( final IOException e )
{
throw new MojoExecutionException( "An error occurred while opening JAR directory: " + file, e );
}
}
private static void closeJars( final Jar...jars )
{
for ( Jar jar : jars )
{
jar.close();
}
}
private String getBundleName()
{
String extension;
try
{
extension = project.getArtifact().getArtifactHandler().getExtension();
}
catch ( Throwable e )
{
extension = project.getArtifact().getType();
}
if ( StringUtils.isEmpty( extension ) || "bundle".equals( extension ) || "pom".equals( extension ) )
{
extension = "jar"; // just in case maven gets confused
}
String classifier = this.comparisonClassifier != null ? this.comparisonClassifier : project.getArtifact().getClassifier();
if ( null != classifier && classifier.trim().length() > 0 )
{
return finalName + '-' + classifier + '.' + extension;
}
return finalName + '.' + extension;
}
private static String getShortDelta( Delta delta )
{
switch ( delta )
{
case ADDED:
return "+";
case CHANGED:
return "~";
case MAJOR:
return ">";
case MICRO:
return "0xB5";
case MINOR:
return "<";
case REMOVED:
return "-";
case UNCHANGED:
return " ";
default:
String deltaString = delta.toString();
return String.valueOf( deltaString.charAt( 0 ) );
}
}
}