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  import org.codehaus.plexus.i18n.I18N;
48  
49  /**
50   * Rule to enforce that the resolved dependency is also the most recent one of all transitive dependencies.
51   * 
52   * @author Geoffrey De Smet
53   * @since 1.1
54   */
55  public class RequireUpperBoundDeps
56      extends AbstractNonCacheableEnforcerRule
57  {
58      private static Log log;
59  
60      private static I18N i18n;
61  
62      /**
63       * @since 1.3
64       */
65      private boolean uniqueVersions;
66  
67      /**
68       * Set to {@code true} if timestamped snapshots should be used.
69       * 
70       * @param uniqueVersions 
71       * @since 1.3
72       */
73      public void setUniqueVersions( boolean uniqueVersions )
74      {
75          this.uniqueVersions = uniqueVersions;
76      }
77  
78      // CHECKSTYLE_OFF: LineLength
79      /**
80       * Uses the {@link EnforcerRuleHelper} to populate the values of the
81       * {@link DependencyTreeBuilder#buildDependencyTree(MavenProject, ArtifactRepository, ArtifactFactory, ArtifactMetadataSource, ArtifactFilter, ArtifactCollector)}
82       * factory method. <br/>
83       * This method simply exists to hide all the ugly lookup that the {@link EnforcerRuleHelper} has to do.
84       * 
85       * @param helper
86       * @return a Dependency Node which is the root of the project's dependency tree
87       * @throws EnforcerRuleException when the build should fail
88       */
89      // CHECKSTYLE_ON: LineLength
90      private DependencyNode getNode( EnforcerRuleHelper helper )
91          throws EnforcerRuleException
92      {
93          try
94          {
95              MavenProject project = (MavenProject) helper.evaluate( "${project}" );
96              DependencyTreeBuilder dependencyTreeBuilder =
97                  (DependencyTreeBuilder) helper.getComponent( DependencyTreeBuilder.class );
98              ArtifactRepository repository = (ArtifactRepository) helper.evaluate( "${localRepository}" );
99              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 }