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.util.List;
25  import java.util.Objects;
26  import java.util.Set;
27  import java.util.stream.Collectors;
28  
29  import org.apache.commons.lang3.StringUtils;
30  import org.apache.maven.RepositoryUtils;
31  import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
32  import org.apache.maven.enforcer.rules.AbstractStandardEnforcerRule;
33  import org.apache.maven.enforcer.rules.utils.ArtifactMatcher;
34  import org.apache.maven.enforcer.rules.utils.ArtifactUtils;
35  import org.apache.maven.execution.MavenSession;
36  import org.eclipse.aether.artifact.ArtifactTypeRegistry;
37  import org.eclipse.aether.graph.Dependency;
38  import org.eclipse.aether.graph.DependencyNode;
39  
40  import static java.util.Optional.ofNullable;
41  
42  /**
43   * This rule bans all transitive dependencies. There is a configuration option to exclude certain artifacts from being
44   * checked.
45   *
46   * @author Jakub Senko
47   */
48  @Named("banTransitiveDependencies")
49  public final class BanTransitiveDependencies extends AbstractStandardEnforcerRule {
50  
51      /**
52       * Specify the dependencies that will be ignored. This can be a list of artifacts in the format
53       * <code>groupId[:artifactId][:version][:type][:scope]</code>. Wildcard '*' can be used to in place of specific
54       * section (ie group:*:1.0 will match both 'group:artifact:1.0' and 'group:anotherArtifact:1.0') <br>
55       * You can override this patterns by using includes. Version is a string representing standard maven version range.
56       * Empty patterns will be ignored.
57       */
58      private List<String> excludes;
59  
60      /**
61       * Specify the dependencies that will be checked. These are exceptions to excludes intended for more convenient and
62       * finer settings. This can be a list of artifacts in the format
63       * <code>groupId[:artifactId][:version][:type][:scope]</code>. Wildcard '*' can be used to in place of specific
64       * section (ie group:*:1.0 will match both 'group:artifact:1.0' and 'group:anotherArtifact:1.0') <br>
65       * Version is a string representing standard maven version range. Empty patterns will be ignored.
66       */
67      private List<String> includes;
68  
69      private final MavenSession session;
70  
71      private final ResolverUtil resolverUtil;
72  
73      @Inject
74      public BanTransitiveDependencies(MavenSession session, ResolverUtil resolverUtil) {
75          this.session = Objects.requireNonNull(session);
76          this.resolverUtil = Objects.requireNonNull(resolverUtil);
77      }
78  
79      /**
80       * Searches dependency tree recursively for transitive dependencies that are not excluded, while generating nice
81       * info message along the way.
82       */
83      private static boolean searchTree(
84              DependencyNode node,
85              int level,
86              ArtifactMatcher excludes,
87              Set<Dependency> directDependencies,
88              StringBuilder message) {
89  
90          List<DependencyNode> children = node.getChildren();
91  
92          /*
93           * if the node is deeper than direct dependency and is empty, it is transitive.
94           */
95          boolean hasTransitiveDependencies = level > 1;
96  
97          boolean excluded = false;
98  
99          /*
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 }