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