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