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 javax.inject.Inject;
22  
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.LinkedHashSet;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.stream.Collectors;
30  
31  import org.apache.maven.artifact.Artifact;
32  import org.apache.maven.model.Dependency;
33  import org.apache.maven.model.DependencyManagement;
34  import org.apache.maven.model.Exclusion;
35  import org.apache.maven.plugin.AbstractMojo;
36  import org.apache.maven.plugin.MojoExecutionException;
37  import org.apache.maven.plugin.MojoFailureException;
38  import org.apache.maven.plugins.annotations.Mojo;
39  import org.apache.maven.plugins.annotations.Parameter;
40  import org.apache.maven.plugins.annotations.ResolutionScope;
41  import org.apache.maven.project.MavenProject;
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 extends AbstractMojo {
53      // fields -----------------------------------------------------------------
54  
55      private final MavenProject project;
56  
57      /**
58       * Fail the build if a problem is detected.
59       */
60      @Parameter(property = "mdep.analyze.failBuild", defaultValue = "false")
61      private boolean failBuild = false;
62  
63      /**
64       * Ignore Direct Dependency Overrides of dependencyManagement section.
65       */
66      @Parameter(property = "mdep.analyze.ignore.direct", defaultValue = "true")
67      private boolean ignoreDirect = true;
68  
69      /**
70       * Skip plugin execution completely.
71       *
72       * @since 2.7
73       */
74      @Parameter(property = "mdep.analyze.skip", defaultValue = "false")
75      private boolean skip;
76  
77      @Inject
78      public AnalyzeDepMgt(MavenProject project) {
79          this.project = project;
80      }
81  
82      // Mojo methods -----------------------------------------------------------
83  
84      /*
85       * @see org.apache.maven.plugin.Mojo#execute()
86       */
87      @Override
88      public void execute() throws MojoExecutionException, MojoFailureException {
89          if (skip) {
90              getLog().info("Skipping plugin execution");
91              return;
92          }
93  
94          boolean result = checkDependencyManagement();
95          if (result) {
96              if (this.failBuild) {
97  
98                  throw new MojoExecutionException("Found Dependency errors.");
99              } else {
100                 getLog().warn("Potential problems found in Dependency Management ");
101             }
102         }
103     }
104 
105     /**
106      * Does the work of checking the DependencyManagement Section.
107      *
108      * @return true if errors are found
109      * @throws MojoExecutionException
110      */
111     private boolean checkDependencyManagement() throws MojoExecutionException {
112         boolean foundError = false;
113 
114         getLog().info("Found Resolved Dependency/DependencyManagement mismatches:");
115 
116         List<Dependency> depMgtDependencies = null;
117 
118         DependencyManagement depMgt = project.getDependencyManagement();
119         if (depMgt != null) {
120             depMgtDependencies = depMgt.getDependencies();
121         }
122 
123         if (depMgtDependencies != null && !depMgtDependencies.isEmpty()) {
124             // put all the dependencies from depMgt into a map for quick lookup
125             Map<String, Dependency> depMgtMap = new HashMap<>();
126             Map<String, Exclusion> exclusions = new HashMap<>();
127             for (Dependency depMgtDependency : depMgtDependencies) {
128                 depMgtMap.put(depMgtDependency.getManagementKey(), depMgtDependency);
129 
130                 // now put all the exclusions into a map for quick lookup
131                 exclusions.putAll(addExclusions(depMgtDependency.getExclusions()));
132             }
133 
134             // get dependencies for the project (including transitive)
135             Set<Artifact> allDependencyArtifacts = new LinkedHashSet<>(project.getArtifacts());
136 
137             // don't warn if a dependency that is directly listed overrides
138             // depMgt. That's ok.
139             if (this.ignoreDirect) {
140                 getLog().info("\tIgnoring Direct Dependencies.");
141                 Set<Artifact> directDependencies = project.getDependencyArtifacts();
142                 allDependencyArtifacts.removeAll(directDependencies);
143             }
144 
145             // log exclusion errors
146             List<Artifact> exclusionErrors = getExclusionErrors(exclusions, allDependencyArtifacts);
147             for (Artifact exclusion : exclusionErrors) {
148                 getLog().info(getArtifactManagementKey(exclusion)
149                         + " was excluded in DepMgt, but version " + exclusion.getVersion()
150                         + " has been found in the dependency tree.");
151                 foundError = true;
152             }
153 
154             // find and log version mismatches
155             Map<Artifact, Dependency> mismatch = getMismatch(depMgtMap, allDependencyArtifacts);
156             for (Map.Entry<Artifact, Dependency> entry : mismatch.entrySet()) {
157                 logMismatch(entry.getKey(), entry.getValue());
158                 foundError = true;
159             }
160             if (!foundError) {
161                 getLog().info("\tNone");
162             }
163         } else {
164             getLog().info("\tNothing in DepMgt.");
165         }
166 
167         return foundError;
168     }
169 
170     /**
171      * Returns a map of the exclusions using the Dependency ManagementKey as the keyset.
172      *
173      * @param exclusionList to be added to the map
174      * @return a map of the exclusions using the Dependency ManagementKey as the keyset
175      */
176     public Map<String, Exclusion> addExclusions(List<Exclusion> exclusionList) {
177         if (exclusionList != null) {
178             return exclusionList.stream().collect(Collectors.toMap(this::getExclusionKey, exclusion -> exclusion));
179         }
180         return Collections.emptyMap();
181     }
182 
183     /**
184      * Returns a List of the artifacts that should have been excluded, but were found in the dependency tree.
185      *
186      * @param exclusions a map of the DependencyManagement exclusions, with the ManagementKey as the key and Dependency
187      *            as the value
188      * @param allDependencyArtifacts resolved artifacts to be compared
189      * @return list of artifacts that should have been excluded
190      */
191     public List<Artifact> getExclusionErrors(Map<String, Exclusion> exclusions, Set<Artifact> allDependencyArtifacts) {
192         return allDependencyArtifacts.stream()
193                 .filter(artifact -> exclusions.containsKey(getExclusionKey(artifact)))
194                 .collect(Collectors.toList());
195     }
196 
197     /**
198      * @param artifact {@link Artifact}
199      * @return the resulting GA
200      */
201     public String getExclusionKey(Artifact artifact) {
202         return artifact.getGroupId() + ":" + artifact.getArtifactId();
203     }
204 
205     /**
206      * @param ex the exclusion key
207      * @return the resulting combination of groupId+artifactId
208      */
209     public String getExclusionKey(Exclusion ex) {
210         return ex.getGroupId() + ":" + ex.getArtifactId();
211     }
212 
213     /**
214      * Calculate the mismatches between the DependencyManagement and resolved artifacts.
215      *
216      * @param depMgtMap a keyset of the Dependency.GetManagementKey for quick lookup
217      * @param allDependencyArtifacts the set of all artifacts to compare
218      * @return a map containing the resolved artifact as the key and the listed dependency as the value
219      */
220     public Map<Artifact, Dependency> getMismatch(
221             Map<String, Dependency> depMgtMap, Set<Artifact> allDependencyArtifacts) {
222         Map<Artifact, Dependency> mismatchMap = new HashMap<>();
223 
224         for (Artifact dependencyArtifact : allDependencyArtifacts) {
225             Dependency depFromDepMgt = depMgtMap.get(getArtifactManagementKey(dependencyArtifact));
226             if (depFromDepMgt != null) {
227                 if (depFromDepMgt.getVersion() != null
228                         && !depFromDepMgt.getVersion().equals(dependencyArtifact.getBaseVersion())) {
229                     mismatchMap.put(dependencyArtifact, depFromDepMgt);
230                 }
231             }
232         }
233         return mismatchMap;
234     }
235 
236     /**
237      * This function displays the log to the screen showing the versions and information about the artifacts that don't
238      * match.
239      *
240      * @param dependencyArtifact the artifact that was resolved
241      * @param dependencyFromDepMgt the dependency listed in the DependencyManagement section
242      * @throws MojoExecutionException in case of errors
243      */
244     public void logMismatch(Artifact dependencyArtifact, Dependency dependencyFromDepMgt)
245             throws MojoExecutionException {
246         if (dependencyArtifact == null || dependencyFromDepMgt == null) {
247             throw new MojoExecutionException(
248                     "Invalid params: Artifact: " + dependencyArtifact + " Dependency: " + dependencyFromDepMgt);
249         }
250 
251         getLog().info("\tDependency: " + dependencyFromDepMgt.getManagementKey());
252         getLog().info("\t\tDepMgt  : " + dependencyFromDepMgt.getVersion());
253         getLog().info("\t\tResolved: " + dependencyArtifact.getBaseVersion());
254     }
255 
256     /**
257      * This function returns a string comparable with Dependency.GetManagementKey.
258      *
259      * @param artifact to gen the key for
260      * @return a string in the form: groupId:ArtifactId:Type[:Classifier]
261      */
262     public String getArtifactManagementKey(Artifact artifact) {
263         return artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getType()
264                 + ((artifact.getClassifier() != null) ? ":" + artifact.getClassifier() : "");
265     }
266 
267     /**
268      * @return the failBuild
269      */
270     protected final boolean isFailBuild() {
271         return this.failBuild;
272     }
273 
274     /**
275      * @param theFailBuild the failBuild to set
276      */
277     public void setFailBuild(boolean theFailBuild) {
278         this.failBuild = theFailBuild;
279     }
280 
281     /**
282      * @return the project
283      */
284     protected final MavenProject getProject() {
285         return this.project;
286     }
287 
288     /**
289      * @return the ignoreDirect
290      */
291     protected final boolean isIgnoreDirect() {
292         return this.ignoreDirect;
293     }
294 
295     /**
296      * @param theIgnoreDirect the ignoreDirect to set
297      */
298     public void setIgnoreDirect(boolean theIgnoreDirect) {
299         this.ignoreDirect = theIgnoreDirect;
300     }
301 }