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.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
62
63 abstract class AbstractBaselinePlugin
64 extends AbstractMojo
65 {
66
67
68
69
70
71
72 protected boolean skip;
73
74
75
76
77
78
79 protected boolean failOnError;
80
81
82
83
84
85
86 protected boolean failOnWarning;
87
88
89
90
91
92
93 protected MavenProject project;
94
95
96
97
98
99
100 private File buildDirectory;
101
102
103
104
105
106
107 private String finalName;
108
109
110
111
112 protected ArtifactResolver resolver;
113
114
115
116
117 protected ArtifactFactory factory;
118
119
120
121
122
123
124 protected ArtifactRepository localRepository;
125
126
127
128
129 private ArtifactMetadataSource metadataSource;
130
131
132
133
134
135
136
137
138 protected String comparisonVersion;
139
140
141
142
143
144
145
146 private String[] filters;
147
148
149
150
151
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
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
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
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
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
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
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
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
411
412 private Jar getCurrentBundle()
413 throws MojoExecutionException
414 {
415
416
417
418
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
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
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";
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 }