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.text.ChoiceFormat;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.List;
29 import java.util.Objects;
30 import java.util.function.Predicate;
31 import java.util.stream.Collectors;
32
33 import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
34 import org.apache.maven.enforcer.rules.AbstractStandardEnforcerRule;
35 import org.apache.maven.enforcer.rules.utils.ArtifactMatcher;
36 import org.apache.maven.enforcer.rules.utils.ArtifactUtils;
37 import org.apache.maven.execution.MavenSession;
38 import org.apache.maven.project.MavenProject;
39 import org.eclipse.aether.RepositorySystem;
40 import org.eclipse.aether.collection.DependencyCollectionException;
41 import org.eclipse.aether.graph.DependencyFilter;
42 import org.eclipse.aether.graph.DependencyNode;
43 import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
44 import org.eclipse.aether.util.graph.visitor.PathRecordingDependencyVisitor;
45 import org.eclipse.aether.util.version.GenericVersionScheme;
46 import org.eclipse.aether.version.InvalidVersionSpecificationException;
47 import org.eclipse.aether.version.VersionConstraint;
48
49
50
51
52
53
54
55
56
57
58
59
60 @Named("banDynamicVersions")
61 public final class BanDynamicVersions extends AbstractStandardEnforcerRule {
62
63 private static final String RELEASE = "RELEASE";
64
65 private static final String LATEST = "LATEST";
66
67 private static final String SNAPSHOT_SUFFIX = "-SNAPSHOT";
68
69
70
71
72 private boolean allowSnapshots;
73
74
75
76
77 private boolean allowLatest;
78
79
80
81
82 private boolean allowRelease;
83
84
85
86
87 private boolean allowRanges;
88
89
90
91
92
93 private boolean allowRangesWithIdenticalBounds;
94
95
96
97
98 private boolean excludeOptionals;
99
100
101
102
103 private List<String> excludedScopes = Collections.emptyList();
104
105
106
107
108
109
110
111
112 private List<String> ignores = null;
113
114
115
116
117
118
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
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
211
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 }