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.graph;
20  
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.IdentityHashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Objects;
27  import java.util.concurrent.ConcurrentHashMap;
28  import java.util.stream.Collectors;
29  
30  import org.apache.maven.execution.ProjectDependencyGraph;
31  import org.apache.maven.project.MavenProject;
32  
33  /**
34   * Provides a sub view of another dependency graph.
35   *
36   * @author Benjamin Bentmann
37   */
38  class FilteredProjectDependencyGraph implements ProjectDependencyGraph {
39  
40      private final ProjectDependencyGraph projectDependencyGraph;
41  
42      private final Map<MavenProject, ?> whiteList;
43  
44      private final List<MavenProject> sortedProjects;
45  
46      private final Map<Key, List<MavenProject>> cache = new ConcurrentHashMap<>();
47  
48      private static class Key {
49          private final MavenProject project;
50          private final boolean transitive;
51          private final boolean upstream;
52  
53          private Key(MavenProject project, boolean transitive, boolean upstream) {
54              this.project = project;
55              this.transitive = transitive;
56              this.upstream = upstream;
57          }
58  
59          @Override
60          public boolean equals(Object o) {
61              if (o == null || getClass() != o.getClass()) {
62                  return false;
63              }
64              Key key = (Key) o;
65              return Objects.equals(project, key.project) && transitive == key.transitive && upstream == key.upstream;
66          }
67  
68          @Override
69          public int hashCode() {
70              return Objects.hash(project, transitive, upstream);
71          }
72      }
73  
74      /**
75       * Creates a new project dependency graph from the specified graph.
76       *
77       * @param projectDependencyGraph The project dependency graph to create a sub view from, must not be {@code null}.
78       * @param whiteList The projects on which the dependency view should focus, must not be {@code null}.
79       */
80      FilteredProjectDependencyGraph(
81              ProjectDependencyGraph projectDependencyGraph, Collection<? extends MavenProject> whiteList) {
82          this.projectDependencyGraph =
83                  Objects.requireNonNull(projectDependencyGraph, "projectDependencyGraph cannot be null");
84          this.whiteList = new IdentityHashMap<>();
85          for (MavenProject project : whiteList) {
86              this.whiteList.put(project, null);
87          }
88          this.sortedProjects = projectDependencyGraph.getSortedProjects().stream()
89                  .filter(this.whiteList::containsKey)
90                  .collect(Collectors.toList());
91      }
92  
93      /**
94       * @since 3.5.0
95       */
96      @Override
97      public List<MavenProject> getAllProjects() {
98          return this.projectDependencyGraph.getAllProjects();
99      }
100 
101     @Override
102     public List<MavenProject> getSortedProjects() {
103         return new ArrayList<>(sortedProjects);
104     }
105 
106     @Override
107     public List<MavenProject> getDownstreamProjects(MavenProject project, boolean transitive) {
108         Key key = new Key(project, transitive, false);
109         // Do not use computeIfAbsent here, as the computation is recursive
110         // and this is not supported by computeIfAbsent.
111         List<MavenProject> list = cache.get(key);
112         if (list == null) {
113             list = applyFilter(projectDependencyGraph.getDownstreamProjects(project, transitive), transitive, false);
114             cache.put(key, list);
115         }
116         return list;
117     }
118 
119     @Override
120     public List<MavenProject> getUpstreamProjects(MavenProject project, boolean transitive) {
121         Key key = new Key(project, transitive, true);
122         // Do not use computeIfAbsent here, as the computation is recursive
123         // and this is not supported by computeIfAbsent.
124         List<MavenProject> list = cache.get(key);
125         if (list == null) {
126             list = applyFilter(projectDependencyGraph.getUpstreamProjects(project, transitive), transitive, true);
127             cache.put(key, list);
128         }
129         return list;
130     }
131 
132     /**
133      * Filter out whitelisted projects with a big twist:
134      * Assume we have all projects {@code a, b, c} while active are {@code a, c} and relation among all projects
135      * is {@code a -> b -> c}. This method handles well the case for transitive list. But, for non-transitive we need
136      * to "pull in" transitive dependencies of eliminated projects, as for case above, the properly filtered list would
137      * be {@code a -> c}.
138      * <p>
139      * Original code would falsely report {@code a} project as "without dependencies", basically would lose link due
140      * filtering. This causes build ordering issues in concurrent builders.
141      */
142     private List<MavenProject> applyFilter(
143             Collection<? extends MavenProject> projects, boolean transitive, boolean upstream) {
144         List<MavenProject> filtered = new ArrayList<>(projects.size());
145         for (MavenProject project : projects) {
146             if (whiteList.containsKey(project)) {
147                 filtered.add(project);
148             } else if (!transitive) {
149                 filtered.addAll(upstream ? getUpstreamProjects(project, false) : getDownstreamProjects(project, false));
150             }
151         }
152         if (filtered.isEmpty() || filtered.size() == 1) {
153             // Optimization to skip streaming, distincting, and collecting to a new list when there is zero or one
154             // project, aka there can't be duplicates.
155             return filtered;
156         }
157 
158         // Distinct the projects to avoid duplicates.  Duplicates are possible in multi-module projects.
159         //
160         // Given a scenario where there is an aggregate POM with modules A, B, C, D, and E and project E depends on
161         // A, B, C, and D. If the aggregate POM is being filtered for non-transitive and downstream dependencies where
162         // only A, C, and E are whitelisted duplicates will occur. When scanning projects A, C, and E, those will be
163         // added to 'filtered' as they are whitelisted. When scanning B and D, they are not whitelisted, and since
164         // transitive is false, their downstream dependencies will be added to 'filtered'. E is a downstream dependency
165         // of A, B, C, and D, so when scanning B and D, E will be added again 'filtered'.
166         //
167         // Without de-duplication, the final list would contain E three times, once for E being in the projects and
168         // whitelisted, and twice more for E being a downstream dependency of B and D.
169         return filtered.stream().distinct().collect(Collectors.toList());
170     }
171 
172     @Override
173     public String toString() {
174         return getSortedProjects().toString();
175     }
176 }