1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
65
66 abstract class AbstractBaselinePlugin
67 extends AbstractMojo
68 {
69
70
71
72
73 @Parameter( property = "baseline.skip", defaultValue = "false" )
74 protected boolean skip;
75
76
77
78
79 @Parameter( property = "baseline.failOnError", defaultValue = "true" )
80 protected boolean failOnError;
81
82
83
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
111
112 @Parameter( defaultValue = "${project.groupId}", property="comparisonGroupId" )
113 protected String comparisonGroupId;
114
115
116
117
118 @Parameter( defaultValue = "${project.artifactId}", property="comparisonArtifactId" )
119 protected String comparisonArtifactId;
120
121
122
123
124 @Parameter( defaultValue = "(,${project.version})", property="comparisonVersion" )
125 protected String comparisonVersion;
126
127
128
129
130 @Parameter( defaultValue = "${project.packaging}", property="comparisonPackaging" )
131 protected String comparisonPackaging;
132
133
134
135
136 @Parameter( property="comparisonClassifier" )
137 protected String comparisonClassifier;
138
139
140
141
142
143 @Parameter
144 private String[] filters;
145
146
147
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
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
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
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
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
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
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
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
422
423 private Jar getCurrentBundle()
424 throws MojoExecutionException
425 {
426
427
428
429
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
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
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";
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 }