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.project;
20  
21  import java.util.Collection;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.stream.Collectors;
27  
28  import org.apache.maven.api.model.Build;
29  import org.apache.maven.api.model.Dependency;
30  import org.apache.maven.api.model.Extension;
31  import org.apache.maven.api.model.Parent;
32  import org.apache.maven.api.model.Plugin;
33  import org.apache.maven.artifact.ArtifactUtils;
34  import org.codehaus.plexus.util.dag.CycleDetectedException;
35  import org.codehaus.plexus.util.dag.DAG;
36  import org.codehaus.plexus.util.dag.TopologicalSorter;
37  import org.codehaus.plexus.util.dag.Vertex;
38  
39  /**
40   * ProjectSorter
41   */
42  public class ProjectSorter {
43      private DAG dag;
44  
45      private List<MavenProject> sortedProjects;
46  
47      private Map<String, MavenProject> projectMap;
48  
49      /**
50       * Sort a list of projects.
51       * <ul>
52       * <li>collect all the vertices for the projects that we want to build.</li>
53       * <li>iterate through the deps of each project and if that dep is within
54       * the set of projects we want to build then add an edge, otherwise throw
55       * the edge away because that dependency is not within the set of projects
56       * we are trying to build. we assume a closed set.</li>
57       * <li>do a topo sort on the graph that remains.</li>
58       * </ul>
59       * @throws DuplicateProjectException if any projects are duplicated by id
60       */
61      // MAVENAPI FIXME: the DAG used is NOT only used to represent the dependency relation,
62      // but also for <parent>, <build><plugin>, <reports>. We need multiple DAG's
63      // since a DAG can only handle 1 type of relationship properly.
64      // Use case:  This is detected as a cycle:
65      // org.apache.maven:maven-plugin-api                -(PARENT)->
66      // org.apache.maven:maven                           -(inherited REPORTING)->
67      // org.apache.maven.plugins:maven-checkstyle-plugin -(DEPENDENCY)->
68      // org.apache.maven:maven-plugin-api
69      // In this case, both the verify and the report goals are called
70      // in a different lifecycle. Though the compiler-plugin has a valid use case, although
71      // that seems to work fine. We need to take versions and lifecycle into account.
72      public ProjectSorter(Collection<MavenProject> projects) throws CycleDetectedException, DuplicateProjectException {
73          dag = new DAG();
74  
75          // groupId:artifactId:version -> project
76          projectMap = new HashMap<>(projects.size() * 2);
77  
78          // groupId:artifactId -> (version -> vertex)
79          Map<String, Map<String, Vertex>> vertexMap = new HashMap<>(projects.size() * 2);
80  
81          for (MavenProject project : projects) {
82              String projectId = getId(project);
83  
84              MavenProject conflictingProject = projectMap.put(projectId, project);
85  
86              if (conflictingProject != null) {
87                  throw new DuplicateProjectException(
88                          projectId,
89                          conflictingProject.getFile(),
90                          project.getFile(),
91                          "Project '" + projectId + "' is duplicated in the reactor");
92              }
93  
94              String projectKey = ArtifactUtils.versionlessKey(project.getGroupId(), project.getArtifactId());
95  
96              Map<String, Vertex> vertices = vertexMap.computeIfAbsent(projectKey, k -> new HashMap<>(2, 1));
97  
98              vertices.put(project.getVersion(), dag.addVertex(projectId));
99          }
100 
101         for (Vertex projectVertex : dag.getVertices()) {
102             String projectId = projectVertex.getLabel();
103 
104             MavenProject project = projectMap.get(projectId);
105 
106             for (Dependency dependency : project.getModel().getDelegate().getDependencies()) {
107                 addEdge(
108                         projectMap,
109                         vertexMap,
110                         project,
111                         projectVertex,
112                         dependency.getGroupId(),
113                         dependency.getArtifactId(),
114                         dependency.getVersion(),
115                         false,
116                         false);
117             }
118 
119             Parent parent = project.getModel().getDelegate().getParent();
120 
121             if (parent != null) {
122                 // Parent is added as an edge, but must not cause a cycle - so we remove any other edges it has
123                 // in conflict
124                 addEdge(
125                         projectMap,
126                         vertexMap,
127                         null,
128                         projectVertex,
129                         parent.getGroupId(),
130                         parent.getArtifactId(),
131                         parent.getVersion(),
132                         true,
133                         false);
134             }
135 
136             Build build = project.getModel().getDelegate().getBuild();
137             if (build != null) {
138                 for (Plugin plugin : build.getPlugins()) {
139                     addEdge(
140                             projectMap,
141                             vertexMap,
142                             project,
143                             projectVertex,
144                             plugin.getGroupId(),
145                             plugin.getArtifactId(),
146                             plugin.getVersion(),
147                             false,
148                             true);
149 
150                     for (Dependency dependency : plugin.getDependencies()) {
151                         addEdge(
152                                 projectMap,
153                                 vertexMap,
154                                 project,
155                                 projectVertex,
156                                 dependency.getGroupId(),
157                                 dependency.getArtifactId(),
158                                 dependency.getVersion(),
159                                 false,
160                                 true);
161                     }
162                 }
163 
164                 for (Extension extension : build.getExtensions()) {
165                     addEdge(
166                             projectMap,
167                             vertexMap,
168                             project,
169                             projectVertex,
170                             extension.getGroupId(),
171                             extension.getArtifactId(),
172                             extension.getVersion(),
173                             false,
174                             true);
175                 }
176             }
177         }
178 
179         List<String> sortedProjectLabels = TopologicalSorter.sort(dag);
180 
181         this.sortedProjects = sortedProjectLabels.stream()
182                 .map(id -> projectMap.get(id))
183                 .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
184     }
185 
186     @SuppressWarnings("checkstyle:parameternumber")
187     private void addEdge(
188             Map<String, MavenProject> projectMap,
189             Map<String, Map<String, Vertex>> vertexMap,
190             MavenProject project,
191             Vertex projectVertex,
192             String groupId,
193             String artifactId,
194             String version,
195             boolean force,
196             boolean safe)
197             throws CycleDetectedException {
198         String projectKey = ArtifactUtils.versionlessKey(groupId, artifactId);
199 
200         Map<String, Vertex> vertices = vertexMap.get(projectKey);
201 
202         if (vertices != null) {
203             if (isSpecificVersion(version)) {
204                 Vertex vertex = vertices.get(version);
205                 if (vertex != null) {
206                     addEdge(projectVertex, vertex, project, projectMap, force, safe);
207                 }
208             } else {
209                 for (Vertex vertex : vertices.values()) {
210                     addEdge(projectVertex, vertex, project, projectMap, force, safe);
211                 }
212             }
213         }
214     }
215 
216     private void addEdge(
217             Vertex fromVertex,
218             Vertex toVertex,
219             MavenProject fromProject,
220             Map<String, MavenProject> projectMap,
221             boolean force,
222             boolean safe)
223             throws CycleDetectedException {
224         if (fromVertex.equals(toVertex)) {
225             return;
226         }
227 
228         if (fromProject != null) {
229             MavenProject toProject = projectMap.get(toVertex.getLabel());
230             fromProject.addProjectReference(toProject);
231         }
232 
233         if (force && toVertex.getChildren().contains(fromVertex)) {
234             dag.removeEdge(toVertex, fromVertex);
235         }
236 
237         try {
238             dag.addEdge(fromVertex, toVertex);
239         } catch (CycleDetectedException e) {
240             if (!safe) {
241                 throw e;
242             }
243         }
244     }
245 
246     private boolean isSpecificVersion(String version) {
247         return !((version == null || version.isEmpty()) || version.startsWith("[") || version.startsWith("("));
248     }
249 
250     // TODO !![jc; 28-jul-2005] check this; if we're using '-r' and there are aggregator tasks, this will result in
251     // weirdness.
252     public MavenProject getTopLevelProject() {
253         return sortedProjects.stream()
254                 .filter(MavenProject::isExecutionRoot)
255                 .findFirst()
256                 .orElse(null);
257     }
258 
259     public List<MavenProject> getSortedProjects() {
260         return sortedProjects;
261     }
262 
263     public boolean hasMultipleProjects() {
264         return sortedProjects.size() > 1;
265     }
266 
267     public List<String> getDependents(String id) {
268         return dag.getParentLabels(id);
269     }
270 
271     public List<String> getDependencies(String id) {
272         return dag.getChildLabels(id);
273     }
274 
275     public static String getId(MavenProject project) {
276         return ArtifactUtils.key(project.getGroupId(), project.getArtifactId(), project.getVersion());
277     }
278 
279     public DAG getDAG() {
280         return dag;
281     }
282 
283     public Map<String, MavenProject> getProjectMap() {
284         return projectMap;
285     }
286 }