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.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   * 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     /**
114      * {@code true} if dependencies should be checked before Maven computes the final
115      * dependency tree. Setting this property will make the rule check dependencies
116      * before any conflicts are resolved. This is similar to the {@code verbose}
117      * parameter for the {@code tree} goal for {@code maven-dependency-plugin}.
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                 // This should never happen.
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                 // This project is also included in the path, but we do
210                 // not want that in the report.
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 }