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; 020 021import javax.inject.Inject; 022import javax.inject.Named; 023 024import java.text.ChoiceFormat; 025import java.util.ArrayDeque; 026import java.util.ArrayList; 027import java.util.Collection; 028import java.util.Collections; 029import java.util.Deque; 030import java.util.List; 031import java.util.Objects; 032import java.util.function.Predicate; 033import java.util.stream.Collectors; 034 035import org.apache.maven.RepositoryUtils; 036import org.apache.maven.enforcer.rule.api.EnforcerRuleException; 037import org.apache.maven.enforcer.rules.utils.ArtifactMatcher; 038import org.apache.maven.enforcer.rules.utils.ArtifactUtils; 039import org.apache.maven.execution.MavenSession; 040import org.apache.maven.project.MavenProject; 041import org.eclipse.aether.DefaultRepositorySystemSession; 042import org.eclipse.aether.RepositorySystem; 043import org.eclipse.aether.RepositorySystemSession; 044import org.eclipse.aether.collection.CollectRequest; 045import org.eclipse.aether.collection.CollectResult; 046import org.eclipse.aether.collection.DependencyCollectionException; 047import org.eclipse.aether.collection.DependencySelector; 048import org.eclipse.aether.graph.Dependency; 049import org.eclipse.aether.graph.DependencyNode; 050import org.eclipse.aether.graph.DependencyVisitor; 051import org.eclipse.aether.util.graph.selector.AndDependencySelector; 052import org.eclipse.aether.util.graph.selector.OptionalDependencySelector; 053import org.eclipse.aether.util.graph.selector.ScopeDependencySelector; 054import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor; 055import org.eclipse.aether.version.VersionConstraint; 056 057/** 058 * This rule bans dependencies having a version which requires resolution (i.e. dynamic versions which might change with 059 * each build). Dynamic versions are either 060 * <ul> 061 * <li>version ranges,</li> 062 * <li>the special placeholders {@code LATEST} or {@code RELEASE} or</li> 063 * <li>versions ending with {@code -SNAPSHOT}. 064 * </ul> 065 * 066 * @since 3.2.0 067 */ 068@Named("banDynamicVersions") 069public final class BanDynamicVersions extends AbstractStandardEnforcerRule { 070 071 private static final String RELEASE = "RELEASE"; 072 073 private static final String LATEST = "LATEST"; 074 075 private static final String SNAPSHOT_SUFFIX = "-SNAPSHOT"; 076 077 /** 078 * {@code true} if versions ending with {@code -SNAPSHOT} should be allowed 079 */ 080 private boolean allowSnapshots; 081 082 /** 083 * {@code true} if versions using {@code LATEST} should be allowed 084 */ 085 private boolean allowLatest; 086 087 /** 088 * {@code true} if versions using {@code RELEASE} should be allowed 089 */ 090 private boolean allowRelease; 091 092 /** 093 * {@code true} if version ranges should be allowed 094 */ 095 private boolean allowRanges; 096 097 /** 098 * {@code true} if ranges having the same upper and lower bound like {@code [1.0]} should be allowed. 099 * Only applicable if {@link #allowRanges} is not set to {@code true}. 100 */ 101 private boolean allowRangesWithIdenticalBounds; 102 103 /** 104 * {@code true} if optional dependencies should not be checked 105 */ 106 private boolean excludeOptionals; 107 108 /** 109 * the scopes of dependencies which should be excluded from this rule 110 */ 111 private String[] excludedScopes; 112 113 /** 114 * Specify the ignored dependencies. This can be a list of artifacts in the format 115 * <code>groupId[:artifactId[:version[:type[:scope:[classifier]]]]]</code>. 116 * Any of the sections can be a wildcard by using '*' (e.g. {@code group:*:1.0}). 117 * <br> 118 * Any of the ignored dependencies may have dynamic versions. 119 */ 120 private List<String> ignores = null; 121 122 private final MavenProject project; 123 124 private final RepositorySystem repoSystem; 125 126 private final MavenSession mavenSession; 127 128 @Inject 129 public BanDynamicVersions(MavenProject project, RepositorySystem repoSystem, MavenSession mavenSession) { 130 this.project = Objects.requireNonNull(project); 131 this.repoSystem = Objects.requireNonNull(repoSystem); 132 this.mavenSession = Objects.requireNonNull(mavenSession); 133 } 134 135 private final class BannedDynamicVersionCollector implements DependencyVisitor { 136 137 private final Deque<DependencyNode> nodeStack; // all intermediate nodes (without the root node) 138 139 private boolean isRoot = true; 140 141 private int numViolations; 142 143 private final Predicate<DependencyNode> predicate; 144 145 public int getNumViolations() { 146 return numViolations; 147 } 148 149 BannedDynamicVersionCollector(Predicate<DependencyNode> predicate) { 150 nodeStack = new ArrayDeque<>(); 151 this.predicate = predicate; 152 this.isRoot = true; 153 numViolations = 0; 154 } 155 156 private boolean isBannedDynamicVersion(VersionConstraint versionConstraint) { 157 if (versionConstraint.getVersion() != null) { 158 if (versionConstraint.getVersion().toString().equals(LATEST)) { 159 return !allowLatest; 160 } else if (versionConstraint.getVersion().toString().equals(RELEASE)) { 161 return !allowRelease; 162 } else if (versionConstraint.getVersion().toString().endsWith(SNAPSHOT_SUFFIX)) { 163 return !allowSnapshots; 164 } 165 } else if (versionConstraint.getRange() != null) { 166 if (allowRangesWithIdenticalBounds 167 && Objects.equals( 168 versionConstraint.getRange().getLowerBound(), 169 versionConstraint.getRange().getUpperBound())) { 170 return false; 171 } 172 return !allowRanges; 173 } else { 174 getLog().warn("Unexpected version constraint found: " + versionConstraint); 175 } 176 return false; 177 } 178 179 @Override 180 public boolean visitEnter(DependencyNode node) { 181 if (isRoot) { 182 isRoot = false; 183 } else { 184 getLog().debug("Found node " + node + " with version constraint " + node.getVersionConstraint()); 185 if (predicate.test(node) && isBannedDynamicVersion(node.getVersionConstraint())) { 186 getLog().warnOrError(() -> new StringBuilder() 187 .append("Dependency ") 188 .append(node.getDependency()) 189 .append(dumpIntermediatePath(nodeStack)) 190 .append(" is referenced with a banned dynamic version ") 191 .append(node.getVersionConstraint())); 192 numViolations++; 193 return false; 194 } 195 nodeStack.addLast(node); 196 } 197 return true; 198 } 199 200 @Override 201 public boolean visitLeave(DependencyNode node) { 202 if (!nodeStack.isEmpty()) { 203 nodeStack.removeLast(); 204 } 205 return true; 206 } 207 } 208 209 @Override 210 public void execute() throws EnforcerRuleException { 211 212 // get a new session to be able to tweak the dependency selector 213 DefaultRepositorySystemSession newRepoSession = 214 new DefaultRepositorySystemSession(mavenSession.getRepositorySession()); 215 216 Collection<DependencySelector> depSelectors = new ArrayList<>(); 217 depSelectors.add(new ScopeDependencySelector(excludedScopes)); 218 if (excludeOptionals) { 219 depSelectors.add(new OptionalDependencySelector()); 220 } 221 newRepoSession.setDependencySelector(new AndDependencySelector(depSelectors)); 222 223 Dependency rootDependency = RepositoryUtils.toDependency(project.getArtifact(), null); 224 try { 225 // use root dependency with unresolved direct dependencies 226 int numViolations = emitDependenciesWithBannedDynamicVersions(rootDependency, newRepoSession); 227 if (numViolations > 0) { 228 ChoiceFormat dependenciesFormat = new ChoiceFormat("1#dependency|1<dependencies"); 229 throw new EnforcerRuleException("Found " + numViolations + " " 230 + dependenciesFormat.format(numViolations) 231 + " with dynamic versions. Look at the warnings emitted above for the details."); 232 } 233 } catch (DependencyCollectionException e) { 234 throw new EnforcerRuleException("Could not retrieve dependency metadata for project", e); 235 } 236 } 237 238 private static String dumpIntermediatePath(Collection<DependencyNode> path) { 239 if (path.isEmpty()) { 240 return ""; 241 } 242 return " via " + path.stream().map(n -> n.getArtifact().toString()).collect(Collectors.joining(" -> ")); 243 } 244 245 private static final class ExcludeArtifactPatternsPredicate implements Predicate<DependencyNode> { 246 247 private final ArtifactMatcher artifactMatcher; 248 249 ExcludeArtifactPatternsPredicate(List<String> excludes) { 250 this.artifactMatcher = new ArtifactMatcher(excludes, Collections.emptyList()); 251 } 252 253 @Override 254 public boolean test(DependencyNode depNode) { 255 return artifactMatcher.match(ArtifactUtils.toArtifact(depNode)); 256 } 257 } 258 259 private int emitDependenciesWithBannedDynamicVersions( 260 Dependency rootDependency, RepositorySystemSession repoSession) throws DependencyCollectionException { 261 CollectRequest collectRequest = new CollectRequest(rootDependency, project.getRemoteProjectRepositories()); 262 CollectResult collectResult = repoSystem.collectDependencies(repoSession, collectRequest); 263 Predicate<DependencyNode> predicate; 264 if (ignores != null && !ignores.isEmpty()) { 265 predicate = new ExcludeArtifactPatternsPredicate(ignores); 266 } else { 267 predicate = d -> true; 268 } 269 BannedDynamicVersionCollector bannedDynamicVersionCollector = new BannedDynamicVersionCollector(predicate); 270 DependencyVisitor depVisitor = new TreeDependencyVisitor(bannedDynamicVersionCollector); 271 collectResult.getRoot().accept(depVisitor); 272 return bannedDynamicVersionCollector.getNumViolations(); 273 } 274 275 @Override 276 public String toString() { 277 return String.format( 278 "BanDynamicVersions[allowSnapshots=%b, allowLatest=%b, allowRelease=%b, allowRanges=%b, allowRangesWithIdenticalBounds=%b, excludeOptionals=%b, excludedScopes=%s, ignores=%s]", 279 allowSnapshots, 280 allowLatest, 281 allowRelease, 282 allowRanges, 283 allowRangesWithIdenticalBounds, 284 excludeOptionals, 285 excludedScopes, 286 ignores); 287 } 288}