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