View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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   * This rule bans dependencies having a version which requires resolution (i.e. dynamic versions which might change with
50   * each build). Dynamic versions are either
51   * <ul>
52   * <li>version ranges,</li>
53   * <li>the special placeholders {@code LATEST} or {@code RELEASE} or</li>
54   * <li>versions ending with {@code -SNAPSHOT}.
55   * </ul>
56   *
57   * @since 3.2.0
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       * {@code true} if versions ending with {@code -SNAPSHOT} should be allowed
70       */
71      private boolean allowSnapshots;
72  
73      /**
74       * {@code true} if versions using {@code LATEST} should be allowed
75       */
76      private boolean allowLatest;
77  
78      /**
79       * {@code true} if versions using {@code RELEASE} should be allowed
80       */
81      private boolean allowRelease;
82  
83      /**
84       * {@code true} if version ranges should be allowed
85       */
86      private boolean allowRanges;
87  
88      /**
89       * {@code true} if ranges having the same upper and lower bound like {@code [1.0]} should be allowed.
90       * Only applicable if {@link #allowRanges} is not set to {@code true}.
91       */
92      private boolean allowRangesWithIdenticalBounds;
93  
94      /**
95       * {@code true} if optional dependencies should not be checked
96       */
97      private boolean excludeOptionals;
98  
99      /**
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 }