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