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.ArrayDeque;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.Deque;
30 import java.util.List;
31 import java.util.Objects;
32 import java.util.function.Predicate;
33 import java.util.stream.Collectors;
34
35 import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
36 import org.apache.maven.enforcer.rules.AbstractStandardEnforcerRule;
37 import org.apache.maven.enforcer.rules.utils.ArtifactMatcher;
38 import org.apache.maven.enforcer.rules.utils.ArtifactUtils;
39 import org.apache.maven.execution.MavenSession;
40 import org.apache.maven.project.MavenProject;
41 import org.eclipse.aether.RepositorySystem;
42 import org.eclipse.aether.collection.DependencyCollectionException;
43 import org.eclipse.aether.graph.DependencyNode;
44 import org.eclipse.aether.graph.DependencyVisitor;
45 import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor;
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 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;
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 }