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