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.graph.DependencyFilter;
41 import org.eclipse.aether.graph.DependencyNode;
42 import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
43 import org.eclipse.aether.util.graph.visitor.PathRecordingDependencyVisitor;
44 import org.eclipse.aether.util.version.GenericVersionScheme;
45 import org.eclipse.aether.version.InvalidVersionSpecificationException;
46 import org.eclipse.aether.version.VersionConstraint;
47
48
49
50
51
52
53
54
55
56
57
58
59 @Named("banDynamicVersions")
60 public final class BanDynamicVersions extends AbstractStandardEnforcerRule {
61
62 private static final String RELEASE = "RELEASE";
63
64 private static final String LATEST = "LATEST";
65
66 private static final String SNAPSHOT_SUFFIX = "-SNAPSHOT";
67
68
69
70
71 private boolean allowSnapshots;
72
73
74
75
76 private boolean allowLatest;
77
78
79
80
81 private boolean allowRelease;
82
83
84
85
86 private boolean allowRanges;
87
88
89
90
91
92 private boolean allowRangesWithIdenticalBounds;
93
94
95
96
97 private boolean excludeOptionals;
98
99
100
101
102 private List<String> excludedScopes = Collections.emptyList();
103
104
105
106
107
108
109
110
111 private List<String> ignores = null;
112
113
114
115
116
117
118
119 private boolean verbose;
120
121 private final ResolverUtil resolverUtil;
122
123 @Inject
124 public BanDynamicVersions(
125 MavenProject project, RepositorySystem repoSystem, MavenSession mavenSession, ResolverUtil resolverUtil) {
126 this.resolverUtil = Objects.requireNonNull(resolverUtil);
127 }
128
129 private final class BannedDynamicVersionCollector implements DependencyFilter {
130
131 private boolean isRoot = true;
132
133 private List<String> violations;
134
135 private final Predicate<DependencyNode> predicate;
136
137 private GenericVersionScheme versionScheme;
138
139 public List<String> getViolations() {
140 return violations;
141 }
142
143 BannedDynamicVersionCollector(Predicate<DependencyNode> predicate) {
144 this.predicate = predicate;
145 this.violations = new ArrayList<>();
146 this.versionScheme = new GenericVersionScheme();
147 }
148
149 private boolean isBannedDynamicVersion(VersionConstraint versionConstraint) {
150 if (versionConstraint.getVersion() != null) {
151 if (versionConstraint.getVersion().toString().equals(LATEST)) {
152 return !allowLatest;
153 } else if (versionConstraint.getVersion().toString().equals(RELEASE)) {
154 return !allowRelease;
155 } else if (versionConstraint.getVersion().toString().endsWith(SNAPSHOT_SUFFIX)) {
156 return !allowSnapshots;
157 }
158 } else if (versionConstraint.getRange() != null) {
159 if (allowRangesWithIdenticalBounds
160 && Objects.equals(
161 versionConstraint.getRange().getLowerBound(),
162 versionConstraint.getRange().getUpperBound())) {
163 return false;
164 }
165 return !allowRanges;
166 } else {
167 getLog().warn("Unexpected version constraint found: " + versionConstraint);
168 }
169 return false;
170 }
171
172 @Override
173 public boolean accept(DependencyNode node, List<DependencyNode> parents) {
174 if (isRoot) {
175 isRoot = false;
176 return false;
177 }
178 getLog().debug("Found node " + node + " with version constraint " + node.getVersionConstraint());
179 if (!predicate.test(node)) {
180 return false;
181 }
182 VersionConstraint versionConstraint = node.getVersionConstraint();
183 if (isBannedDynamicVersion(versionConstraint)) {
184 addViolation(versionConstraint, node, parents);
185 return true;
186 }
187 try {
188 if (verbose) {
189 String premanagedVersion = DependencyManagerUtils.getPremanagedVersion(node);
190 if (premanagedVersion != null) {
191 VersionConstraint premanagedContraint = versionScheme.parseVersionConstraint(premanagedVersion);
192 if (isBannedDynamicVersion(premanagedContraint)) {
193 addViolation(premanagedContraint, node, parents);
194 return true;
195 }
196 }
197 }
198 } catch (InvalidVersionSpecificationException ex) {
199
200 throw new RuntimeException("Failed to parse version for " + node, ex);
201 }
202 return false;
203 }
204
205 private void addViolation(
206 VersionConstraint versionContraint, DependencyNode node, List<DependencyNode> parents) {
207 List<DependencyNode> intermediatePath = new ArrayList<>(parents);
208 if (!intermediatePath.isEmpty()) {
209
210
211 intermediatePath.remove(intermediatePath.size() - 1);
212 }
213 violations.add("Dependency "
214 + node.getDependency()
215 + dumpIntermediatePath(intermediatePath)
216 + " is referenced with a banned dynamic version "
217 + versionContraint);
218 }
219 }
220
221 @Override
222 public void execute() throws EnforcerRuleException {
223 DependencyNode rootDependency =
224 resolverUtil.resolveTransitiveDependencies(verbose, excludeOptionals, excludedScopes);
225
226 List<String> violations = collectDependenciesWithBannedDynamicVersions(rootDependency);
227 if (!violations.isEmpty()) {
228 ChoiceFormat dependenciesFormat = new ChoiceFormat("1#dependency|1<dependencies");
229 throw new EnforcerRuleException("Found " + violations.size() + " "
230 + dependenciesFormat.format(violations.size())
231 + " with dynamic versions." + System.lineSeparator()
232 + String.join(System.lineSeparator(), violations));
233 }
234 }
235
236 private static String dumpIntermediatePath(Collection<DependencyNode> path) {
237 if (path.isEmpty()) {
238 return "";
239 }
240 return " via " + path.stream().map(n -> n.getArtifact().toString()).collect(Collectors.joining(" -> "));
241 }
242
243 private static final class ExcludeArtifactPatternsPredicate implements Predicate<DependencyNode> {
244
245 private final ArtifactMatcher artifactMatcher;
246
247 ExcludeArtifactPatternsPredicate(List<String> excludes) {
248 this.artifactMatcher = new ArtifactMatcher(excludes, Collections.emptyList());
249 }
250
251 @Override
252 public boolean test(DependencyNode depNode) {
253 return !artifactMatcher.match(ArtifactUtils.toArtifact(depNode));
254 }
255 }
256
257 private List<String> collectDependenciesWithBannedDynamicVersions(DependencyNode rootDependency) {
258 Predicate<DependencyNode> predicate;
259 if (ignores != null && !ignores.isEmpty()) {
260 predicate = new ExcludeArtifactPatternsPredicate(ignores);
261 } else {
262 predicate = d -> true;
263 }
264 BannedDynamicVersionCollector collector = new BannedDynamicVersionCollector(predicate);
265 rootDependency.accept(new PathRecordingDependencyVisitor(collector));
266 return collector.getViolations();
267 }
268
269 public void setVerbose(boolean verbose) {
270 this.verbose = verbose;
271 }
272
273 @Override
274 public String toString() {
275 return String.format(
276 "BanDynamicVersions[allowSnapshots=%b, allowLatest=%b, allowRelease=%b, allowRanges=%b, allowRangesWithIdenticalBounds=%b, excludeOptionals=%b, excludedScopes=%s, ignores=%s, verbose=%b]",
277 allowSnapshots,
278 allowLatest,
279 allowRelease,
280 allowRanges,
281 allowRangesWithIdenticalBounds,
282 excludeOptionals,
283 excludedScopes,
284 ignores,
285 verbose);
286 }
287 }