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.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   * This rule bans dependencies having a version which requires resolution (i.e. dynamic versions which might change with
51   * each build). Dynamic versions are either
52   * <ul>
53   * <li>version ranges,</li>
54   * <li>the special placeholders {@code LATEST} or {@code RELEASE} or</li>
55   * <li>versions ending with {@code -SNAPSHOT}.
56   * </ul>
57   *
58   * @since 3.2.0
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       * {@code true} if versions ending with {@code -SNAPSHOT} should be allowed
71       */
72      private boolean allowSnapshots;
73  
74      /**
75       * {@code true} if versions using {@code LATEST} should be allowed
76       */
77      private boolean allowLatest;
78  
79      /**
80       * {@code true} if versions using {@code RELEASE} should be allowed
81       */
82      private boolean allowRelease;
83  
84      /**
85       * {@code true} if version ranges should be allowed
86       */
87      private boolean allowRanges;
88  
89      /**
90       * {@code true} if ranges having the same upper and lower bound like {@code [1.0]} should be allowed.
91       * Only applicable if {@link #allowRanges} is not set to {@code true}.
92       */
93      private boolean allowRangesWithIdenticalBounds;
94  
95      /**
96       * {@code true} if optional dependencies should not be checked
97       */
98      private boolean excludeOptionals;
99  
100     /**
101      * the scopes of dependencies which should be excluded from this rule
102      */
103     private List<String> excludedScopes = Collections.emptyList();
104 
105     /**
106      * Specify the ignored dependencies. This can be a list of artifacts in the format
107      * <code>groupId[:artifactId[:version[:type[:scope:[classifier]]]]]</code>.
108      * Any of the sections can be a wildcard by using '*' (e.g. {@code group:*:1.0}).
109      * <br>
110      * Any of the ignored dependencies may have dynamic versions.
111      */
112     private List<String> ignores = null;
113 
114     /**
115      * {@code true} if dependencies should be checked before Maven computes the final
116      * dependency tree. Setting this property will make the rule check dependencies
117      * before any conflicts are resolved. This is similar to the {@code verbose}
118      * parameter for the {@code tree} goal for {@code maven-dependency-plugin}.
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                 // This should never happen.
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                 // This project is also included in the path, but we do
211                 // not want that in the report.
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 }