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