001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.maven.enforcer.rules.dependency; 020 021import javax.inject.Inject; 022import javax.inject.Named; 023 024import java.util.ArrayList; 025import java.util.Arrays; 026import java.util.Collections; 027import java.util.HashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.Objects; 031 032import org.apache.maven.artifact.Artifact; 033import org.apache.maven.artifact.versioning.ArtifactVersion; 034import org.apache.maven.artifact.versioning.DefaultArtifactVersion; 035import org.apache.maven.artifact.versioning.OverConstrainedVersionException; 036import org.apache.maven.enforcer.rule.api.EnforcerRuleException; 037import org.apache.maven.enforcer.rules.AbstractStandardEnforcerRule; 038import org.apache.maven.enforcer.rules.utils.ArtifactUtils; 039import org.apache.maven.enforcer.rules.utils.ParentNodeProvider; 040import org.apache.maven.enforcer.rules.utils.ParentsVisitor; 041import org.eclipse.aether.graph.DependencyNode; 042import org.eclipse.aether.graph.DependencyVisitor; 043import org.eclipse.aether.util.graph.manager.DependencyManagerUtils; 044 045import static org.apache.maven.artifact.Artifact.SCOPE_PROVIDED; 046import static org.apache.maven.artifact.Artifact.SCOPE_TEST; 047 048/** 049 * Rule to enforce that the resolved dependency is also the most recent one of all transitive dependencies. 050 * 051 * @author Geoffrey De Smet 052 * @since 1.1 053 */ 054@Named("requireUpperBoundDeps") 055public final class RequireUpperBoundDeps extends AbstractStandardEnforcerRule { 056 057 /** 058 * @since 1.3 059 */ 060 private boolean uniqueVersions; 061 062 /** 063 * Dependencies to ignore. 064 * 065 * @since TBD 066 */ 067 private List<String> excludes = null; 068 069 /** 070 * Dependencies to include. 071 * 072 * @since 3.0.0 073 */ 074 private List<String> includes = null; 075 076 /** 077 * Scope to exclude. 078 */ 079 private List<String> excludedScopes = Arrays.asList(SCOPE_TEST, SCOPE_PROVIDED); 080 081 private RequireUpperBoundDepsVisitor upperBoundDepsVisitor; 082 083 private final ResolverUtil resolverUtil; 084 085 @Inject 086 public RequireUpperBoundDeps(ResolverUtil resolverUtil) { 087 this.resolverUtil = Objects.requireNonNull(resolverUtil); 088 } 089 090 /** 091 * Sets dependencies to exclude. 092 * @param excludes a list of {@code groupId:artifactId} names 093 */ 094 public void setExcludes(List<String> excludes) { 095 this.excludes = excludes; 096 } 097 098 /** 099 * 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}