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 }