View Javadoc
1   package org.apache.maven.plugins.enforcer;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.LinkedHashMap;
25  import java.util.List;
26  import java.util.Map;
27  
28  import org.apache.maven.artifact.Artifact;
29  import org.apache.maven.artifact.factory.ArtifactFactory;
30  import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
31  import org.apache.maven.artifact.repository.ArtifactRepository;
32  import org.apache.maven.artifact.resolver.ArtifactCollector;
33  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
34  import org.apache.maven.artifact.versioning.ArtifactVersion;
35  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
36  import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
37  import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
38  import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
39  import org.apache.maven.plugin.logging.Log;
40  import org.apache.maven.project.MavenProject;
41  import org.apache.maven.shared.dependency.tree.DependencyNode;
42  import org.apache.maven.shared.dependency.tree.DependencyTreeBuilder;
43  import org.apache.maven.shared.dependency.tree.DependencyTreeBuilderException;
44  import org.apache.maven.shared.dependency.tree.traversal.DependencyNodeVisitor;
45  import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
46  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
47  
48  /**
49   * Rule to enforce that the resolved dependency is also the most recent one of all transitive dependencies.
50   * 
51   * @author Geoffrey De Smet
52   * @since 1.1
53   */
54  public class RequireUpperBoundDeps
55      extends AbstractNonCacheableEnforcerRule
56  {
57      private static Log log;
58  
59      /**
60       * @since 1.3
61       */
62      private boolean uniqueVersions;
63  
64      /**
65       * Dependencies to ignore.
66       *
67       * @since TBD
68       */
69      private List<String> excludes = null;
70  
71      /**
72       * Set to {@code true} if timestamped snapshots should be used.
73       * 
74       * @param uniqueVersions 
75       * @since 1.3
76       */
77      public void setUniqueVersions( boolean uniqueVersions )
78      {
79          this.uniqueVersions = uniqueVersions;
80      }
81  
82      /**
83       * Sets dependencies to exclude.
84       * @param excludes a list of {@code groupId:artifactId} names
85       */
86      public void setExcludes( List<String> excludes )
87      {
88          this.excludes = excludes;
89      }
90  
91      // CHECKSTYLE_OFF: LineLength
92      /**
93       * Uses the {@link EnforcerRuleHelper} to populate the values of the
94       * {@link DependencyTreeBuilder#buildDependencyTree(MavenProject, ArtifactRepository, ArtifactFactory, ArtifactMetadataSource, ArtifactFilter, ArtifactCollector)}
95       * factory method. <br/>
96       * This method simply exists to hide all the ugly lookup that the {@link EnforcerRuleHelper} has to do.
97       * 
98       * @param helper
99       * @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 }