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.execution.MavenSession;
40  import org.apache.maven.plugin.logging.Log;
41  import org.apache.maven.project.DefaultProjectBuildingRequest;
42  import org.apache.maven.project.MavenProject;
43  import org.apache.maven.project.ProjectBuildingRequest;
44  import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder;
45  import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilderException;
46  import org.apache.maven.shared.dependency.graph.DependencyNode;
47  import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor;
48  import org.apache.maven.shared.utils.logging.MessageUtils;
49  import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
50  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
51  
52  /**
53   * Rule to enforce that the resolved dependency is also the most recent one of all transitive dependencies.
54   *
55   * @author Geoffrey De Smet
56   * @since 1.1
57   */
58  public class RequireUpperBoundDeps
59      extends AbstractNonCacheableEnforcerRule
60  {
61      private static Log log;
62  
63      /**
64       * @since 1.3
65       */
66      private boolean uniqueVersions;
67  
68      /**
69       * Dependencies to ignore.
70       *
71       * @since TBD
72       */
73      private List<String> excludes = null;
74  
75      /**
76       * Dependencies to include.
77       *
78       * @since 3.0.0
79       */
80      private List<String> includes = null;
81  
82      /**
83       * Set to {@code true} if timestamped snapshots should be used.
84       *
85       * @param uniqueVersions
86       * @since 1.3
87       */
88      public void setUniqueVersions( boolean uniqueVersions )
89      {
90          this.uniqueVersions = uniqueVersions;
91      }
92  
93      /**
94       * Sets dependencies to exclude.
95       * @param excludes a list of {@code groupId:artifactId} names
96       */
97      public void setExcludes( List<String> excludes )
98      {
99          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 }