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.enforcer.rules.dependency;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Objects;
31  
32  import org.apache.maven.artifact.Artifact;
33  import org.apache.maven.artifact.versioning.ArtifactVersion;
34  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
35  import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
36  import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
37  import org.apache.maven.enforcer.rules.AbstractStandardEnforcerRule;
38  import org.apache.maven.enforcer.rules.utils.ArtifactUtils;
39  import org.apache.maven.enforcer.rules.utils.ParentNodeProvider;
40  import org.apache.maven.enforcer.rules.utils.ParentsVisitor;
41  import org.eclipse.aether.graph.DependencyNode;
42  import org.eclipse.aether.graph.DependencyVisitor;
43  import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
44  
45  import static org.apache.maven.artifact.Artifact.SCOPE_PROVIDED;
46  import static org.apache.maven.artifact.Artifact.SCOPE_TEST;
47  
48  /**
49   * Rule to enforce that the resolved dependency is also the most recent one of all transitive dependencies.
50   *
51   * @author Geoffrey De Smet
52   * @since 1.1
53   */
54  @Named("requireUpperBoundDeps")
55  public final class RequireUpperBoundDeps extends AbstractStandardEnforcerRule {
56  
57      /**
58       * @since 1.3
59       */
60      private boolean uniqueVersions;
61  
62      /**
63       * Dependencies to ignore.
64       *
65       * @since TBD
66       */
67      private List<String> excludes = null;
68  
69      /**
70       * Dependencies to include.
71       *
72       * @since 3.0.0
73       */
74      private List<String> includes = null;
75  
76      /**
77       * Scope to exclude.
78       */
79      private List<String> excludedScopes = Arrays.asList(SCOPE_TEST, SCOPE_PROVIDED);
80  
81      private RequireUpperBoundDepsVisitor upperBoundDepsVisitor;
82  
83      private final ResolverUtil resolverUtil;
84  
85      @Inject
86      public RequireUpperBoundDeps(ResolverUtil resolverUtil) {
87          this.resolverUtil = Objects.requireNonNull(resolverUtil);
88      }
89  
90      /**
91       * Sets dependencies to exclude.
92       * @param excludes a list of {@code groupId:artifactId} names
93       */
94      public void setExcludes(List<String> excludes) {
95          this.excludes = excludes;
96      }
97  
98      /**
99       * Sets dependencies to include.
100      *
101      * @param includes a list of {@code groupId:artifactId} names
102      */
103     public void setIncludes(List<String> includes) {
104         this.includes = includes;
105     }
106 
107     @Override
108     public void execute() throws EnforcerRuleException {
109         DependencyNode node = resolverUtil.resolveTransitiveDependenciesVerbose(excludedScopes);
110         upperBoundDepsVisitor = new RequireUpperBoundDepsVisitor()
111                 .setUniqueVersions(uniqueVersions)
112                 .setIncludes(includes);
113         getLog().debug(() -> resolverUtil.dumpTree(node));
114         node.accept(upperBoundDepsVisitor);
115         List<String> errorMessages = buildErrorMessages(upperBoundDepsVisitor.getConflicts());
116         if (!errorMessages.isEmpty()) {
117             throw new EnforcerRuleException(
118                     "Failed while enforcing RequireUpperBoundDeps. The error(s) are " + errorMessages);
119         }
120     }
121 
122     private List<String> buildErrorMessages(List<List<DependencyNode>> conflicts) {
123         List<String> errorMessages = new ArrayList<>(conflicts.size());
124         for (List<DependencyNode> conflict : conflicts) {
125             org.eclipse.aether.artifact.Artifact artifact = conflict.get(0).getArtifact();
126             String groupArt = artifact.getGroupId() + ":" + artifact.getArtifactId();
127             if (excludes != null && excludes.contains(groupArt)) {
128                 getLog().info("Ignoring requireUpperBoundDeps in " + groupArt);
129             } else {
130                 errorMessages.add(buildErrorMessage(conflict));
131             }
132         }
133         return errorMessages;
134     }
135 
136     private String buildErrorMessage(List<DependencyNode> conflict) {
137         StringBuilder errorMessage = new StringBuilder();
138         errorMessage
139                 .append(System.lineSeparator())
140                 .append("Require upper bound dependencies error for ")
141                 .append(getFullArtifactName(conflict.get(0), false))
142                 .append(" paths to dependency are:")
143                 .append(System.lineSeparator());
144         if (conflict.size() > 0) {
145             errorMessage.append(buildTreeString(conflict.get(0)));
146         }
147         for (DependencyNode node : conflict.subList(1, conflict.size())) {
148             errorMessage.append("and").append(System.lineSeparator());
149             errorMessage.append(buildTreeString(node));
150         }
151         return errorMessage.toString();
152     }
153 
154     private StringBuilder buildTreeString(DependencyNode node) {
155         List<String> loc = new ArrayList<>();
156         DependencyNode currentNode = node;
157         while (currentNode != null) {
158             StringBuilder line = new StringBuilder(getFullArtifactName(currentNode, false));
159 
160             if (DependencyManagerUtils.getPremanagedVersion(currentNode) != null) {
161                 line.append(" (managed) <-- ");
162                 line.append(getFullArtifactName(currentNode, true));
163             }
164 
165             loc.add(line.toString());
166             currentNode = upperBoundDepsVisitor.getParent(currentNode);
167         }
168         Collections.reverse(loc);
169         StringBuilder builder = new StringBuilder();
170         for (int i = 0; i < loc.size(); i++) {
171             for (int j = 0; j < i; j++) {
172                 builder.append("  ");
173             }
174             builder.append("+-").append(loc.get(i));
175             builder.append(System.lineSeparator());
176         }
177         return builder;
178     }
179 
180     private String getFullArtifactName(DependencyNode node, boolean usePremanaged) {
181         Artifact artifact = ArtifactUtils.toArtifact(node);
182 
183         String version = DependencyManagerUtils.getPremanagedVersion(node);
184         if (!usePremanaged || version == null) {
185             version = uniqueVersions ? artifact.getVersion() : artifact.getBaseVersion();
186         }
187         String result = artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + version;
188 
189         String classifier = artifact.getClassifier();
190         if (classifier != null && !classifier.isEmpty()) {
191             result += ":" + classifier;
192         }
193 
194         String scope = artifact.getScope();
195         if (scope != null && !"compile".equals(scope)) {
196             result += " [" + scope + ']';
197         }
198 
199         return result;
200     }
201 
202     private static class RequireUpperBoundDepsVisitor implements DependencyVisitor, ParentNodeProvider {
203 
204         private final ParentsVisitor parentsVisitor = new ParentsVisitor();
205         private boolean uniqueVersions;
206         private List<String> includes = null;
207 
208         public RequireUpperBoundDepsVisitor setUniqueVersions(boolean uniqueVersions) {
209             this.uniqueVersions = uniqueVersions;
210             return this;
211         }
212 
213         public RequireUpperBoundDepsVisitor setIncludes(List<String> includes) {
214             this.includes = includes;
215             return this;
216         }
217 
218         private final Map<String, List<DependencyNodeHopCountPair>> keyToPairsMap = new HashMap<>();
219 
220         @Override
221         public boolean visitEnter(DependencyNode node) {
222             parentsVisitor.visitEnter(node);
223             DependencyNodeHopCountPair pair = new DependencyNodeHopCountPair(node, this);
224             String key = pair.constructKey();
225 
226             if (includes != null && !includes.isEmpty() && !includes.contains(key)) {
227                 return true;
228             }
229 
230             keyToPairsMap.computeIfAbsent(key, k1 -> new ArrayList<>()).add(pair);
231             keyToPairsMap.get(key).sort(DependencyNodeHopCountPair::compareTo);
232             return true;
233         }
234 
235         @Override
236         public boolean visitLeave(DependencyNode node) {
237             return parentsVisitor.visitLeave(node);
238         }
239 
240         public List<List<DependencyNode>> getConflicts() {
241             List<List<DependencyNode>> output = new ArrayList<>();
242             for (List<DependencyNodeHopCountPair> pairs : keyToPairsMap.values()) {
243                 if (containsConflicts(pairs)) {
244                     List<DependencyNode> outputSubList = new ArrayList<>(pairs.size());
245                     for (DependencyNodeHopCountPair pair : pairs) {
246                         outputSubList.add(pair.getNode());
247                     }
248                     output.add(outputSubList);
249                 }
250             }
251             return output;
252         }
253 
254         private boolean containsConflicts(List<DependencyNodeHopCountPair> pairs) {
255             DependencyNodeHopCountPair resolvedPair = pairs.get(0);
256             ArtifactVersion resolvedVersion = resolvedPair.extractArtifactVersion(uniqueVersions, false);
257 
258             for (DependencyNodeHopCountPair pair : pairs) {
259                 ArtifactVersion version = pair.extractArtifactVersion(uniqueVersions, true);
260                 if (resolvedVersion.compareTo(version) < 0) {
261                     return true;
262                 }
263             }
264             return false;
265         }
266 
267         @Override
268         public DependencyNode getParent(DependencyNode node) {
269             return parentsVisitor.getParent(node);
270         }
271     }
272 
273     private static class DependencyNodeHopCountPair implements Comparable<DependencyNodeHopCountPair> {
274         private final DependencyNode node;
275         private int hopCount;
276         private final ParentNodeProvider parentNodeProvider;
277 
278         private DependencyNodeHopCountPair(DependencyNode node, ParentNodeProvider parentNodeProvider) {
279             this.parentNodeProvider = parentNodeProvider;
280             this.node = node;
281             countHops();
282         }
283 
284         private void countHops() {
285             hopCount = 0;
286             DependencyNode parent = parentNodeProvider.getParent(node);
287             while (parent != null) {
288                 hopCount++;
289                 parent = parentNodeProvider.getParent(parent);
290             }
291         }
292 
293         private String constructKey() {
294             Artifact artifact = ArtifactUtils.toArtifact(node);
295             return artifact.getGroupId() + ":" + artifact.getArtifactId();
296         }
297 
298         public DependencyNode getNode() {
299             return node;
300         }
301 
302         private ArtifactVersion extractArtifactVersion(boolean uniqueVersions, boolean usePremanagedVersion) {
303             if (usePremanagedVersion && DependencyManagerUtils.getPremanagedVersion(node) != null) {
304                 return new DefaultArtifactVersion(DependencyManagerUtils.getPremanagedVersion(node));
305             }
306 
307             Artifact artifact = ArtifactUtils.toArtifact(node);
308             String version = uniqueVersions ? artifact.getVersion() : artifact.getBaseVersion();
309             if (version != null) {
310                 return new DefaultArtifactVersion(version);
311             }
312             try {
313                 return artifact.getSelectedVersion();
314             } catch (OverConstrainedVersionException e) {
315                 throw new RuntimeException("Version ranges problem with " + node.getArtifact(), e);
316             }
317         }
318 
319         public int getHopCount() {
320             return hopCount;
321         }
322 
323         public int compareTo(DependencyNodeHopCountPair other) {
324             return Integer.compare(hopCount, other.getHopCount());
325         }
326     }
327 }