blob: a73641034730e4c475fb0d0cadf5f4e530e62d4a [file] [log] [blame]
package org.apache.maven.project;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Extension;
import org.apache.maven.model.Parent;
import org.apache.maven.model.Plugin;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.dag.CycleDetectedException;
import org.codehaus.plexus.util.dag.DAG;
import org.codehaus.plexus.util.dag.TopologicalSorter;
import org.codehaus.plexus.util.dag.Vertex;
/**
* ProjectSorter
*/
public class ProjectSorter
{
private DAG dag;
private List<MavenProject> sortedProjects;
private Map<String, MavenProject> projectMap;
/**
* Sort a list of projects.
* <ul>
* <li>collect all the vertices for the projects that we want to build.</li>
* <li>iterate through the deps of each project and if that dep is within
* the set of projects we want to build then add an edge, otherwise throw
* the edge away because that dependency is not within the set of projects
* we are trying to build. we assume a closed set.</li>
* <li>do a topo sort on the graph that remains.</li>
* </ul>
* @throws DuplicateProjectException if any projects are duplicated by id
*/
// MAVENAPI FIXME: the DAG used is NOT only used to represent the dependency relation,
// but also for <parent>, <build><plugin>, <reports>. We need multiple DAG's
// since a DAG can only handle 1 type of relationship properly.
// Usecase: This is detected as a cycle:
// org.apache.maven:maven-plugin-api -(PARENT)->
// org.apache.maven:maven -(inherited REPORTING)->
// org.apache.maven.plugins:maven-checkstyle-plugin -(DEPENDENCY)->
// org.apache.maven:maven-plugin-api
// In this case, both the verify and the report goals are called
// in a different lifecycle. Though the compiler-plugin has a valid usecase, although
// that seems to work fine. We need to take versions and lifecycle into account.
public ProjectSorter( Collection<MavenProject> projects )
throws CycleDetectedException, DuplicateProjectException
{
dag = new DAG();
// groupId:artifactId:version -> project
projectMap = new HashMap<>( projects.size() * 2 );
// groupId:artifactId -> (version -> vertex)
Map<String, Map<String, Vertex>> vertexMap = new HashMap<>( projects.size() * 2 );
for ( MavenProject project : projects )
{
String projectId = getId( project );
MavenProject conflictingProject = projectMap.put( projectId, project );
if ( conflictingProject != null )
{
throw new DuplicateProjectException( projectId, conflictingProject.getFile(), project.getFile(),
"Project '" + projectId + "' is duplicated in the reactor" );
}
String projectKey = ArtifactUtils.versionlessKey( project.getGroupId(), project.getArtifactId() );
Map<String, Vertex> vertices = vertexMap.computeIfAbsent( projectKey, k -> new HashMap<>( 2, 1 ) );
vertices.put( project.getVersion(), dag.addVertex( projectId ) );
}
for ( Vertex projectVertex : dag.getVertices() )
{
String projectId = projectVertex.getLabel();
MavenProject project = projectMap.get( projectId );
for ( Dependency dependency : project.getDependencies() )
{
addEdge( projectMap, vertexMap, project, projectVertex, dependency.getGroupId(),
dependency.getArtifactId(), dependency.getVersion(), false, false );
}
Parent parent = project.getModel().getParent();
if ( parent != null )
{
// Parent is added as an edge, but must not cause a cycle - so we remove any other edges it has
// in conflict
addEdge( projectMap, vertexMap, null, projectVertex, parent.getGroupId(), parent.getArtifactId(),
parent.getVersion(), true, false );
}
for ( Plugin plugin : project.getBuildPlugins() )
{
addEdge( projectMap, vertexMap, project, projectVertex, plugin.getGroupId(),
plugin.getArtifactId(), plugin.getVersion(), false, true );
for ( Dependency dependency : plugin.getDependencies() )
{
addEdge( projectMap, vertexMap, project, projectVertex, dependency.getGroupId(),
dependency.getArtifactId(), dependency.getVersion(), false, true );
}
}
for ( Extension extension : project.getBuildExtensions() )
{
addEdge( projectMap, vertexMap, project, projectVertex, extension.getGroupId(),
extension.getArtifactId(), extension.getVersion(), false, true );
}
}
List<String> sortedProjectLabels = TopologicalSorter.sort( dag );
this.sortedProjects = sortedProjectLabels.stream().map( id -> projectMap.get( id ) )
.collect( Collectors.collectingAndThen( Collectors.toList(), Collections::unmodifiableList ) );
}
@SuppressWarnings( "checkstyle:parameternumber" )
private void addEdge( Map<String, MavenProject> projectMap, Map<String, Map<String, Vertex>> vertexMap,
MavenProject project, Vertex projectVertex, String groupId, String artifactId,
String version, boolean force, boolean safe )
throws CycleDetectedException
{
String projectKey = ArtifactUtils.versionlessKey( groupId, artifactId );
Map<String, Vertex> vertices = vertexMap.get( projectKey );
if ( vertices != null )
{
if ( isSpecificVersion( version ) )
{
Vertex vertex = vertices.get( version );
if ( vertex != null )
{
addEdge( projectVertex, vertex, project, projectMap, force, safe );
}
}
else
{
for ( Vertex vertex : vertices.values() )
{
addEdge( projectVertex, vertex, project, projectMap, force, safe );
}
}
}
}
private void addEdge( Vertex fromVertex, Vertex toVertex, MavenProject fromProject,
Map<String, MavenProject> projectMap, boolean force, boolean safe )
throws CycleDetectedException
{
if ( fromVertex.equals( toVertex ) )
{
return;
}
if ( fromProject != null )
{
MavenProject toProject = projectMap.get( toVertex.getLabel() );
fromProject.addProjectReference( toProject );
}
if ( force && toVertex.getChildren().contains( fromVertex ) )
{
dag.removeEdge( toVertex, fromVertex );
}
try
{
dag.addEdge( fromVertex, toVertex );
}
catch ( CycleDetectedException e )
{
if ( !safe )
{
throw e;
}
}
}
private boolean isSpecificVersion( String version )
{
return !( StringUtils.isEmpty( version ) || version.startsWith( "[" ) || version.startsWith( "(" ) );
}
// TODO !![jc; 28-jul-2005] check this; if we're using '-r' and there are aggregator tasks, this will result in weirdness.
public MavenProject getTopLevelProject()
{
return sortedProjects.stream().filter( MavenProject::isExecutionRoot ).findFirst()
.orElse( null );
}
public List<MavenProject> getSortedProjects()
{
return sortedProjects;
}
public boolean hasMultipleProjects()
{
return sortedProjects.size() > 1;
}
public List<String> getDependents( String id )
{
return dag.getParentLabels( id );
}
public List<String> getDependencies( String id )
{
return dag.getChildLabels( id );
}
public static String getId( MavenProject project )
{
return ArtifactUtils.key( project.getGroupId(), project.getArtifactId(), project.getVersion() );
}
public DAG getDAG()
{
return dag;
}
public Map<String, MavenProject> getProjectMap()
{
return projectMap;
}
}