1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
50
51
52
53
54 @Named("requireUpperBoundDeps")
55 public final class RequireUpperBoundDeps extends AbstractStandardEnforcerRule {
56
57
58
59
60 private boolean uniqueVersions;
61
62
63
64
65
66
67 private List<String> excludes = null;
68
69
70
71
72
73
74 private List<String> includes = null;
75
76
77
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
92
93
94 public void setExcludes(List<String> excludes) {
95 this.excludes = excludes;
96 }
97
98
99
100
101
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 }