View Javadoc
1   package org.apache.maven.plugins.dependency.analyze;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.LinkedHashSet;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  import java.util.stream.Collectors;
29  
30  import org.apache.maven.artifact.Artifact;
31  import org.apache.maven.model.Dependency;
32  import org.apache.maven.model.DependencyManagement;
33  import org.apache.maven.model.Exclusion;
34  import org.apache.maven.plugin.AbstractMojo;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugin.MojoFailureException;
37  import org.apache.maven.plugins.annotations.Mojo;
38  import org.apache.maven.plugins.annotations.Parameter;
39  import org.apache.maven.plugins.annotations.ResolutionScope;
40  import org.apache.maven.project.MavenProject;
41  import org.codehaus.plexus.util.StringUtils;
42  
43  /**
44   * This mojo looks at the dependencies after final resolution and looks for mismatches in your dependencyManagement
45   * section. This mojo is also useful for detecting projects that override the dependencyManagement directly.
46   * Set ignoreDirect to false to detect these otherwise normal conditions.
47   *
48   * @author <a href="mailto:brianefox@gmail.com">Brian Fox</a>
49   * @since 2.0-alpha-3
50   */
51  @Mojo( name = "analyze-dep-mgt", requiresDependencyResolution = ResolutionScope.TEST, threadSafe = true )
52  public class AnalyzeDepMgt
53      extends AbstractMojo
54  {
55      // fields -----------------------------------------------------------------
56  
57      /**
58       *
59       */
60      @Parameter( defaultValue = "${project}", readonly = true, required = true )
61      private MavenProject project;
62  
63      /**
64       * Fail the build if a problem is detected.
65       */
66      @Parameter( property = "mdep.analyze.failBuild", defaultValue = "false" )
67      private boolean failBuild = false;
68  
69      /**
70       * Ignore Direct Dependency Overrides of dependencyManagement section.
71       */
72      @Parameter( property = "mdep.analyze.ignore.direct", defaultValue = "true" )
73      private boolean ignoreDirect = true;
74  
75      /**
76       * Skip plugin execution completely.
77       *
78       * @since 2.7
79       */
80      @Parameter( property = "mdep.analyze.skip", defaultValue = "false" )
81      private boolean skip;
82  
83      // Mojo methods -----------------------------------------------------------
84  
85      /*
86       * @see org.apache.maven.plugin.Mojo#execute()
87       */
88      @Override
89      public void execute()
90          throws MojoExecutionException, MojoFailureException
91      {
92          if ( skip )
93          {
94              getLog().info( "Skipping plugin execution" );
95              return;
96          }
97  
98          boolean result = checkDependencyManagement();
99          if ( result )
100         {
101             if ( this.failBuild )
102 
103             {
104                 throw new MojoExecutionException( "Found Dependency errors." );
105             }
106             else
107             {
108                 getLog().warn( "Potential problems found in Dependency Management " );
109             }
110         }
111     }
112 
113     /**
114      * Does the work of checking the DependencyManagement Section.
115      *
116      * @return true if errors are found.
117      * @throws MojoExecutionException
118      */
119     private boolean checkDependencyManagement()
120         throws MojoExecutionException
121     {
122         boolean foundError = false;
123 
124         getLog().info( "Found Resolved Dependency/DependencyManagement mismatches:" );
125 
126         List<Dependency> depMgtDependencies = null;
127 
128         DependencyManagement depMgt = project.getDependencyManagement();
129         if ( depMgt != null )
130         {
131             depMgtDependencies = depMgt.getDependencies();
132         }
133 
134         if ( depMgtDependencies != null && !depMgtDependencies.isEmpty() )
135         {
136             // put all the dependencies from depMgt into a map for quick lookup
137             Map<String, Dependency> depMgtMap = new HashMap<>();
138             Map<String, Exclusion> exclusions = new HashMap<>();
139             for ( Dependency depMgtDependency : depMgtDependencies )
140             {
141                 depMgtMap.put( depMgtDependency.getManagementKey(), depMgtDependency );
142 
143                 // now put all the exclusions into a map for quick lookup
144                 exclusions.putAll( addExclusions( depMgtDependency.getExclusions() ) );
145             }
146 
147             // get dependencies for the project (including transitive)
148             Set<Artifact> allDependencyArtifacts = new LinkedHashSet<>( project.getArtifacts() );
149 
150             // don't warn if a dependency that is directly listed overrides
151             // depMgt. That's ok.
152             if ( this.ignoreDirect )
153             {
154                 getLog().info( "\tIgnoring Direct Dependencies." );
155                 Set<Artifact> directDependencies = project.getDependencyArtifacts();
156                 allDependencyArtifacts.removeAll( directDependencies );
157             }
158 
159             // log exclusion errors
160             List<Artifact> exclusionErrors = getExclusionErrors( exclusions, allDependencyArtifacts );
161             for ( Artifact exclusion : exclusionErrors )
162             {
163                 getLog().info( StringUtils.stripEnd( getArtifactManagementKey( exclusion ), ":" )
164                     + " was excluded in DepMgt, but version " + exclusion.getVersion()
165                     + " has been found in the dependency tree." );
166                 foundError = true;
167             }
168 
169             // find and log version mismatches
170             Map<Artifact, Dependency> mismatch = getMismatch( depMgtMap, allDependencyArtifacts );
171             for ( Map.Entry<Artifact, Dependency> entry : mismatch.entrySet() )
172             {
173                 logMismatch( entry.getKey(), entry.getValue() );
174                 foundError = true;
175             }
176             if ( !foundError )
177             {
178                 getLog().info( "\tNone" );
179             }
180         }
181         else
182         {
183             getLog().info( "\tNothing in DepMgt." );
184         }
185 
186         return foundError;
187     }
188 
189     /**
190      * Returns a map of the exclusions using the Dependency ManagementKey as the keyset.
191      *
192      * @param exclusionList to be added to the map.
193      * @return a map of the exclusions using the Dependency ManagementKey as the keyset.
194      */
195     public Map<String, Exclusion> addExclusions( List<Exclusion> exclusionList )
196     {
197         if ( exclusionList != null )
198         {
199             return exclusionList.stream()
200                     .collect( Collectors.toMap( this::getExclusionKey, exclusion -> exclusion ) );
201         }
202         return Collections.emptyMap();
203     }
204 
205     /**
206      * Returns a List of the artifacts that should have been excluded, but were found in the dependency tree.
207      *
208      * @param exclusions a map of the DependencyManagement exclusions, with the ManagementKey as the key and Dependency
209      *            as the value.
210      * @param allDependencyArtifacts resolved artifacts to be compared.
211      * @return list of artifacts that should have been excluded.
212      */
213     public List<Artifact> getExclusionErrors( Map<String, Exclusion> exclusions, Set<Artifact> allDependencyArtifacts )
214     {
215         return allDependencyArtifacts.stream()
216                 .filter( artifact -> exclusions.containsKey( getExclusionKey( artifact ) ) )
217                 .collect( Collectors.toList( ) );
218     }
219 
220     /**
221      * @param artifact {@link Artifact}
222      * @return The resulting GA.
223      */
224     public String getExclusionKey( Artifact artifact )
225     {
226         return artifact.getGroupId() + ":" + artifact.getArtifactId();
227     }
228 
229     /**
230      * @param ex The exclusion key.
231      * @return The resulting combination of groupId+artifactId.
232      */
233     public String getExclusionKey( Exclusion ex )
234     {
235         return ex.getGroupId() + ":" + ex.getArtifactId();
236     }
237 
238     /**
239      * Calculate the mismatches between the DependencyManagement and resolved artifacts
240      *
241      * @param depMgtMap contains the Dependency.GetManagementKey as the keyset for quick lookup.
242      * @param allDependencyArtifacts contains the set of all artifacts to compare.
243      * @return a map containing the resolved artifact as the key and the listed dependency as the value.
244      */
245     public Map<Artifact, Dependency> getMismatch( Map<String, Dependency> depMgtMap,
246                                                   Set<Artifact> allDependencyArtifacts )
247     {
248         Map<Artifact, Dependency> mismatchMap = new HashMap<>();
249 
250         for ( Artifact dependencyArtifact : allDependencyArtifacts )
251         {
252             Dependency depFromDepMgt = depMgtMap.get( getArtifactManagementKey( dependencyArtifact ) );
253             if ( depFromDepMgt != null )
254             {
255                 // workaround for MNG-2961
256                 dependencyArtifact.isSnapshot();
257 
258                 if ( depFromDepMgt.getVersion() != null
259                     && !depFromDepMgt.getVersion().equals( dependencyArtifact.getBaseVersion() ) )
260                 {
261                     mismatchMap.put( dependencyArtifact, depFromDepMgt );
262                 }
263             }
264         }
265         return mismatchMap;
266     }
267 
268     /**
269      * This function displays the log to the screen showing the versions and information about the artifacts that don't
270      * match.
271      *
272      * @param dependencyArtifact the artifact that was resolved.
273      * @param dependencyFromDepMgt the dependency listed in the DependencyManagement section.
274      * @throws MojoExecutionException in case of errors.
275      */
276     public void logMismatch( Artifact dependencyArtifact, Dependency dependencyFromDepMgt )
277         throws MojoExecutionException
278     {
279         if ( dependencyArtifact == null || dependencyFromDepMgt == null )
280         {
281             throw new MojoExecutionException( "Invalid params: Artifact: " + dependencyArtifact + " Dependency: "
282                 + dependencyFromDepMgt );
283         }
284 
285         getLog().info( "\tDependency: " + StringUtils.stripEnd( dependencyFromDepMgt.getManagementKey(), ":" ) );
286         getLog().info( "\t\tDepMgt  : " + dependencyFromDepMgt.getVersion() );
287         getLog().info( "\t\tResolved: " + dependencyArtifact.getBaseVersion() );
288     }
289 
290     /**
291      * This function returns a string comparable with Dependency.GetManagementKey.
292      *
293      * @param artifact to gen the key for
294      * @return a string in the form: groupId:ArtifactId:Type[:Classifier]
295      */
296     public String getArtifactManagementKey( Artifact artifact )
297     {
298         return artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getType()
299             + ( ( artifact.getClassifier() != null ) ? ":" + artifact.getClassifier() : "" );
300     }
301 
302     /**
303      * @return the failBuild
304      */
305     protected final boolean isFailBuild()
306     {
307         return this.failBuild;
308     }
309 
310     /**
311      * @param theFailBuild the failBuild to set
312      */
313     public void setFailBuild( boolean theFailBuild )
314     {
315         this.failBuild = theFailBuild;
316     }
317 
318     /**
319      * @return the project
320      */
321     protected final MavenProject getProject()
322     {
323         return this.project;
324     }
325 
326     /**
327      * @param theProject the project to set
328      */
329     public void setProject( MavenProject theProject )
330     {
331         this.project = theProject;
332     }
333 
334     /**
335      * @return the ignoreDirect
336      */
337     protected final boolean isIgnoreDirect()
338     {
339         return this.ignoreDirect;
340     }
341 
342     /**
343      * @param theIgnoreDirect the ignoreDirect to set
344      */
345     public void setIgnoreDirect( boolean theIgnoreDirect )
346     {
347         this.ignoreDirect = theIgnoreDirect;
348     }
349 }