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