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