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.util.List;
025import java.util.Objects;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.commons.lang3.StringUtils;
030import org.apache.maven.RepositoryUtils;
031import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
032import org.apache.maven.enforcer.rules.AbstractStandardEnforcerRule;
033import org.apache.maven.enforcer.rules.utils.ArtifactMatcher;
034import org.apache.maven.enforcer.rules.utils.ArtifactUtils;
035import org.apache.maven.execution.MavenSession;
036import org.eclipse.aether.artifact.ArtifactTypeRegistry;
037import org.eclipse.aether.graph.Dependency;
038import org.eclipse.aether.graph.DependencyNode;
039
040import static java.util.Optional.ofNullable;
041
042/**
043 * This rule bans all transitive dependencies. There is a configuration option to exclude certain artifacts from being
044 * checked.
045 *
046 * @author Jakub Senko
047 */
048@Named("banTransitiveDependencies")
049public final class BanTransitiveDependencies extends AbstractStandardEnforcerRule {
050
051    /**
052     * Specify the dependencies that will be ignored. This can be a list of artifacts in the format
053     * <code>groupId[:artifactId][:version][:type][:scope]</code>. Wildcard '*' can be used to in place of specific
054     * section (ie group:*:1.0 will match both 'group:artifact:1.0' and 'group:anotherArtifact:1.0') <br>
055     * You can override this patterns by using includes. Version is a string representing standard maven version range.
056     * Empty patterns will be ignored.
057     */
058    private List<String> excludes;
059
060    /**
061     * Specify the dependencies that will be checked. These are exceptions to excludes intended for more convenient and
062     * finer settings. This can be a list of artifacts in the format
063     * <code>groupId[:artifactId][:version][:type][:scope]</code>. Wildcard '*' can be used to in place of specific
064     * section (ie group:*:1.0 will match both 'group:artifact:1.0' and 'group:anotherArtifact:1.0') <br>
065     * Version is a string representing standard maven version range. Empty patterns will be ignored.
066     */
067    private List<String> includes;
068
069    private final MavenSession session;
070
071    private final ResolverUtil resolverUtil;
072
073    @Inject
074    public BanTransitiveDependencies(MavenSession session, ResolverUtil resolverUtil) {
075        this.session = Objects.requireNonNull(session);
076        this.resolverUtil = Objects.requireNonNull(resolverUtil);
077    }
078
079    /**
080     * Searches dependency tree recursively for transitive dependencies that are not excluded, while generating nice
081     * info message along the way.
082     */
083    private static boolean searchTree(
084            DependencyNode node,
085            int level,
086            ArtifactMatcher excludes,
087            Set<Dependency> directDependencies,
088            StringBuilder message) {
089
090        List<DependencyNode> children = node.getChildren();
091
092        /*
093         * if the node is deeper than direct dependency and is empty, it is transitive.
094         */
095        boolean hasTransitiveDependencies = level > 1;
096
097        boolean excluded = false;
098
099        /*
100         * holds recursive message from children, will be appended to current message if this node has any transitive
101         * descendants if message is null, don't generate recursive message.
102         */
103        StringBuilder messageFromChildren = message == null ? null : new StringBuilder();
104
105        if (excludes.match(ArtifactUtils.toArtifact(node))) {
106            // is excluded, we don't care about descendants
107            excluded = true;
108            hasTransitiveDependencies = false;
109        } else if (directDependencies.contains(node.getDependency())) {
110            hasTransitiveDependencies = false;
111        } else {
112            for (DependencyNode childNode : children) {
113                /*
114                 * if any of the children has transitive d. so does the parent
115                 */
116                hasTransitiveDependencies = hasTransitiveDependencies
117                        || searchTree(childNode, level + 1, excludes, directDependencies, messageFromChildren);
118            }
119        }
120
121        if ((excluded || hasTransitiveDependencies) && message != null) // then generate message
122        {
123            message.append(StringUtils.repeat("   ", level)).append(node.getArtifact());
124
125            if (excluded) {
126                message.append(" [excluded]").append(System.lineSeparator());
127            }
128
129            if (hasTransitiveDependencies) {
130                if (level > 0) {
131                    message.append(" has transitive dependencies:");
132                }
133
134                message.append(System.lineSeparator()).append(messageFromChildren);
135            }
136        }
137
138        return hasTransitiveDependencies;
139    }
140
141    @Override
142    public void execute() throws EnforcerRuleException {
143        ArtifactTypeRegistry artifactTypeRegistry =
144                session.getRepositorySession().getArtifactTypeRegistry();
145        ArtifactMatcher exclusions = new ArtifactMatcher(excludes, includes);
146        Set<Dependency> directDependencies = session.getCurrentProject().getDependencies().stream()
147                .map(d -> RepositoryUtils.toDependency(d, artifactTypeRegistry))
148                .collect(Collectors.toSet());
149
150        DependencyNode rootNode = resolverUtil.resolveTransitiveDependencies();
151        StringBuilder generatedMessage = new StringBuilder();
152        if (searchTree(rootNode, 0, exclusions, directDependencies, generatedMessage)) {
153            throw new EnforcerRuleException(ofNullable(getMessage()).orElse(generatedMessage.toString()));
154        }
155    }
156
157    @Override
158    public String toString() {
159        return String.format("BanTransitiveDependencies[message=%s, excludes=%s]", getMessage(), excludes);
160    }
161}