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