blob: 458554ac8be6a5e7602a84a31ec310c70bf2a8cc [file] [log] [blame]
package org.apache.maven.plugins.enforcer;
/*
* 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.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactCollector;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.dependency.tree.DependencyNode;
import org.apache.maven.shared.dependency.tree.DependencyTreeBuilder;
import org.apache.maven.shared.dependency.tree.DependencyTreeBuilderException;
import org.apache.maven.shared.dependency.tree.traversal.DependencyNodeVisitor;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
/**
* Rule to enforce that the resolved dependency is also the most recent one of all transitive dependencies.
*
* @author Geoffrey De Smet
* @since 1.1
*/
public class RequireUpperBoundDeps
extends AbstractNonCacheableEnforcerRule
{
private static Log log;
/**
* @since 1.3
*/
private boolean uniqueVersions;
/**
* Dependencies to ignore.
*
* @since TBD
*/
private List<String> excludes = null;
/**
* Set to {@code true} if timestamped snapshots should be used.
*
* @param uniqueVersions
* @since 1.3
*/
public void setUniqueVersions( boolean uniqueVersions )
{
this.uniqueVersions = uniqueVersions;
}
/**
* Sets dependencies to exclude.
* @param excludes a list of {@code groupId:artifactId} names
*/
public void setExcludes( List<String> excludes )
{
this.excludes = excludes;
}
// CHECKSTYLE_OFF: LineLength
/**
* Uses the {@link EnforcerRuleHelper} to populate the values of the
* {@link DependencyTreeBuilder#buildDependencyTree(MavenProject, ArtifactRepository, ArtifactFactory, ArtifactMetadataSource, ArtifactFilter, ArtifactCollector)}
* factory method. <br/>
* This method simply exists to hide all the ugly lookup that the {@link EnforcerRuleHelper} has to do.
*
* @param helper
* @return a Dependency Node which is the root of the project's dependency tree
* @throws EnforcerRuleException when the build should fail
*/
// CHECKSTYLE_ON: LineLength
private DependencyNode getNode( EnforcerRuleHelper helper )
throws EnforcerRuleException
{
try
{
MavenProject project = (MavenProject) helper.evaluate( "${project}" );
DependencyTreeBuilder dependencyTreeBuilder =
(DependencyTreeBuilder) helper.getComponent( DependencyTreeBuilder.class );
ArtifactRepository repository = (ArtifactRepository) helper.evaluate( "${localRepository}" );
ArtifactFactory factory = (ArtifactFactory) helper.getComponent( ArtifactFactory.class );
ArtifactMetadataSource metadataSource =
(ArtifactMetadataSource) helper.getComponent( ArtifactMetadataSource.class );
ArtifactCollector collector = (ArtifactCollector) helper.getComponent( ArtifactCollector.class );
ArtifactFilter filter = null; // we need to evaluate all scopes
DependencyNode node =
dependencyTreeBuilder.buildDependencyTree( project, repository, factory, metadataSource, filter,
collector );
return node;
}
catch ( ExpressionEvaluationException e )
{
throw new EnforcerRuleException( "Unable to lookup an expression " + e.getLocalizedMessage(), e );
}
catch ( ComponentLookupException e )
{
throw new EnforcerRuleException( "Unable to lookup a component " + e.getLocalizedMessage(), e );
}
catch ( DependencyTreeBuilderException e )
{
throw new EnforcerRuleException( "Could not build dependency tree " + e.getLocalizedMessage(), e );
}
}
@Override
public void execute( EnforcerRuleHelper helper )
throws EnforcerRuleException
{
if ( log == null )
{
log = helper.getLog();
}
try
{
DependencyNode node = getNode( helper );
RequireUpperBoundDepsVisitor visitor = new RequireUpperBoundDepsVisitor();
visitor.setUniqueVersions( uniqueVersions );
node.accept( visitor );
List<String> errorMessages = buildErrorMessages( visitor.getConflicts() );
if ( errorMessages.size() > 0 )
{
throw new EnforcerRuleException( "Failed while enforcing RequireUpperBoundDeps. The error(s) are "
+ errorMessages );
}
}
catch ( Exception e )
{
throw new EnforcerRuleException( e.getLocalizedMessage(), e );
}
}
private List<String> buildErrorMessages( List<List<DependencyNode>> conflicts )
{
List<String> errorMessages = new ArrayList<String>( conflicts.size() );
for ( List<DependencyNode> conflict : conflicts )
{
Artifact artifact = conflict.get( 0 ).getArtifact();
String groupArt = artifact.getGroupId() + ":" + artifact.getArtifactId();
if ( excludes != null && excludes.contains( groupArt ) )
{
log.info( "Ignoring requireUpperBoundDeps in " + groupArt );
}
else
{
errorMessages.add( buildErrorMessage( conflict ) );
}
}
return errorMessages;
}
private String buildErrorMessage( List<DependencyNode> conflict )
{
StringBuilder errorMessage = new StringBuilder();
errorMessage.append( System.lineSeparator() + "Require upper bound dependencies error for "
+ getFullArtifactName( conflict.get( 0 ), false ) + " paths to dependency are:" + System.lineSeparator() );
if ( conflict.size() > 0 )
{
errorMessage.append( buildTreeString( conflict.get( 0 ) ) );
}
for ( DependencyNode node : conflict.subList( 1, conflict.size() ) )
{
errorMessage.append( "and" + System.lineSeparator() );
errorMessage.append( buildTreeString( node ) );
}
return errorMessage.toString();
}
private StringBuilder buildTreeString( DependencyNode node )
{
List<String> loc = new ArrayList<String>();
DependencyNode currentNode = node;
while ( currentNode != null )
{
StringBuilder line = new StringBuilder( getFullArtifactName( currentNode, false ) );
if ( currentNode.getPremanagedVersion() != null )
{
line.append( " (managed) <-- " );
line.append( getFullArtifactName( currentNode, true ) );
}
loc.add( line.toString() );
currentNode = currentNode.getParent();
}
Collections.reverse( loc );
StringBuilder builder = new StringBuilder();
for ( int i = 0; i < loc.size(); i++ )
{
for ( int j = 0; j < i; j++ )
{
builder.append( " " );
}
builder.append( "+-" ).append( loc.get( i ) );
builder.append( System.lineSeparator() );
}
return builder;
}
private String getFullArtifactName( DependencyNode node, boolean usePremanaged )
{
Artifact artifact = node.getArtifact();
String version = node.getPremanagedVersion();
if ( !usePremanaged || version == null )
{
version = uniqueVersions ? artifact.getVersion() : artifact.getBaseVersion();
}
return artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + version;
}
private static class RequireUpperBoundDepsVisitor
implements DependencyNodeVisitor
{
private boolean uniqueVersions;
public void setUniqueVersions( boolean uniqueVersions )
{
this.uniqueVersions = uniqueVersions;
}
private Map<String, List<DependencyNodeHopCountPair>> keyToPairsMap =
new LinkedHashMap<String, List<DependencyNodeHopCountPair>>();
public boolean visit( DependencyNode node )
{
DependencyNodeHopCountPair pair = new DependencyNodeHopCountPair( node );
String key = pair.constructKey();
List<DependencyNodeHopCountPair> pairs = keyToPairsMap.get( key );
if ( pairs == null )
{
pairs = new ArrayList<DependencyNodeHopCountPair>();
keyToPairsMap.put( key, pairs );
}
pairs.add( pair );
Collections.sort( pairs );
return true;
}
public boolean endVisit( DependencyNode node )
{
return true;
}
public List<List<DependencyNode>> getConflicts()
{
List<List<DependencyNode>> output = new ArrayList<List<DependencyNode>>();
for ( List<DependencyNodeHopCountPair> pairs : keyToPairsMap.values() )
{
if ( containsConflicts( pairs ) )
{
List<DependencyNode> outputSubList = new ArrayList<DependencyNode>( pairs.size() );
for ( DependencyNodeHopCountPair pair : pairs )
{
outputSubList.add( pair.getNode() );
}
output.add( outputSubList );
}
}
return output;
}
@SuppressWarnings( "unchecked" )
private boolean containsConflicts( List<DependencyNodeHopCountPair> pairs )
{
DependencyNodeHopCountPair resolvedPair = pairs.get( 0 );
// search for artifact with lowest hopCount
for ( DependencyNodeHopCountPair hopPair : pairs.subList( 1, pairs.size() ) )
{
if ( hopPair.getHopCount() < resolvedPair.getHopCount() )
{
resolvedPair = hopPair;
}
}
ArtifactVersion resolvedVersion = resolvedPair.extractArtifactVersion( uniqueVersions, false );
for ( DependencyNodeHopCountPair pair : pairs )
{
ArtifactVersion version = pair.extractArtifactVersion( uniqueVersions, true );
if ( resolvedVersion.compareTo( version ) < 0 )
{
return true;
}
}
return false;
}
}
private static class DependencyNodeHopCountPair
implements Comparable<DependencyNodeHopCountPair>
{
private DependencyNode node;
private int hopCount;
private DependencyNodeHopCountPair( DependencyNode node )
{
this.node = node;
countHops();
}
private void countHops()
{
hopCount = 0;
DependencyNode parent = node.getParent();
while ( parent != null )
{
hopCount++;
parent = parent.getParent();
}
}
private String constructKey()
{
Artifact artifact = node.getArtifact();
return artifact.getGroupId() + ":" + artifact.getArtifactId();
}
public DependencyNode getNode()
{
return node;
}
private ArtifactVersion extractArtifactVersion( boolean uniqueVersions, boolean usePremanagedVersion )
{
if ( usePremanagedVersion && node.getPremanagedVersion() != null )
{
return new DefaultArtifactVersion( node.getPremanagedVersion() );
}
Artifact artifact = node.getArtifact();
String version = uniqueVersions ? artifact.getVersion() : artifact.getBaseVersion();
if ( version != null )
{
return new DefaultArtifactVersion( version );
}
try
{
return artifact.getSelectedVersion();
}
catch ( OverConstrainedVersionException e )
{
throw new RuntimeException( "Version ranges problem with " + node.getArtifact(), e );
}
}
public int getHopCount()
{
return hopCount;
}
public int compareTo( DependencyNodeHopCountPair other )
{
return Integer.valueOf( hopCount ).compareTo( Integer.valueOf( other.getHopCount() ) );
}
}
}