001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.enforcer.rules.dependency;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023
024import java.text.ChoiceFormat;
025import java.util.ArrayDeque;
026import java.util.ArrayList;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.Deque;
030import java.util.List;
031import java.util.Objects;
032import java.util.function.Predicate;
033import java.util.stream.Collectors;
034
035import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
036import org.apache.maven.enforcer.rules.AbstractStandardEnforcerRule;
037import org.apache.maven.enforcer.rules.utils.ArtifactMatcher;
038import org.apache.maven.enforcer.rules.utils.ArtifactUtils;
039import org.apache.maven.execution.MavenSession;
040import org.apache.maven.project.MavenProject;
041import org.eclipse.aether.RepositorySystem;
042import org.eclipse.aether.collection.DependencyCollectionException;
043import org.eclipse.aether.graph.DependencyNode;
044import org.eclipse.aether.graph.DependencyVisitor;
045import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor;
046import org.eclipse.aether.version.VersionConstraint;
047
048/**
049 * This rule bans dependencies having a version which requires resolution (i.e. dynamic versions which might change with
050 * each build). Dynamic versions are either
051 * <ul>
052 * <li>version ranges,</li>
053 * <li>the special placeholders {@code LATEST} or {@code RELEASE} or</li>
054 * <li>versions ending with {@code -SNAPSHOT}.
055 * </ul>
056 *
057 * @since 3.2.0
058 */
059@Named("banDynamicVersions")
060public final class BanDynamicVersions extends AbstractStandardEnforcerRule {
061
062    private static final String RELEASE = "RELEASE";
063
064    private static final String LATEST = "LATEST";
065
066    private static final String SNAPSHOT_SUFFIX = "-SNAPSHOT";
067
068    /**
069     * {@code true} if versions ending with {@code -SNAPSHOT} should be allowed
070     */
071    private boolean allowSnapshots;
072
073    /**
074     * {@code true} if versions using {@code LATEST} should be allowed
075     */
076    private boolean allowLatest;
077
078    /**
079     * {@code true} if versions using {@code RELEASE} should be allowed
080     */
081    private boolean allowRelease;
082
083    /**
084     * {@code true} if version ranges should be allowed
085     */
086    private boolean allowRanges;
087
088    /**
089     * {@code true} if ranges having the same upper and lower bound like {@code [1.0]} should be allowed.
090     * Only applicable if {@link #allowRanges} is not set to {@code true}.
091     */
092    private boolean allowRangesWithIdenticalBounds;
093
094    /**
095     * {@code true} if optional dependencies should not be checked
096     */
097    private boolean excludeOptionals;
098
099    /**
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}