1 package org.codehaus.mojo.clirr;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 import net.sf.clirr.core.Checker;
20 import net.sf.clirr.core.CheckerException;
21 import net.sf.clirr.core.ClassFilter;
22 import net.sf.clirr.core.DiffListener;
23 import net.sf.clirr.core.PlainDiffListener;
24 import net.sf.clirr.core.Severity;
25 import net.sf.clirr.core.internal.bcel.BcelJavaType;
26 import net.sf.clirr.core.internal.bcel.BcelTypeArrayBuilder;
27 import net.sf.clirr.core.spi.JavaType;
28 import org.apache.bcel.classfile.ClassParser;
29 import org.apache.bcel.classfile.JavaClass;
30 import org.apache.bcel.util.ClassLoaderRepository;
31 import org.apache.bcel.util.Repository;
32 import org.apache.maven.artifact.Artifact;
33 import org.apache.maven.artifact.factory.ArtifactFactory;
34 import org.apache.maven.artifact.metadata.ArtifactMetadataRetrievalException;
35 import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
36 import org.apache.maven.artifact.repository.ArtifactRepository;
37 import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
38 import org.apache.maven.artifact.resolver.ArtifactResolutionException;
39 import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
40 import org.apache.maven.artifact.resolver.ArtifactResolver;
41 import org.apache.maven.artifact.versioning.ArtifactVersion;
42 import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
43 import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
44 import org.apache.maven.artifact.versioning.VersionRange;
45 import org.apache.maven.plugin.AbstractMojo;
46 import org.apache.maven.plugin.MojoExecutionException;
47 import org.apache.maven.plugin.MojoFailureException;
48 import org.apache.maven.project.MavenProject;
49 import org.apache.maven.project.MavenProjectBuilder;
50 import org.apache.maven.project.ProjectBuildingException;
51 import org.apache.maven.project.artifact.InvalidDependencyVersionException;
52 import org.codehaus.plexus.util.DirectoryScanner;
53 import org.codehaus.plexus.util.IOUtil;
54 import org.codehaus.plexus.util.ReaderFactory;
55 import org.codehaus.plexus.util.xml.XmlStreamReader;
56 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
57
58 import java.io.File;
59 import java.io.FileInputStream;
60 import java.io.IOException;
61 import java.io.InputStream;
62 import java.net.MalformedURLException;
63 import java.net.URL;
64 import java.net.URLClassLoader;
65 import java.util.ArrayList;
66 import java.util.Arrays;
67 import java.util.Collection;
68 import java.util.Collections;
69 import java.util.HashSet;
70 import java.util.Iterator;
71 import java.util.List;
72 import java.util.Set;
73
74
75
76
77
78
79
80
81 public abstract class AbstractClirrMojo
82 extends AbstractMojo
83 {
84
85
86
87
88
89 protected boolean skip;
90
91
92
93
94
95
96 protected MavenProject project;
97
98
99
100
101 protected ArtifactResolver resolver;
102
103
104
105
106 protected ArtifactFactory factory;
107
108
109
110
111
112
113 protected ArtifactRepository localRepository;
114
115
116
117
118 private ArtifactMetadataSource metadataSource;
119
120
121
122
123 private MavenProjectBuilder mavenProjectBuilder;
124
125
126
127
128
129
130 protected File classesDirectory;
131
132
133
134
135
136
137 protected String comparisonVersion;
138
139
140
141
142
143
144
145
146
147
148 protected ArtifactSpecification[] comparisonArtifacts;
149
150
151
152
153
154
155
156 protected String minSeverity;
157
158
159
160
161
162
163 protected File textOutputFile;
164
165
166
167
168
169
170 protected File xmlOutputFile;
171
172
173
174
175
176
177
178 protected String[] includes;
179
180
181
182
183
184
185
186 protected String[] excludes;
187
188
189
190
191
192
193
194
195
196 protected Difference[] ignored;
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218 protected File ignoredDifferencesFile;
219
220
221
222
223
224
225 protected boolean logResults;
226
227
228
229
230 private boolean skipArtifactTypeTest;
231
232 private static final URL[] EMPTY_URL_ARRAY = new URL[0];
233
234 public void execute()
235 throws MojoExecutionException, MojoFailureException
236 {
237 if ( skip )
238 {
239 getLog().info( "Skipping execution" );
240 }
241 else
242 {
243 doExecute();
244 }
245 }
246
247 protected abstract void doExecute()
248 throws MojoExecutionException, MojoFailureException;
249
250 public ClirrDiffListener executeClirr()
251 throws MojoExecutionException, MojoFailureException
252 {
253 return executeClirr( null );
254 }
255
256 protected ClirrDiffListener executeClirr( Severity minSeverity )
257 throws MojoExecutionException, MojoFailureException
258 {
259 ClirrDiffListener listener = new ClirrDiffListener();
260
261 ClassFilter classFilter = new ClirrClassFilter( includes, excludes );
262
263 JavaType[] origClasses = resolvePreviousReleaseClasses( classFilter );
264
265 JavaType[] currentClasses = resolveCurrentClasses( classFilter );
266
267
268 Checker checker = new Checker();
269
270 List<DiffListener> listeners = new ArrayList<DiffListener>();
271
272 listeners.add( listener );
273
274 if ( xmlOutputFile != null )
275 {
276 try
277 {
278 listeners.add( new TypeRevealingXmlDiffListener( xmlOutputFile.getAbsolutePath() ) );
279 }
280 catch ( IOException e )
281 {
282 throw new MojoExecutionException( "Error adding '" + xmlOutputFile + "' for output: " + e.getMessage(),
283 e );
284 }
285 }
286
287 if ( textOutputFile != null )
288 {
289 try
290 {
291 listeners.add( new PlainDiffListener( textOutputFile.getAbsolutePath() ) );
292 }
293 catch ( IOException e )
294 {
295 throw new MojoExecutionException( "Error adding '" + textOutputFile + "' for output: " + e.getMessage(),
296 e );
297 }
298 }
299
300 if ( logResults )
301 {
302 listeners.add( new LogDiffListener( getLog() ) );
303 }
304
305 checker.addDiffListener( new DelegatingListener( listeners, minSeverity, getAllIgnored() ) );
306
307 reportDiffs( checker, origClasses, currentClasses );
308
309 return listener;
310 }
311
312 protected List<Difference> getAllIgnored()
313 {
314 Difference[] ret = ignored;
315
316 if ( ignoredDifferencesFile != null && ignoredDifferencesFile.exists() )
317 {
318 XmlStreamReader rdr = null;
319 try
320 {
321 rdr = ReaderFactory.newXmlReader( ignoredDifferencesFile );
322
323 Difference[] diffs = Difference.parseXml( rdr );
324
325 int ignoredLength = ignored == null ? 0 : ignored.length;
326
327 Difference[] tmp = new Difference[ignoredLength + diffs.length];
328
329 if ( ignored != null )
330 {
331 System.arraycopy( ignored, 0, tmp, 0, ignoredLength );
332 }
333
334 System.arraycopy( diffs, 0, tmp, ignoredLength, diffs.length );
335
336 ret = tmp;
337 }
338 catch ( IOException e )
339 {
340 getLog().error( "Could not read the ignored differences file.", e );
341 }
342 catch ( XmlPullParserException e )
343 {
344 getLog().error( "Could not read the ignored differences file.", e );
345 }
346 finally
347 {
348 IOUtil.close( rdr );
349 }
350 }
351
352 return ret == null ? Collections.<Difference>emptyList() : Arrays.asList( ret );
353 }
354
355 private JavaType[] resolveCurrentClasses( ClassFilter classFilter )
356 throws MojoExecutionException
357 {
358 try
359 {
360 ClassLoader currentDepCL = createClassLoader( project.getArtifacts(), null );
361 return createClassSet( classesDirectory, currentDepCL, classFilter );
362 }
363 catch ( MalformedURLException e )
364 {
365 throw new MojoExecutionException( "Error creating classloader for current classes", e );
366 }
367 }
368
369 private JavaType[] resolvePreviousReleaseClasses( ClassFilter classFilter )
370 throws MojoFailureException, MojoExecutionException
371 {
372 final Set previousArtifacts;
373 final Artifact firstPreviousArtifact;
374 if ( comparisonArtifacts == null )
375 {
376 firstPreviousArtifact = getComparisonArtifact();
377 comparisonVersion = firstPreviousArtifact.getVersion();
378 getLog().info( "Comparing to version: " + comparisonVersion );
379 previousArtifacts = Collections.singleton( firstPreviousArtifact );
380 }
381 else
382 {
383 previousArtifacts = resolveArtifacts( comparisonArtifacts );
384 Artifact a = null;
385 for ( Iterator iter = previousArtifacts.iterator(); iter.hasNext(); )
386 {
387 Artifact artifact = (Artifact) iter.next();
388 if ( a == null )
389 {
390 a = artifact;
391 }
392 getLog().debug( "Comparing to " + artifact.getGroupId() + ":" + artifact.getArtifactId() + ":"
393 + artifact.getVersion() + ":" + artifact.getClassifier() + ":"
394 + artifact.getType() );
395 }
396 firstPreviousArtifact = a;
397 }
398
399 try
400 {
401 for ( Iterator iter = previousArtifacts.iterator(); iter.hasNext(); )
402 {
403 Artifact artifact = (Artifact) iter.next();
404 resolver.resolve( artifact, project.getRemoteArtifactRepositories(), localRepository );
405 }
406
407 final List dependencies = getTransitiveDependencies( previousArtifacts );
408
409 ClassLoader origDepCL = createClassLoader( dependencies, previousArtifacts );
410 final Set files = new HashSet();
411 for ( Iterator iter = previousArtifacts.iterator(); iter.hasNext(); )
412 {
413 Artifact artifact = (Artifact) iter.next();
414
415
416
417 if ( "jar".equals( artifact.getType() ) || "maven-plugin".equals( artifact.getType() )
418 || "bundle".equals( artifact.getType() ) || artifact.getArtifactHandler().isAddedToClasspath()
419 || skipArtifactTypeTest )
420 {
421 files.add( new File( localRepository.getBasedir(), localRepository.pathOf( artifact ) ) );
422 }
423 }
424 return BcelTypeArrayBuilder.createClassSet( (File[]) files.toArray( new File[files.size()] ), origDepCL,
425 classFilter );
426 }
427 catch ( ProjectBuildingException e )
428 {
429 throw new MojoExecutionException( "Failed to build project for previous artifact: " + e.getMessage(), e );
430 }
431 catch ( InvalidDependencyVersionException e )
432 {
433 throw new MojoExecutionException( e.getMessage(), e );
434 }
435 catch ( ArtifactResolutionException e )
436 {
437 throw new MissingPreviousException( "Error resolving previous version: " + e.getMessage(), e );
438 }
439 catch ( ArtifactNotFoundException e )
440 {
441 getLog().warn( "Impossible to find previous version" );
442 return new JavaType[0];
443
444 }
445 catch ( MalformedURLException e )
446 {
447 throw new MojoExecutionException( "Error creating classloader for previous version's classes", e );
448 }
449 }
450
451 protected List getTransitiveDependencies( final Set previousArtifacts )
452 throws ProjectBuildingException, InvalidDependencyVersionException, ArtifactResolutionException,
453 ArtifactNotFoundException
454 {
455 final List dependencies = new ArrayList();
456 for ( Iterator iter = previousArtifacts.iterator(); iter.hasNext(); )
457 {
458 final Artifact a = (Artifact) iter.next();
459 final Artifact pomArtifact =
460 factory.createArtifact( a.getGroupId(), a.getArtifactId(), a.getVersion(), a.getScope(), "pom" );
461 final MavenProject pomProject =
462 mavenProjectBuilder.buildFromRepository( pomArtifact, project.getRemoteArtifactRepositories(),
463 localRepository );
464 final Set pomProjectArtifacts = pomProject.createArtifacts( factory, null, null );
465 final ArtifactResolutionResult result =
466 resolver.resolveTransitively( pomProjectArtifacts, pomArtifact, localRepository,
467 project.getRemoteArtifactRepositories(), metadataSource, null );
468 dependencies.addAll( result.getArtifacts() );
469 }
470 return dependencies;
471 }
472
473 private Artifact resolveArtifact( ArtifactSpecification artifactSpec )
474 throws MojoFailureException, MojoExecutionException
475 {
476 final String groupId = artifactSpec.getGroupId();
477 if ( groupId == null )
478 {
479 throw new MojoFailureException( "An artifacts groupId is required." );
480 }
481 final String artifactId = artifactSpec.getArtifactId();
482 if ( artifactId == null )
483 {
484 throw new MojoFailureException( "An artifacts artifactId is required." );
485 }
486 final String version = artifactSpec.getVersion();
487 if ( version == null )
488 {
489 throw new MojoFailureException( "An artifacts version number is required." );
490 }
491 final VersionRange versionRange = VersionRange.createFromVersion( version );
492 String type = artifactSpec.getType();
493 if ( type == null )
494 {
495 type = "jar";
496 }
497
498 Artifact artifact =
499 factory.createDependencyArtifact( groupId, artifactId, versionRange, type, artifactSpec.getClassifier(),
500 Artifact.SCOPE_COMPILE );
501 return artifact;
502 }
503
504 protected Set resolveArtifacts( ArtifactSpecification[] artifacts )
505 throws MojoFailureException, MojoExecutionException
506 {
507 Set artifactSet = new HashSet();
508 Artifact[] result = new Artifact[artifacts.length];
509 for ( int i = 0; i < result.length; i++ )
510 {
511 artifactSet.add( resolveArtifact( artifacts[i] ) );
512 }
513 return artifactSet;
514 }
515
516 private Artifact getComparisonArtifact()
517 throws MojoFailureException, MojoExecutionException
518 {
519
520 VersionRange range;
521 try
522 {
523 range = VersionRange.createFromVersionSpec( comparisonVersion );
524 }
525 catch ( InvalidVersionSpecificationException e )
526 {
527 throw new MojoFailureException( "Invalid comparison version: " + e.getMessage() );
528 }
529
530 Artifact previousArtifact;
531 try
532 {
533 previousArtifact = factory.createDependencyArtifact( project.getGroupId(), project.getArtifactId(), range,
534 project.getPackaging(), null, Artifact.SCOPE_COMPILE );
535
536 if ( !previousArtifact.getVersionRange().isSelectedVersionKnown( previousArtifact ) )
537 {
538 getLog().debug( "Searching for versions in range: " + previousArtifact.getVersionRange() );
539 List availableVersions = metadataSource.retrieveAvailableVersions( previousArtifact, localRepository,
540 project.getRemoteArtifactRepositories() );
541 filterSnapshots( availableVersions );
542 ArtifactVersion version = range.matchVersion( availableVersions );
543 if ( version != null )
544 {
545 previousArtifact.selectVersion( version.toString() );
546 }
547 }
548 }
549 catch ( OverConstrainedVersionException e1 )
550 {
551 throw new MojoFailureException( "Invalid comparison version: " + e1.getMessage() );
552 }
553 catch ( ArtifactMetadataRetrievalException e11 )
554 {
555 throw new MojoExecutionException( "Error determining previous version: " + e11.getMessage(), e11 );
556 }
557
558 if ( previousArtifact.getVersion() == null )
559 {
560 getLog().info( "Unable to find a previous version of the project in the repository" );
561 }
562
563 return previousArtifact;
564 }
565
566 private void filterSnapshots( List versions )
567 {
568 for ( Iterator versionIterator = versions.iterator(); versionIterator.hasNext(); )
569 {
570 ArtifactVersion version = (ArtifactVersion) versionIterator.next();
571 if ( "SNAPSHOT".equals( version.getQualifier() ) )
572 {
573 versionIterator.remove();
574 }
575 }
576 }
577
578 public static JavaType[] createClassSet( File classes, ClassLoader thirdPartyClasses, ClassFilter classFilter )
579 throws MalformedURLException
580 {
581 ClassLoader classLoader = new URLClassLoader( new URL[]{ classes.toURI().toURL() }, thirdPartyClasses );
582
583 Repository repository = new ClassLoaderRepository( classLoader );
584
585 List selected = new ArrayList();
586
587 DirectoryScanner scanner = new DirectoryScanner();
588 scanner.setBasedir( classes );
589 scanner.setIncludes( new String[]{ "**/*.class" } );
590 scanner.scan();
591
592 String[] files = scanner.getIncludedFiles();
593
594 for ( int i = 0; i < files.length; i++ )
595 {
596 File f = new File( classes, files[i] );
597 JavaClass clazz = extractClass( f, repository );
598 if ( classFilter.isSelected( clazz ) )
599 {
600 selected.add( new BcelJavaType( clazz ) );
601 repository.storeClass( clazz );
602 }
603 }
604
605 JavaType[] ret = new JavaType[selected.size()];
606 selected.toArray( ret );
607 return ret;
608 }
609
610 private static JavaClass extractClass( File f, Repository repository )
611 throws CheckerException
612 {
613 InputStream is = null;
614 try
615 {
616 is = new FileInputStream( f );
617
618 ClassParser parser = new ClassParser( is, f.getName() );
619 JavaClass clazz = parser.parse();
620 clazz.setRepository( repository );
621 return clazz;
622 }
623 catch ( IOException ex )
624 {
625 throw new CheckerException( "Cannot read " + f, ex );
626 }
627 finally
628 {
629 IOUtil.close( is );
630 }
631 }
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648 protected static ClassLoader createClassLoader( Collection artifacts, Set previousArtifacts )
649 throws MalformedURLException
650 {
651 URLClassLoader cl = null;
652 if ( !artifacts.isEmpty() )
653 {
654 List urls = new ArrayList( artifacts.size() );
655 for ( Iterator i = artifacts.iterator(); i.hasNext(); )
656 {
657 Artifact artifact = (Artifact) i.next();
658 if ( previousArtifacts == null || !previousArtifacts.contains( artifact ) )
659 {
660 urls.add( artifact.getFile().toURI().toURL() );
661 }
662 }
663 if ( !urls.isEmpty() )
664 {
665 cl = new URLClassLoader( (URL[]) urls.toArray( EMPTY_URL_ARRAY ) );
666 }
667 }
668 return cl;
669 }
670
671 protected static Severity convertSeverity( String minSeverity )
672 {
673 Severity s;
674 if ( "info".equals( minSeverity ) )
675 {
676 s = Severity.INFO;
677 }
678 else if ( "warning".equals( minSeverity ) )
679 {
680 s = Severity.WARNING;
681 }
682 else if ( "error".equals( minSeverity ) )
683 {
684 s = Severity.ERROR;
685 }
686 else
687 {
688 s = null;
689 }
690 return s;
691 }
692
693 protected boolean canGenerate()
694 throws MojoFailureException, MojoExecutionException
695 {
696 boolean classes = false;
697
698 if ( classesDirectory.exists() )
699 {
700 classes = true;
701 }
702 else
703 {
704 getLog().debug( "Classes directory not found: " + classesDirectory );
705 }
706
707 if ( !classes )
708 {
709 getLog().info( "Not generating Clirr report as there are no classes generated by the project" );
710 return false;
711 }
712
713 if ( comparisonArtifacts == null || comparisonArtifacts.length == 0 )
714 {
715 Artifact previousArtifact = getComparisonArtifact();
716 if ( previousArtifact.getVersion() == null )
717 {
718 getLog().info(
719 "Not generating Clirr report as there is no previous version of the library to compare against" );
720 return false;
721 }
722 }
723
724 return true;
725 }
726
727
728
729
730
731
732
733
734
735 private void reportDiffs( Checker checker, JavaType[] origClasses, JavaType[] currentClasses )
736 {
737 try
738 {
739 checker.reportDiffs( origClasses, currentClasses );
740 }
741 catch ( CheckerException e )
742 {
743 getLog().error( e.getMessage() );
744
745
746 int matchingClasses = 0;
747 int j = 0;
748 for ( int i = 0; i < origClasses.length; i++ )
749 {
750 if ( !e.getMessage().endsWith( origClasses[i].getName() ) )
751 {
752 matchingClasses++;
753 }
754 }
755 JavaType[] origClasses2 = new JavaType[matchingClasses];
756 for ( int i = 0; i < origClasses.length; i++ )
757 {
758 if ( !e.getMessage().endsWith( origClasses[i].getName() ) )
759 {
760 origClasses2[j++] = origClasses[i];
761 }
762 }
763
764 matchingClasses = 0;
765 j = 0;
766 for ( int i = 0; i < currentClasses.length; i++ )
767 {
768 if ( !e.getMessage().endsWith( currentClasses[i].getName() ) )
769 {
770 matchingClasses++;
771 }
772 }
773 JavaType[] currentClasses2 = new JavaType[matchingClasses];
774 for ( int i = 0; i < currentClasses.length; i++ )
775 {
776 if ( !e.getMessage().endsWith( currentClasses[i].getName() ) )
777 {
778 currentClasses2[j++] = currentClasses[i];
779 }
780 }
781
782 reportDiffs( checker, origClasses2, currentClasses2 );
783 }
784 }
785 }