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.ArrayList;
023import java.util.Collections;
024import java.util.LinkedHashMap;
025import java.util.List;
026import java.util.Map;
027
028import org.apache.maven.artifact.Artifact;
029import org.apache.maven.artifact.factory.ArtifactFactory;
030import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
031import org.apache.maven.artifact.repository.ArtifactRepository;
032import org.apache.maven.artifact.resolver.ArtifactCollector;
033import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
034import org.apache.maven.artifact.versioning.ArtifactVersion;
035import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
036import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
037import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
038import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
039import org.apache.maven.execution.MavenSession;
040import org.apache.maven.plugin.logging.Log;
041import org.apache.maven.project.DefaultProjectBuildingRequest;
042import org.apache.maven.project.MavenProject;
043import org.apache.maven.project.ProjectBuildingRequest;
044import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder;
045import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilderException;
046import org.apache.maven.shared.dependency.graph.DependencyNode;
047import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor;
048import org.apache.maven.shared.utils.logging.MessageUtils;
049import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
050import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
051
052/**
053 * Rule to enforce that the resolved dependency is also the most recent one of all transitive dependencies.
054 *
055 * @author Geoffrey De Smet
056 * @since 1.1
057 */
058public class RequireUpperBoundDeps
059    extends AbstractNonCacheableEnforcerRule
060{
061    private static Log log;
062
063    /**
064     * @since 1.3
065     */
066    private boolean uniqueVersions;
067
068    /**
069     * Dependencies to ignore.
070     *
071     * @since TBD
072     */
073    private List<String> excludes = null;
074
075    /**
076     * Dependencies to include.
077     *
078     * @since 3.0.0
079     */
080    private List<String> includes = null;
081
082    /**
083     * Set to {@code true} if timestamped snapshots should be used.
084     *
085     * @param uniqueVersions
086     * @since 1.3
087     */
088    public void setUniqueVersions( boolean uniqueVersions )
089    {
090        this.uniqueVersions = uniqueVersions;
091    }
092
093    /**
094     * Sets dependencies to exclude.
095     * @param excludes a list of {@code groupId:artifactId} names
096     */
097    public void setExcludes( List<String> excludes )
098    {
099        this.excludes = excludes;
100    }
101
102    /**
103     * Sets dependencies to include.
104     *
105     * @param includes a list of {@code groupId:artifactId} names
106     */
107    public void setIncludes( List<String> includes )
108    {
109        this.includes = includes;
110    }
111
112    // CHECKSTYLE_OFF: LineLength
113    /**
114     * Uses the {@link EnforcerRuleHelper} to populate the values of the
115     * {@link DependencyTreeBuilder#buildDependencyTree(MavenProject, ArtifactRepository, ArtifactFactory, ArtifactMetadataSource, ArtifactFilter, ArtifactCollector)}
116     * factory method. <br/>
117     * This method simply exists to hide all the ugly lookup that the {@link EnforcerRuleHelper} has to do.
118     *
119     * @param helper
120     * @return a Dependency Node which is the root of the project's dependency tree
121     * @throws EnforcerRuleException when the build should fail
122     */
123    // CHECKSTYLE_ON: LineLength
124    private DependencyNode getNode( EnforcerRuleHelper helper )
125        throws EnforcerRuleException
126    {
127        try
128        {
129            MavenProject project = (MavenProject) helper.evaluate( "${project}" );
130            MavenSession session = (MavenSession) helper.evaluate( "${session}" );
131            DependencyCollectorBuilder dependencyCollectorBuilder =
132                helper.getComponent( DependencyCollectorBuilder.class );
133            ArtifactRepository repository = (ArtifactRepository) helper.evaluate( "${localRepository}" );
134            
135            ProjectBuildingRequest buildingRequest =
136                new DefaultProjectBuildingRequest( session.getProjectBuildingRequest() );
137            buildingRequest.setProject( project );
138            buildingRequest.setLocalRepository( repository );
139            ArtifactFilter filter = null; // we need to evaluate all scopes
140            return dependencyCollectorBuilder.collectDependencyGraph( buildingRequest, filter );
141        }
142        catch ( ExpressionEvaluationException e )
143        {
144            throw new EnforcerRuleException( "Unable to lookup an expression " + e.getLocalizedMessage(), e );
145        }
146        catch ( ComponentLookupException e )
147        {
148            throw new EnforcerRuleException( "Unable to lookup a component " + e.getLocalizedMessage(), e );
149        }
150        catch ( DependencyCollectorBuilderException e )
151        {
152            throw new EnforcerRuleException( "Could not build dependency tree " + e.getLocalizedMessage(), e );
153        }
154    }
155
156    @Override
157    public void execute( EnforcerRuleHelper helper )
158        throws EnforcerRuleException
159    {
160        if ( log == null )
161        {
162            log = helper.getLog();
163        }
164        DependencyNode node = getNode( helper );
165        RequireUpperBoundDepsVisitor visitor = new RequireUpperBoundDepsVisitor();
166        visitor.setUniqueVersions( uniqueVersions );
167        visitor.setIncludes( includes );
168        node.accept( visitor );
169        List<String> errorMessages = buildErrorMessages( visitor.getConflicts() );
170        if ( errorMessages.size() > 0 )
171        {
172            throw new EnforcerRuleException( "Failed while enforcing RequireUpperBoundDeps. The error(s) are "
173                + errorMessages );
174        }
175    }
176
177    private List<String> buildErrorMessages( List<List<DependencyNode>> conflicts )
178    {
179        List<String> errorMessages = new ArrayList<>( conflicts.size() );
180        for ( List<DependencyNode> conflict : conflicts )
181        {
182            Artifact artifact = conflict.get( 0 ).getArtifact();
183            String groupArt = artifact.getGroupId() + ":" + artifact.getArtifactId();
184            if ( excludes != null && excludes.contains( groupArt ) )
185            {
186                log.info( "Ignoring requireUpperBoundDeps in " + groupArt );
187            }
188            else
189            {
190                errorMessages.add( buildErrorMessage( conflict ) );
191            }
192        }
193        return errorMessages;
194    }
195
196    private String buildErrorMessage( List<DependencyNode> conflict )
197    {
198        StringBuilder errorMessage = new StringBuilder();
199        errorMessage.append(
200                System.lineSeparator() + "Require upper bound dependencies error for " + getFullArtifactName(
201                        conflict.get( 0 ), false ) + " paths to dependency are:" + System.lineSeparator() );
202        if ( conflict.size() > 0 )
203        {
204            errorMessage.append( buildTreeString( conflict.get( 0 ) ) );
205        }
206        for ( DependencyNode node : conflict.subList( 1, conflict.size() ) )
207        {
208            errorMessage.append( "and" + System.lineSeparator() );
209            errorMessage.append( buildTreeString( node ) );
210        }
211        return errorMessage.toString();
212    }
213
214    private StringBuilder buildTreeString( DependencyNode node )
215    {
216        List<String> loc = new ArrayList<>();
217        DependencyNode currentNode = node;
218        while ( currentNode != null )
219        {
220            StringBuilder line = new StringBuilder( getFullArtifactName( currentNode, false ) );
221
222            if ( currentNode.getPremanagedVersion() != null )
223            {
224                line.append( " (managed) <-- " );
225                line.append( getFullArtifactName( currentNode, true ) );
226            }
227
228            loc.add( line.toString() );
229            currentNode = currentNode.getParent();
230        }
231        Collections.reverse( loc );
232        StringBuilder builder = new StringBuilder();
233        for ( int i = 0; i < loc.size(); i++ )
234        {
235            for ( int j = 0; j < i; j++ )
236            {
237                builder.append( "  " );
238            }
239            builder.append( "+-" ).append( loc.get( i ) );
240            builder.append( System.lineSeparator() );
241        }
242        return builder;
243    }
244
245    private String getFullArtifactName( DependencyNode node, boolean usePremanaged )
246    {
247        Artifact artifact = node.getArtifact();
248
249        String version = node.getPremanagedVersion();
250        if ( !usePremanaged || version == null )
251        {
252            version = uniqueVersions ? artifact.getVersion() : artifact.getBaseVersion();
253        }
254        String result = artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + version;
255
256        String classifier = artifact.getClassifier();
257        if ( classifier != null && !classifier.isEmpty() )
258        {
259            result += ":" + classifier;
260        }
261  
262        String scope = artifact.getScope();      
263        if ( "compile".equals( scope ) )
264        {
265            result = MessageUtils.buffer().strong( result ).toString();
266        }
267        else if ( scope != null )
268        {
269            result += " [" + scope + ']';
270        }
271        
272        return result;
273    }
274
275    private static class RequireUpperBoundDepsVisitor
276        implements DependencyNodeVisitor
277    {
278
279        private boolean uniqueVersions;
280
281        private List<String> includes = null;
282
283        public void setUniqueVersions( boolean uniqueVersions )
284        {
285            this.uniqueVersions = uniqueVersions;
286        }
287
288        public void setIncludes( List<String> includes )
289        {
290            this.includes = includes;
291        }
292
293        private Map<String, List<DependencyNodeHopCountPair>> keyToPairsMap = new LinkedHashMap<>();
294
295        public boolean visit( DependencyNode node )
296        {
297            DependencyNodeHopCountPair pair = new DependencyNodeHopCountPair( node );
298            String key = pair.constructKey();
299
300            if ( includes != null && !includes.isEmpty() && !includes.contains( key ) )
301            {
302                return true;
303            }
304
305            List<DependencyNodeHopCountPair> pairs = keyToPairsMap.get( key );
306            if ( pairs == null )
307            {
308                pairs = new ArrayList<>();
309                keyToPairsMap.put( key, pairs );
310            }
311            pairs.add( pair );
312            Collections.sort( pairs );
313            return true;
314        }
315
316        public boolean endVisit( DependencyNode node )
317        {
318            return true;
319        }
320
321        public List<List<DependencyNode>> getConflicts()
322        {
323            List<List<DependencyNode>> output = new ArrayList<>();
324            for ( List<DependencyNodeHopCountPair> pairs : keyToPairsMap.values() )
325            {
326                if ( containsConflicts( pairs ) )
327                {
328                    List<DependencyNode> outputSubList = new ArrayList<>( pairs.size() );
329                    for ( DependencyNodeHopCountPair pair : pairs )
330                    {
331                        outputSubList.add( pair.getNode() );
332                    }
333                    output.add( outputSubList );
334                }
335            }
336            return output;
337        }
338
339        private boolean containsConflicts( List<DependencyNodeHopCountPair> pairs )
340        {
341            DependencyNodeHopCountPair resolvedPair = pairs.get( 0 );
342
343            // search for artifact with lowest hopCount
344            for ( DependencyNodeHopCountPair hopPair : pairs.subList( 1, pairs.size() ) )
345            {
346                if ( hopPair.getHopCount() < resolvedPair.getHopCount() )
347                {
348                    resolvedPair = hopPair;
349                }
350            }
351
352            ArtifactVersion resolvedVersion = resolvedPair.extractArtifactVersion( uniqueVersions, false );
353
354            for ( DependencyNodeHopCountPair pair : pairs )
355            {
356                ArtifactVersion version = pair.extractArtifactVersion( uniqueVersions, true );
357                if ( resolvedVersion.compareTo( version ) < 0 )
358                {
359                    return true;
360                }
361            }
362            return false;
363        }
364
365    }
366
367    private static class DependencyNodeHopCountPair
368        implements Comparable<DependencyNodeHopCountPair>
369    {
370
371        private DependencyNode node;
372
373        private int hopCount;
374
375        private DependencyNodeHopCountPair( DependencyNode node )
376        {
377            this.node = node;
378            countHops();
379        }
380
381        private void countHops()
382        {
383            hopCount = 0;
384            DependencyNode parent = node.getParent();
385            while ( parent != null )
386            {
387                hopCount++;
388                parent = parent.getParent();
389            }
390        }
391
392        private String constructKey()
393        {
394            Artifact artifact = node.getArtifact();
395            return artifact.getGroupId() + ":" + artifact.getArtifactId();
396        }
397
398        public DependencyNode getNode()
399        {
400            return node;
401        }
402
403        private ArtifactVersion extractArtifactVersion( boolean uniqueVersions, boolean usePremanagedVersion )
404        {
405            if ( usePremanagedVersion && node.getPremanagedVersion() != null )
406            {
407                return new DefaultArtifactVersion( node.getPremanagedVersion() );
408            }
409
410            Artifact artifact = node.getArtifact();
411            String version = uniqueVersions ? artifact.getVersion() : artifact.getBaseVersion();
412            if ( version != null )
413            {
414                return new DefaultArtifactVersion( version );
415            }
416            try
417            {
418                return artifact.getSelectedVersion();
419            }
420            catch ( OverConstrainedVersionException e )
421            {
422                throw new RuntimeException( "Version ranges problem with " + node.getArtifact(), e );
423            }
424        }
425
426        public int getHopCount()
427        {
428            return hopCount;
429        }
430
431        public int compareTo( DependencyNodeHopCountPair other )
432        {
433            return Integer.valueOf( hopCount ).compareTo( Integer.valueOf( other.getHopCount() ) );
434        }
435    }
436
437}