001package org.apache.maven.plugins.enforcer;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *  http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.util.Collections;
023import java.util.List;
024
025import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
026import org.apache.maven.enforcer.rule.api.EnforcerRule;
027import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
028import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
029import org.apache.maven.execution.MavenSession;
030import org.apache.maven.plugins.enforcer.utils.ArtifactMatcher;
031import org.apache.maven.project.DefaultProjectBuildingRequest;
032import org.apache.maven.project.MavenProject;
033import org.apache.maven.project.ProjectBuildingRequest;
034import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
035import org.apache.maven.shared.dependency.graph.DependencyNode;
036import org.apache.maven.shared.dependency.graph.internal.DefaultDependencyGraphBuilder;
037import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
038import org.codehaus.plexus.logging.console.ConsoleLogger;
039
040/**
041 * This rule bans all transitive dependencies. There is a configuration option to exclude certain artifacts from being
042 * checked.
043 * 
044 * @author Jakub Senko
045 */
046public class BanTransitiveDependencies
047    extends AbstractNonCacheableEnforcerRule
048    implements EnforcerRule
049{
050
051    private EnforcerRuleHelper helper;
052
053    /**
054     * Specify the dependencies that will be ignored. This can be a list of artifacts in the format
055     * <code>groupId[:artifactId][:version][:type][:scope]</code>. Wildcard '*' can be used to in place of specific
056     * section (ie group:*:1.0 will match both 'group:artifact:1.0' and 'group:anotherArtifact:1.0') <br>
057     * You can override this patterns by using includes. Version is a string representing standard maven version range.
058     * Empty patterns will be ignored.
059     */
060    private List<String> excludes;
061
062    /**
063     * Specify the dependencies that will be checked. These are exceptions to excludes intended for more convenient and
064     * finer settings. This can be a list of artifacts in the format
065     * <code>groupId[:artifactId][:version][:type][:scope]</code>. Wildcard '*' can be used to in place of specific
066     * section (ie group:*:1.0 will match both 'group:artifact:1.0' and 'group:anotherArtifact:1.0') <br>
067     * Version is a string representing standard maven version range. Empty patterns will be ignored.
068     */
069    private List<String> includes;
070
071    /**
072     * Searches dependency tree recursively for transitive dependencies that are not excluded, while generating nice
073     * info message along the way.
074     * 
075     * @throws InvalidVersionSpecificationException
076     */
077    private static boolean searchTree( DependencyNode node, int level, ArtifactMatcher excludes, StringBuilder message )
078        throws InvalidVersionSpecificationException
079    {
080
081        List<DependencyNode> children = node.getChildren();
082
083        /*
084         * if the node is deeper than direct dependency and is empty, it is transitive.
085         */
086        boolean hasTransitiveDependencies = level > 1;
087
088        boolean excluded = false;
089
090        /*
091         * holds recursive message from children, will be appended to current message if this node has any transitive
092         * descendants if message is null, don't generate recursive message.
093         */
094        StringBuilder messageFromChildren = message == null ? null : new StringBuilder();
095
096        if ( excludes.match( node.getArtifact() ) )
097        {
098            // is excluded, we don't care about descendants
099            excluded = true;
100            hasTransitiveDependencies = false;
101        }
102        else
103        {
104            for ( DependencyNode childNode : children )
105            {
106                /*
107                 * if any of the children has transitive d. so does the parent
108                 */
109                hasTransitiveDependencies =
110                    ( searchTree( childNode, level + 1, excludes, messageFromChildren ) || hasTransitiveDependencies );
111            }
112        }
113
114        if ( ( excluded || hasTransitiveDependencies ) && message != null ) // then generate message
115        {
116            for ( int i = 0; i < level; i++ )
117            {
118                message.append( "   " );
119            }
120
121            message.append( node.getArtifact() );
122
123            if ( excluded )
124            {
125                message.append( " [excluded]" + System.lineSeparator() );
126            }
127
128            if ( hasTransitiveDependencies )
129            {
130                if ( level == 1 )
131                {
132                    message.append( " has transitive dependencies:" );
133                }
134
135                message.append( System.lineSeparator() ).append( messageFromChildren );
136            }
137        }
138
139        return hasTransitiveDependencies;
140    }
141
142    @Override
143    public void execute( EnforcerRuleHelper helper )
144        throws EnforcerRuleException
145    {
146        this.helper = helper;
147
148        if ( excludes == null )
149        {
150            excludes = Collections.emptyList();
151        }
152        if ( includes == null )
153        {
154            includes = Collections.emptyList();
155        }
156
157        final ArtifactMatcher exclusions = new ArtifactMatcher( excludes, includes );
158
159        DependencyNode rootNode = null;
160
161        try
162        {
163            MavenProject project = (MavenProject) helper.evaluate( "${project}" );
164            MavenSession session = (MavenSession) helper.evaluate( "${session}" );
165            
166            ProjectBuildingRequest buildingRequest =
167                new DefaultProjectBuildingRequest( session.getProjectBuildingRequest() );
168            buildingRequest.setProject( project );
169            
170            rootNode = createDependencyGraphBuilder().buildDependencyGraph( buildingRequest, null );
171        }
172        catch ( Exception e )
173        {
174            throw new EnforcerRuleException( "Error: Could not construct dependency tree.", e );
175        }
176
177        String message = getMessage();
178        StringBuilder generatedMessage = null;
179        if ( message == null )
180        {
181            generatedMessage = new StringBuilder();
182        }
183
184        try
185        {
186            if ( searchTree( rootNode, 0, exclusions, generatedMessage ) )
187            {
188                throw new EnforcerRuleException( message == null ? generatedMessage.toString() : message );
189            }
190        }
191        catch ( InvalidVersionSpecificationException e )
192        {
193            throw new EnforcerRuleException( "Error: Invalid version range.", e );
194        }
195
196    }
197
198    private DependencyGraphBuilder createDependencyGraphBuilder()
199        throws ComponentLookupException
200    {
201        // CHECKSTYLE_OFF: LineLength
202        DefaultDependencyGraphBuilder builder =
203            (DefaultDependencyGraphBuilder) helper.getContainer().lookup( DependencyGraphBuilder.class.getCanonicalName(),
204                                                                          "default" );
205        // CHECKSTYLE_ON: LineLength
206
207        builder.enableLogging( new ConsoleLogger( ConsoleLogger.LEVEL_DISABLED, "DefaultDependencyGraphBuilder" ) );
208
209        return builder;
210    }
211
212}