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
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
64
65 abstract class AbstractBaselinePlugin
66 extends AbstractMojo
67 {
68
69
70
71
72 @Parameter( property = "baseline.skip", defaultValue = "false" )
73 protected boolean skip;
74
75
76
77
78 @Parameter( property = "baseline.failOnError", defaultValue = "true" )
79 protected boolean failOnError;
80
81
82
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
110
111 @Parameter( defaultValue = "${project.groupId}", property="comparisonGroupId" )
112 protected String comparisonGroupId;
113
114
115
116
117 @Parameter( defaultValue = "${project.artifactId}", property="comparisonArtifactId" )
118 protected String comparisonArtifactId;
119
120
121
122
123 @Parameter( defaultValue = "(,${project.version})", property="comparisonVersion" )
124 protected String comparisonVersion;
125
126
127
128
129 @Parameter( defaultValue = "${project.packaging}", property="comparisonPackaging" )
130 protected String comparisonPackaging;
131
132
133
134
135 @Parameter( property="comparisonClassifier" )
136 protected String comparisonClassifier;
137
138
139
140
141
142 @Parameter
143 private String[] filters;
144
145
146
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
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
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
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
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
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
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
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
418
419 private Jar getCurrentBundle()
420 throws MojoExecutionException
421 {
422
423
424
425
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
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
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";
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 }