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.execution.MavenSession; 040import org.apache.maven.plugin.logging.Log; 041import org.apache.maven.project.DefaultProjectBuildingRequest; 042import org.apache.maven.project.MavenProject; 043import org.apache.maven.project.ProjectBuildingRequest; 044import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder; 045import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilderException; 046import org.apache.maven.shared.dependency.graph.DependencyNode; 047import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor; 048import org.apache.maven.shared.utils.logging.MessageUtils; 049import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException; 050import org.codehaus.plexus.component.repository.exception.ComponentLookupException; 051 052/** 053 * Rule to enforce that the resolved dependency is also the most recent one of all transitive dependencies. 054 * 055 * @author Geoffrey De Smet 056 * @since 1.1 057 */ 058public class RequireUpperBoundDeps 059 extends AbstractNonCacheableEnforcerRule 060{ 061 private static Log log; 062 063 /** 064 * @since 1.3 065 */ 066 private boolean uniqueVersions; 067 068 /** 069 * Dependencies to ignore. 070 * 071 * @since TBD 072 */ 073 private List<String> excludes = null; 074 075 /** 076 * Dependencies to include. 077 * 078 * @since 3.0.0 079 */ 080 private List<String> includes = null; 081 082 /** 083 * Set to {@code true} if timestamped snapshots should be used. 084 * 085 * @param uniqueVersions 086 * @since 1.3 087 */ 088 public void setUniqueVersions( boolean uniqueVersions ) 089 { 090 this.uniqueVersions = uniqueVersions; 091 } 092 093 /** 094 * Sets dependencies to exclude. 095 * @param excludes a list of {@code groupId:artifactId} names 096 */ 097 public void setExcludes( List<String> excludes ) 098 { 099 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}