    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one
    3    * or more contributor license agreements.  See the NOTICE file
    4    * distributed with this work for additional information
    5    * regarding copyright ownership.  The ASF licenses this file
    6    * to you under the Apache License, Version 2.0 (the
    7    * "License"); you may not use this file except in compliance
    8    * with the License.  You may obtain a copy of the License at
    9    *
    10   *   http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing,
    13   * software distributed under the License is distributed on an
    14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    15   * KIND, either express or implied.  See the License for the
    16   * specific language governing permissions and limitations
    17   * under the License.
    18   */
    19  package org.apache.felix.bundleplugin.baseline;
    20
    21  import java.io.File;
    22  import java.io.IOException;
    23  import java.text.SimpleDateFormat;
    24  import java.util.Arrays;
    25  import java.util.Date;
    26  import java.util.Iterator;
    27  import java.util.List;
    28  import java.util.Map;
    29  import java.util.Set;
    30
    31  import org.apache.maven.artifact.Artifact;
    32  import org.apache.maven.artifact.factory.ArtifactFactory;
    33  import org.apache.maven.artifact.metadata.ArtifactMetadataRetrievalException;
    34  import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
    35  import org.apache.maven.artifact.repository.ArtifactRepository;
    36  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
    37  import org.apache.maven.artifact.resolver.ArtifactResolutionException;
    38  import org.apache.maven.artifact.resolver.ArtifactResolver;
    39  import org.apache.maven.artifact.versioning.ArtifactVersion;
    40  import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
    41  import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
    42  import org.apache.maven.artifact.versioning.VersionRange;
    43  import org.apache.maven.plugin.AbstractMojo;
    44  import org.apache.maven.plugin.MojoExecutionException;
    45  import org.apache.maven.plugin.MojoFailureException;
    46  import org.apache.maven.project.MavenProject;
    47  import org.codehaus.plexus.util.StringUtils;
    48
    49  import aQute.bnd.differ.Baseline;
    50  import aQute.bnd.differ.Baseline.Info;
    51  import aQute.bnd.differ.DiffPluginImpl;
    52  import aQute.bnd.osgi.Instructions;
    53  import aQute.bnd.osgi.Jar;
    54  import aQute.bnd.osgi.Processor;
    55  import aQute.bnd.service.diff.Delta;
    56  import aQute.bnd.service.diff.Diff;
    57  import aQute.bnd.version.Version;
    58  import aQute.service.reporter.Reporter;
    59
    60  /**
    61   * Abstract BND Baseline check between two bundles.
    62   */
    63  abstract class AbstractBaselinePlugin
    64      extends AbstractMojo
    65  {
    66
    67      /**
    68       * Flag to easily skip execution.
    69       *
    70       * @parameter expression="${baseline.skip}" default-value="false"
    71       */
    72      protected boolean skip;
    73
    74      /**
    75       * Whether to fail on errors.
    76       *
    77       * @parameter expression="${baseline.failOnError}" default-value="true"
    78       */
    79      protected boolean failOnError;
    80
    81      /**
    82       * Whether to fail on warnings.
    83       *
    84       * @parameter expression="${baseline.failOnWarning}" default-value="false"
    85       */
    86      protected boolean failOnWarning;
    87
    88      /**
    89       * @parameter expression="${project}"
    90       * @required
    91       * @readonly
    92       */
    93      protected MavenProject project;
    94
    95      /**
    96       * @parameter expression="${project.build.directory}"
    97       * @required
    98       * @readonly
    99       */
    100     private File buildDirectory;
    101
    102     /**
    103      * @parameter expression="${project.build.finalName}"
    104      * @required
    105      * @readonly
    106      */
    107     private String finalName;
    108
    109     /**
    110      * @component
    111      */
    112     protected ArtifactResolver resolver;
    113
    114     /**
    115      * @component
    116      */
    117     protected ArtifactFactory factory;
    118
    119     /**
    120      * @parameter default-value="${localRepository}"
    121      * @required
    122      * @readonly
    123      */
    124     protected ArtifactRepository localRepository;
    125
    126     /**
    127      * @component
    128      */
    129     private ArtifactMetadataSource metadataSource;
    130
    131     /**
    132      * Version to compare the current code against.
    133      *
    134      * @parameter expression="${comparisonVersion}" default-value="(,${project.version})"
    135      * @required
    136      * @readonly
    137      */
    138     protected String comparisonVersion;
    139
    140     /**
    141      * A list of packages filter, if empty the whole bundle will be traversed. Values are specified in OSGi package
    142      * instructions notation, e.g. <code>!org.apache.felix.bundleplugin</code>.
    143      *
    144      * @parameter
    145      */
    146     private String[] filters;
    147
    148     /**
    149      * Project types which this plugin supports.
    150      *
    151      * @parameter
    152      */
    153     protected List supportedProjectTypes = Arrays.asList( new String[] { "jar", "bundle" } );
    154
    155     public final void execute()
    156         throws MojoExecutionException, MojoFailureException
    157     {
    158         if ( skip )
    159         {
    160             getLog().info( "Skipping Baseline execution" );
    161             return;
    162         }
    163
    164         if ( !supportedProjectTypes.contains( project.getArtifact().getType() ) )
    165         {
    166             getLog().info("Skipping Baseline (project type " + project.getArtifact().getType() + " not supported)");
    167             return;
    168         }
    169
    170         // get the bundles that have to be compared
    171
    172         final Jar currentBundle = getCurrentBundle();
    173         if ( currentBundle == null )
    174         {
    175             getLog().info( "Not generating Baseline report as there is no bundle generated by the project" );
    176             return;
    177         }
    178
    179         final Jar previousBundle = getPreviousBundle();
    180         if ( previousBundle == null )
    181         {
    182             getLog().info( "Not generating Baseline report as there is no previous version of the library to compare against" );
    183             return;
    184         }
    185
    186         // preparing the filters
    187
    188         final Instructions packageFilters;
    189         if ( filters == null || filters.length == 0 )
    190         {
    191             packageFilters = new Instructions();
    192         }
    193         else
    194         {
    195             packageFilters = new Instructions( Arrays.asList( filters ) );
    196         }
    197
    198         // go!
    199
    200         init();
    201
    202         String generationDate = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm'Z'" ).format( new Date() );
    203         Reporter reporter = new Processor();
    204
    205         try
    206         {
    207             Set<Info> infoSet = new Baseline( reporter, new DiffPluginImpl() )
    208                                 .baseline( currentBundle, previousBundle, packageFilters );
    209
    210             startBaseline( generationDate, project.getArtifactId(), project.getVersion(), comparisonVersion );
    211
    212             final Info[] infos = infoSet.toArray( new Info[infoSet.size()] );
    213             Arrays.sort( infos, new InfoComparator() );
    214
    215             for ( Info info : infos )
    216             {
    217                 DiffMessage diffMessage = null;
    218                 Version newerVersion = info.newerVersion;
    219                 Version suggestedVersion = info.suggestedVersion;
    220
    221                 if ( suggestedVersion != null )
    222                 {
    223                     if ( newerVersion.compareTo( suggestedVersion ) > 0 )
    224                     {
    225                         diffMessage = new DiffMessage( "Excessive version increase", DiffMessage.Type.warning );
    226                         reporter.warning( "%s: %s; detected %s, suggested %s",
    227                                           info.packageName, diffMessage, info.newerVersion, info.suggestedVersion );
    228                     }
    229                     else if ( newerVersion.compareTo( suggestedVersion ) < 0 )
    230                     {
    231                         diffMessage = new DiffMessage( "Version increase required", DiffMessage.Type.error );
    232                         reporter.error( "%s: %s; detected %s, suggested %s",
    233                                         info.packageName, diffMessage, info.newerVersion, info.suggestedVersion );
    234                     }
    235                 }
    236
    237                 Diff packageDiff = info.packageDiff;
    238
    239                 Delta delta = packageDiff.getDelta();
    240
    241                 switch ( delta )
    242                 {
    243                     case UNCHANGED:
    244                         if ( ( suggestedVersion.getMajor() != newerVersion.getMajor() )
    245                             || ( suggestedVersion.getMicro() != newerVersion.getMicro() )
    246                             || ( suggestedVersion.getMinor() != newerVersion.getMinor() ) )
    247                         {
    248                             diffMessage = new DiffMessage( "Version has been increased but analysis detected no changes", DiffMessage.Type.warning );
    249                             reporter.warning( "%s: %s; detected %s, suggested %s",
    250                                               info.packageName, diffMessage, info.newerVersion, info.suggestedVersion );
    251                         }
    252                         break;
    253
    254                     case REMOVED:
    255                         diffMessage = new DiffMessage( "Package removed", DiffMessage.Type.info );
    256                         reporter.trace( "%s: %s ", info.packageName, diffMessage );
    257                         break;
    258
    259                     case CHANGED:
    260                     case MICRO:
    261                     case MINOR:
    262                     case MAJOR:
    263                     case ADDED:
    264                     default:
    265                         // ok
    266                         break;
    267                 }
    268
    269                 boolean mismatch = info.mismatch;
    270                 String packageName = info.packageName;
    271                 String shortDelta = getShortDelta( info.packageDiff.getDelta() );
    272                 String deltaString = StringUtils.lowerCase( String.valueOf( info.packageDiff.getDelta() ) );
    273                 String newerVersionString = String.valueOf( info.newerVersion );
    274                 String olderVersionString = String.valueOf( info.olderVersion );
    275                 String suggestedVersionString = String.valueOf( ( info.suggestedVersion == null ) ? "-" : info.suggestedVersion );
    276                 Map<String,String> attributes = info.attributes;
    277
    278                 startPackage( mismatch,
    279                               packageName,
    280                               shortDelta,
    281                               deltaString,
    282                               newerVersionString,
    283                               olderVersionString,
    284                               suggestedVersionString,
    285                               diffMessage,
    286                               attributes );
    287
    288                 if ( Delta.REMOVED != delta )
    289                 {
    290                     doPackageDiff( packageDiff );
    291                 }
    292
    293                 endPackage();
    294             }
    295
    296             endBaseline();
    297         }
    298         catch ( Exception e )
    299         {
    300             throw new MojoExecutionException( "Impossible to calculate the baseline", e );
    301         }
    302         finally
    303         {
    304             closeJars( currentBundle, previousBundle );
    305         }
    306
    307         // check if it has to fail if some error has been detected
    308
    309         boolean fail = false;
    310
    311         if ( !reporter.isOk() )
    312         {
    313             for ( String errorMessage : reporter.getErrors() )
    314             {
    315                 getLog().error( errorMessage );
    316             }
    317
    318             if ( failOnError )
    319             {
    320                 fail = true;
    321             }
    322         }
    323
    324         // check if it has to fail if some warning has been detected
    325
    326         if ( !reporter.getWarnings().isEmpty() )
    327         {
    328             for ( String warningMessage : reporter.getWarnings() )
    329             {
    330                 getLog().warn( warningMessage );
    331             }
    332
    333             if ( failOnWarning )
    334             {
    335                 fail = true;
    336             }
    337         }
    338
    339         getLog().info( String.format( "Baseline analisys complete, %s error(s), %s warning(s)",
    340                                       reporter.getErrors().size(),
    341                                       reporter.getWarnings().size() ) );
    342
    343         if ( fail )
    344         {
    345             throw new MojoFailureException( "Baseline failed, see generated report" );
    346         }
    347     }
    348
    349     private void doPackageDiff( Diff diff )
    350     {
    351         int depth = 1;
    352
    353         for ( Diff curDiff : diff.getChildren() )
    354         {
    355             if ( Delta.UNCHANGED != curDiff.getDelta() )
    356             {
    357                 doDiff( curDiff, depth );
    358             }
    359         }
    360     }
    361
    362     private void doDiff( Diff diff, int depth )
    363     {
    364         String type = StringUtils.lowerCase( String.valueOf( diff.getType() ) );
    365         String shortDelta = getShortDelta( diff.getDelta() );
    366         String delta = StringUtils.lowerCase( String.valueOf( diff.getDelta() ) );
    367         String name = diff.getName();
    368
    369         startDiff( depth, type, name, delta, shortDelta );
    370
    371         for ( Diff curDiff : diff.getChildren() )
    372         {
    373             if ( Delta.UNCHANGED != curDiff.getDelta() )
    374             {
    375                 doDiff( curDiff, depth + 1 );
    376             }
    377         }
    378
    379         endDiff( depth );
    380     }
    381
    382     // extensions APIs
    383
    384     protected abstract void init();
    385
    386     protected abstract void startBaseline( String generationDate, String bundleName, String currentVersion, String previousVersion );
    387
    388     protected abstract void startPackage( boolean mismatch,
    389                                           String name,
    390                                           String shortDelta,
    391                                           String delta,
    392                                           String newerVersion,
    393                                           String olderVersion,
    394                                           String suggestedVersion,
    395                                           DiffMessage diffMessage,
    396                                           Map<String,String> attributes );
    397
    398     protected abstract void startDiff( int depth,
    399                                        String type,
    400                                        String name,
    401                                        String delta,
    402                                        String shortDelta );
    403
    404     protected abstract void endDiff( int depth );
    405
    406     protected abstract void endPackage();
    407
    408     protected abstract void endBaseline();
    409
    410     // internals
    411
    412     private Jar getCurrentBundle()
    413         throws MojoExecutionException
    414     {
    415         /*
    416          * Resolving the aQute.bnd.osgi.Jar via the produced artifact rather than what is produced in the target/classes
    417          * directory would make the Mojo working also in projects where the bundle-plugin is used just to generate the
    418          * manifest file and the final jar is assembled via the jar-plugin
    419          */
    420         File currentBundle = new File( buildDirectory, getBundleName() );
    421         if ( !currentBundle.exists() )
    422         {
    423             getLog().debug( "Produced bundle not found: " + currentBundle );
    424             return null;
    425         }
    426
    427         return openJar( currentBundle );
    428     }
    429
    430     private Jar getPreviousBundle()
    431         throws MojoFailureException, MojoExecutionException
    432     {
    433         // Find the previous version JAR and resolve it, and it's dependencies
    434         final VersionRange range;
    435         try
    436         {
    437             range = VersionRange.createFromVersionSpec( comparisonVersion );
    438         }
    439         catch ( InvalidVersionSpecificationException e )
    440         {
    441             throw new MojoFailureException( "Invalid comparison version: " + e.getMessage() );
    442         }
    443
    444         final Artifact previousArtifact;
    445         try
    446         {
    447             previousArtifact =
    448                 factory.createDependencyArtifact( project.getGroupId(),
    449                                                   project.getArtifactId(),
    450                                                   range,
    451                                                   project.getPackaging(),
    452                                                   null,
    453                                                   Artifact.SCOPE_COMPILE );
    454
    455             if ( !previousArtifact.getVersionRange().isSelectedVersionKnown( previousArtifact ) )
    456             {
    457                 getLog().debug( "Searching for versions in range: " + previousArtifact.getVersionRange() );
    458                 @SuppressWarnings( "unchecked" )
    459                 // type is konwn
    460                 List<ArtifactVersion> availableVersions =
    461                     metadataSource.retrieveAvailableVersions( previousArtifact, localRepository,
    462                                                               project.getRemoteArtifactRepositories() );
    463                 filterSnapshots( availableVersions );
    464                 ArtifactVersion version = range.matchVersion( availableVersions );
    465                 if ( version != null )
    466                 {
    467                     previousArtifact.selectVersion( version.toString() );
    468                 }
    469             }
    470         }
    471         catch ( OverConstrainedVersionException ocve )
    472         {
    473             throw new MojoFailureException( "Invalid comparison version: " + ocve.getMessage() );
    474         }
    475         catch ( ArtifactMetadataRetrievalException amre )
    476         {
    477             throw new MojoExecutionException( "Error determining previous version: " + amre.getMessage(), amre );
    478         }
    479
    480         if ( previousArtifact.getVersion() == null )
    481         {
    482             getLog().info( "Unable to find a previous version of the project in the repository" );
    483             return null;
    484         }
    485
    486         try
    487         {
    488             resolver.resolve( previousArtifact, project.getRemoteArtifactRepositories(), localRepository );
    489         }
    490         catch ( ArtifactResolutionException are )
    491         {
    492             throw new MojoExecutionException( "Artifact " + previousArtifact + " cannot be resolved", are );
    493         }
    494         catch ( ArtifactNotFoundException anfe )
    495         {
    496             throw new MojoExecutionException( "Artifact " + previousArtifact
    497                 + " does not exist on local/remote repositories", anfe );
    498         }
    499
    500         return openJar( previousArtifact.getFile() );
    501     }
    502
    503     private void filterSnapshots( List<ArtifactVersion> versions )
    504     {
    505         for ( Iterator<ArtifactVersion> versionIterator = versions.iterator(); versionIterator.hasNext(); )
    506         {
    507             ArtifactVersion version = versionIterator.next();
    508             if ( "SNAPSHOT".equals( version.getQualifier() ) )
    509             {
    510                 versionIterator.remove();
    511             }
    512         }
    513     }
    514
    515     private static Jar openJar( File file )
    516         throws MojoExecutionException
    517     {
    518         try
    519         {
    520             return new Jar( file );
    521         }
    522         catch ( IOException e )
    523         {
    524             throw new MojoExecutionException( "An error occurred while opening JAR directory: " + file, e );
    525         }
    526     }
    527
    528     private static void closeJars( Jar...jars )
    529     {
    530         for ( Jar jar : jars )
    531         {
    532             jar.close();
    533         }
    534     }
    535
    536     private String getBundleName()
    537     {
    538         String extension;
    539         try
    540         {
    541             extension = project.getArtifact().getArtifactHandler().getExtension();
    542         }
    543         catch ( Throwable e )
    544         {
    545             extension = project.getArtifact().getType();
    546         }
    547
    548         if ( StringUtils.isEmpty( extension ) || "bundle".equals( extension ) || "pom".equals( extension ) )
    549         {
    550             extension = "jar"; // just in case maven gets confused
    551         }
    552
    553         String classifier = project.getArtifact().getClassifier();
    554         if ( null != classifier && classifier.trim().length() > 0 )
    555         {
    556             return finalName + '-' + classifier + '.' + extension;
    557         }
    558
    559         return finalName + '.' + extension;
    560     }
    561
    562     private static String getShortDelta( Delta delta )
    563     {
    564         switch ( delta )
    565         {
    566             case ADDED:
    567                 return "+";
    568
    569             case CHANGED:
    570                 return "~";
    571
    572             case MAJOR:
    573                 return ">";
    574
    575             case MICRO:
    576                 return "0xB5";
    577
    578             case MINOR:
    579                 return "<";
    580
    581             case REMOVED:
    582                 return "-";
    583
    584             case UNCHANGED:
    585                 return " ";
    586
    587             default:
    588                 String deltaString = delta.toString();
    589                 return String.valueOf( deltaString.charAt( 0 ) );
    590         }
    591     }
    592
    593 }