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