blob: 43027bee65fb6a688381043bfbea2bf26c5c5dcf [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 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.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.codehaus.plexus.util.StringUtils;
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;
/**
* Abstract BND Baseline check between two bundles.
*/
abstract class AbstractBaselinePlugin
extends AbstractMojo
{
/**
* Flag to easily skip execution.
*
* @parameter expression="${baseline.skip}" default-value="false"
*/
protected boolean skip;
/**
* Whether to fail on errors.
*
* @parameter expression="${baseline.failOnError}" default-value="true"
*/
protected boolean failOnError;
/**
* Whether to fail on warnings.
*
* @parameter expression="${baseline.failOnWarning}" default-value="false"
*/
protected boolean failOnWarning;
/**
* @parameter expression="${project}"
* @required
* @readonly
*/
protected MavenProject project;
/**
* @parameter expression="${project.build.directory}"
* @required
* @readonly
*/
private File buildDirectory;
/**
* @parameter expression="${project.build.finalName}"
* @required
* @readonly
*/
private String finalName;
/**
* @component
*/
protected ArtifactResolver resolver;
/**
* @component
*/
protected ArtifactFactory factory;
/**
* @parameter default-value="${localRepository}"
* @required
* @readonly
*/
protected ArtifactRepository localRepository;
/**
* @component
*/
private ArtifactMetadataSource metadataSource;
/**
* Version to compare the current code against.
*
* @parameter expression="${comparisonVersion}" default-value="(,${project.version})"
* @required
* @readonly
*/
protected String comparisonVersion;
/**
* 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;
public final void execute()
throws MojoExecutionException, MojoFailureException
{
if ( skip )
{
getLog().info( "Skipping Baseline execution" );
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 Jar previousBundle = getPreviousBundle();
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 ) );
}
// go!
init();
String generationDate = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm'Z'" ).format( new Date() );
Reporter reporter = new Processor();
try
{
Set<Info> infoSet = new Baseline( reporter, new DiffPluginImpl() )
.baseline( currentBundle, previousBundle, packageFilters );
startBaseline( generationDate, project.getArtifactId(), project.getVersion(), comparisonVersion );
final Info[] infos = infoSet.toArray( new Info[infoSet.size()] );
Arrays.sort( infos, new InfoComparator() );
for ( Info info : infos )
{
DiffMessage diffMessage = null;
Version newerVersion = info.newerVersion;
Version suggestedVersion = info.suggestedVersion;
if ( suggestedVersion != null )
{
if ( newerVersion.compareTo( 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 ( newerVersion.compareTo( 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 );
}
}
Diff packageDiff = info.packageDiff;
Delta delta = packageDiff.getDelta();
switch ( delta )
{
case UNCHANGED:
if ( ( suggestedVersion.getMajor() != newerVersion.getMajor() )
|| ( suggestedVersion.getMicro() != newerVersion.getMicro() )
|| ( suggestedVersion.getMinor() != newerVersion.getMinor() ) )
{
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;
}
boolean mismatch = info.mismatch;
String packageName = info.packageName;
String shortDelta = getShortDelta( info.packageDiff.getDelta() );
String deltaString = StringUtils.lowerCase( String.valueOf( info.packageDiff.getDelta() ) );
String newerVersionString = String.valueOf( info.newerVersion );
String olderVersionString = String.valueOf( info.olderVersion );
String suggestedVersionString = String.valueOf( ( info.suggestedVersion == null ) ? "-" : info.suggestedVersion );
Map<String,String> attributes = info.attributes;
startPackage( mismatch,
packageName,
shortDelta,
deltaString,
newerVersionString,
olderVersionString,
suggestedVersionString,
diffMessage,
attributes );
if ( Delta.REMOVED != delta )
{
doPackageDiff( packageDiff );
}
endPackage();
}
endBaseline();
}
catch ( Exception e )
{
throw new MojoExecutionException( "Impossible to calculate the baseline", e );
}
finally
{
closeJars( currentBundle, previousBundle );
}
// 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 analisys 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( Diff diff )
{
int depth = 1;
for ( Diff curDiff : diff.getChildren() )
{
if ( Delta.UNCHANGED != curDiff.getDelta() )
{
doDiff( curDiff, depth );
}
}
}
private void doDiff( 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( depth, type, name, delta, shortDelta );
for ( Diff curDiff : diff.getChildren() )
{
if ( Delta.UNCHANGED != curDiff.getDelta() )
{
doDiff( curDiff, depth + 1 );
}
}
endDiff( depth );
}
// extensions APIs
protected abstract void init();
protected abstract void startBaseline( String generationDate, String bundleName, String currentVersion, String previousVersion );
protected abstract void startPackage( boolean mismatch,
String name,
String shortDelta,
String delta,
String newerVersion,
String olderVersion,
String suggestedVersion,
DiffMessage diffMessage,
Map<String,String> attributes );
protected abstract void startDiff( int depth,
String type,
String name,
String delta,
String shortDelta );
protected abstract void endDiff( int depth );
protected abstract void endPackage();
protected abstract void endBaseline();
// 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 Jar getPreviousBundle()
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( 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() );
@SuppressWarnings( "unchecked" )
// type is konwn
List<ArtifactVersion> availableVersions =
metadataSource.retrieveAvailableVersions( previousArtifact, localRepository,
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(), localRepository );
}
catch ( ArtifactResolutionException are )
{
throw new MojoExecutionException( "Artifact " + previousArtifact + " cannot be resolved", are );
}
catch ( ArtifactNotFoundException anfe )
{
throw new MojoExecutionException( "Artifact " + previousArtifact
+ " does not exist on local/remote repositories", anfe );
}
return openJar( previousArtifact.getFile() );
}
private void filterSnapshots( List<ArtifactVersion> versions )
{
for ( Iterator<ArtifactVersion> versionIterator = versions.iterator(); versionIterator.hasNext(); )
{
ArtifactVersion version = versionIterator.next();
if ( "SNAPSHOT".equals( version.getQualifier() ) )
{
versionIterator.remove();
}
}
}
private static Jar openJar( File file )
throws MojoExecutionException
{
try
{
return new Jar( file );
}
catch ( IOException e )
{
throw new MojoExecutionException( "An error occurred while opening JAR directory: " + file, e );
}
}
private static void closeJars( 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 = 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 ) );
}
}
}