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.repository.ArtifactRepository;
030import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
031import org.apache.maven.artifact.versioning.ArtifactVersion;
032import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
033import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
034import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
035import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
036import org.apache.maven.execution.MavenSession;
037import org.apache.maven.plugin.logging.Log;
038import org.apache.maven.project.DefaultProjectBuildingRequest;
039import org.apache.maven.project.MavenProject;
040import org.apache.maven.project.ProjectBuildingRequest;
041import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder;
042import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilderException;
043import org.apache.maven.shared.dependency.graph.DependencyNode;
044import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor;
045import org.apache.maven.shared.utils.logging.MessageUtils;
046import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
047import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
048
049/**
050 * Rule to enforce that the resolved dependency is also the most recent one of all transitive dependencies.
051 *
052 * @author Geoffrey De Smet
053 * @since 1.1
054 */
055public class RequireUpperBoundDeps
056    extends AbstractNonCacheableEnforcerRule
057{
058    private static Log log;
059
060    /**
061     * @since 1.3
062     */
063    private boolean uniqueVersions;
064
065    /**
066     * Dependencies to ignore.
067     *
068     * @since TBD
069     */
070    private List<String> excludes = null;
071
072    /**
073     * Dependencies to include.
074     *
075     * @since 3.0.0
076     */
077    private List<String> includes = null;
078
079    /**
080     * Set to {@code true} if timestamped snapshots should be used.
081     *
082     * @param uniqueVersions
083     * @since 1.3
084     */
085    public void setUniqueVersions( boolean uniqueVersions )
086    {
087        this.uniqueVersions = uniqueVersions;
088    }
089
090    /**
091     * Sets dependencies to exclude.
092     * @param excludes a list of {@code groupId:artifactId} names
093     */
094    public void setExcludes( List<String> excludes )
095    {
096        this.excludes = excludes;
097    }
098
099    /**
100     * Sets dependencies to include.
101     *
102     * @param includes a list of {@code groupId:artifactId} names
103     */
104    public void setIncludes( List<String> includes )
105    {
106        this.includes = includes;
107    }
108
109    // CHECKSTYLE_OFF: LineLength
110    /**
111     * Uses the {@link EnforcerRuleHelper} to populate the values of the
112     * {@link DependencyCollectorBuilder#collectDependencyGraph(ProjectBuildingRequest, ArtifactFilter)} 
113     * factory method. <br/>
114     * This method simply exists to hide all the ugly lookup that the {@link EnforcerRuleHelper} has to do.
115     *
116     * @param helper
117     * @return a Dependency Node which is the root of the project's dependency tree
118     * @throws EnforcerRuleException when the build should fail
119     */
120    // CHECKSTYLE_ON: LineLength
121    private DependencyNode getNode( EnforcerRuleHelper helper )
122        throws EnforcerRuleException
123    {
124        try
125        {
126            MavenProject project = (MavenProject) helper.evaluate( "${project}" );
127            MavenSession session = (MavenSession) helper.evaluate( "${session}" );
128            DependencyCollectorBuilder dependencyCollectorBuilder =
129                helper.getComponent( DependencyCollectorBuilder.class );
130            ArtifactRepository repository = (ArtifactRepository) helper.evaluate( "${localRepository}" );
131            
132            ProjectBuildingRequest buildingRequest =
133                new DefaultProjectBuildingRequest( session.getProjectBuildingRequest() );
134            buildingRequest.setProject( project );
135            buildingRequest.setLocalRepository( repository );
136            ArtifactFilter filter = ( Artifact a ) -> ( "compile".equalsIgnoreCase( a.getScope () )
137                    || "runtime".equalsIgnoreCase( a.getScope () ) )
138                    && !a.isOptional();
139            
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}