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}