blob: c3a5cc07315b8df9cb9ca2feed13a0e143b3c970 [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.text.ChoiceFormat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.maven.RepositoryUtils;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
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.plugins.enforcer.utils.ArtifactMatcher;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.utils.logging.MessageBuilder;
import org.apache.maven.shared.utils.logging.MessageUtils;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.collection.CollectResult;
import org.eclipse.aether.collection.DependencyCollectionException;
import org.eclipse.aether.collection.DependencySelector;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.graph.DependencyVisitor;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.util.graph.selector.AndDependencySelector;
import org.eclipse.aether.util.graph.selector.OptionalDependencySelector;
import org.eclipse.aether.util.graph.selector.ScopeDependencySelector;
import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor;
import org.eclipse.aether.version.VersionConstraint;
/**
* This rule bans dependencies having a version which requires resolution (i.e. dynamic versions which might change with
* each build). Dynamic versions are either
* <ul>
* <li>version ranges,</li>
* <li>the special placeholders {@code LATEST} or {@code RELEASE} or</li>
* <li>versions ending with {@code -SNAPSHOT}.
* </ul>
*
* @since 3.2.0
*/
public class BanDynamicVersions
extends AbstractNonCacheableEnforcerRule
{
private static final String RELEASE = "RELEASE";
private static final String LATEST = "LATEST";
private static final String SNAPSHOT_SUFFIX = "-SNAPSHOT";
/**
* {@code true} if versions ending with {@code -SNAPSHOT} should be allowed
*/
private boolean allowSnapshots;
/**
* {@code true} if versions using {@code LATEST} should be allowed
*/
private boolean allowLatest;
/**
* {@code true} if versions using {@code RELEASE} should be allowed
*/
private boolean allowRelease;
/**
* {@code true} if version ranges should be allowed
*/
private boolean allowRanges;
/**
* {@code true} if ranges having the same upper and lower bound like {@code [1.0]} should be allowed.
* Only applicable if {@link #allowRanges} is not set to {@code true}.
*/
private boolean allowRangesWithIdenticalBounds;
/**
* {@code true} if optional dependencies should not be checked
*/
private boolean excludeOptionals;
/**
* the scopes of dependencies which should be excluded from this rule
*/
private String[] excludedScopes;
/**
* Specify the ignored dependencies. This can be a list of artifacts in the format
* <code>groupId[:artifactId[:version[:type[:scope:[classifier]]]]]</code>.
* Any of the sections can be a wildcard by using '*' (e.g. {@code group:*:1.0}).
* <br>
* Any of the ignored dependencies may have dynamic versions.
*
* @see {@link #setIgnores(List)}
*/
private List<String> ignores = null;
public void setIgnores( List<String> ignores )
{
this.ignores = ignores;
}
public void setAllowSnapshots( boolean allowSnapshots )
{
this.allowSnapshots = allowSnapshots;
}
public void setAllowLatest( boolean allowLatest )
{
this.allowLatest = allowLatest;
}
public void setAllowRelease( boolean allowRelease )
{
this.allowRelease = allowRelease;
}
public void setAllowRanges( boolean allowRanges )
{
this.allowRanges = allowRanges;
}
public void setExcludeOptionals( boolean excludeOptionals )
{
this.excludeOptionals = excludeOptionals;
}
public void setExcludedScopes( String[] excludedScopes )
{
this.excludedScopes = excludedScopes;
}
private final class BannedDynamicVersionCollector
implements DependencyVisitor
{
private final Log log;
private final Deque<DependencyNode> nodeStack; // all intermediate nodes (without the root node)
private boolean isRoot = true;
private int numViolations;
private final Predicate<DependencyNode> predicate;
public int getNumViolations()
{
return numViolations;
}
BannedDynamicVersionCollector( Log log, Predicate<DependencyNode> predicate )
{
this.log = log;
nodeStack = new ArrayDeque<>();
this.predicate = predicate;
this.isRoot = true;
numViolations = 0;
}
private boolean isBannedDynamicVersion( VersionConstraint versionConstraint )
{
if ( versionConstraint.getVersion() != null )
{
if ( versionConstraint.getVersion().toString().equals( LATEST ) )
{
return !allowLatest;
}
else if ( versionConstraint.getVersion().toString().equals( RELEASE ) )
{
return !allowRelease;
}
else if ( versionConstraint.getVersion().toString().endsWith( SNAPSHOT_SUFFIX ) )
{
return !allowSnapshots;
}
}
else if ( versionConstraint.getRange() != null )
{
if ( allowRangesWithIdenticalBounds
&& Objects.equals( versionConstraint.getRange().getLowerBound(),
versionConstraint.getRange().getUpperBound() ) )
{
return false;
}
return !allowRanges;
}
else
{
log.warn( "Unexpected version constraint found: " + versionConstraint );
}
return false;
}
@Override
public boolean visitEnter( DependencyNode node )
{
if ( isRoot )
{
isRoot = false;
}
else
{
log.debug( "Found node " + node + " with version constraint " + node.getVersionConstraint() );
if ( predicate.test( node ) && isBannedDynamicVersion( node.getVersionConstraint() ) )
{
MessageBuilder msgBuilder = MessageUtils.buffer();
log.warn( msgBuilder.a( "Dependency " )
.strong( node.getDependency() )
.mojo( dumpIntermediatePath( nodeStack ) )
.a( " is referenced with a banned dynamic version " + node.getVersionConstraint() )
.toString() );
numViolations++;
return false;
}
nodeStack.addLast( node );
}
return true;
}
@Override
public boolean visitLeave( DependencyNode node )
{
if ( !nodeStack.isEmpty() )
{
nodeStack.removeLast();
}
return true;
}
}
@SuppressWarnings( "unchecked" )
@Override
public void execute( EnforcerRuleHelper helper )
throws EnforcerRuleException
{
MavenProject project;
DefaultRepositorySystemSession newRepoSession;
RepositorySystem repoSystem;
List<RemoteRepository> remoteRepositories;
try
{
project = (MavenProject) Objects.requireNonNull( helper.evaluate( "${project}" ), "${project} is null" );
RepositorySystemSession repoSession =
(RepositorySystemSession) Objects.requireNonNull( helper.evaluate( "${repositorySystemSession}" ),
"${repositorySystemSession} is null" );
// get a new session to be able to tweak the dependency selector
newRepoSession = new DefaultRepositorySystemSession( repoSession );
remoteRepositories = (List<RemoteRepository>) helper.evaluate( "${project.remoteProjectRepositories}" );
repoSystem = helper.getComponent( RepositorySystem.class );
}
catch ( ExpressionEvaluationException eee )
{
throw new EnforcerRuleException( "Cannot resolve expression", eee );
}
catch ( ComponentLookupException cle )
{
throw new EnforcerRuleException( "Unable to retrieve component RepositorySystem", cle );
}
Log log = helper.getLog();
Collection<DependencySelector> depSelectors = new ArrayList<>();
depSelectors.add( new ScopeDependencySelector( excludedScopes ) );
if ( excludeOptionals )
{
depSelectors.add( new OptionalDependencySelector() );
}
newRepoSession.setDependencySelector( new AndDependencySelector( depSelectors ) );
Dependency rootDependency = RepositoryUtils.toDependency( project.getArtifact(), null );
try
{
// use root dependency with unresolved direct dependencies
int numViolations = emitDependenciesWithBannedDynamicVersions( rootDependency, repoSystem, newRepoSession,
remoteRepositories, log );
if ( numViolations > 0 )
{
ChoiceFormat dependenciesFormat = new ChoiceFormat( "1#dependency|1<dependencies" );
throw new EnforcerRuleException( "Found " + numViolations + " "
+ dependenciesFormat.format( numViolations )
+ " with dynamic versions. Look at the warnings emitted above for the details." );
}
}
catch ( DependencyCollectionException e )
{
throw new EnforcerRuleException( "Could not retrieve dependency metadata for project",
e );
}
}
private static String dumpIntermediatePath( Collection<DependencyNode> path )
{
if ( path.isEmpty() )
{
return "";
}
return " via " + path.stream().map( n -> n.getArtifact().toString() ).collect( Collectors.joining( " -> " ) );
}
private static final class ExcludeArtifactPatternsPredicate
implements Predicate<DependencyNode>
{
private final ArtifactMatcher artifactMatcher;
ExcludeArtifactPatternsPredicate( List<String> excludes )
{
this.artifactMatcher = new ArtifactMatcher( excludes, Collections.emptyList() );
}
@Override
public boolean test( DependencyNode depNode )
{
try
{
return artifactMatcher.match( RepositoryUtils.toArtifact( depNode.getArtifact() ) );
}
catch ( InvalidVersionSpecificationException e )
{
throw new IllegalArgumentException( "Invalid version found for dependency node " + depNode, e );
}
}
}
protected int emitDependenciesWithBannedDynamicVersions( org.eclipse.aether.graph.Dependency rootDependency,
RepositorySystem repoSystem,
RepositorySystemSession repoSession,
List<RemoteRepository> remoteRepositories, Log log )
throws DependencyCollectionException
{
CollectRequest collectRequest = new CollectRequest( rootDependency, remoteRepositories );
CollectResult collectResult = repoSystem.collectDependencies( repoSession, collectRequest );
Predicate<DependencyNode> predicate;
if ( ignores != null && !ignores.isEmpty() )
{
predicate = new ExcludeArtifactPatternsPredicate( ignores );
}
else
{
predicate = d -> true;
}
BannedDynamicVersionCollector bannedDynamicVersionCollector =
new BannedDynamicVersionCollector( log, predicate );
DependencyVisitor depVisitor = new TreeDependencyVisitor( bannedDynamicVersionCollector );
collectResult.getRoot().accept( depVisitor );
return bannedDynamicVersionCollector.getNumViolations();
}
}