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.repository.ArtifactRepository; 030import org.apache.maven.artifact.resolver.filter.ArtifactFilter; 031import org.apache.maven.artifact.versioning.ArtifactVersion; 032import org.apache.maven.artifact.versioning.DefaultArtifactVersion; 033import org.apache.maven.artifact.versioning.OverConstrainedVersionException; 034import org.apache.maven.enforcer.rule.api.EnforcerRuleException; 035import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper; 036import org.apache.maven.execution.MavenSession; 037import org.apache.maven.plugin.logging.Log; 038import org.apache.maven.project.DefaultProjectBuildingRequest; 039import org.apache.maven.project.MavenProject; 040import org.apache.maven.project.ProjectBuildingRequest; 041import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder; 042import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilderException; 043import org.apache.maven.shared.dependency.graph.DependencyNode; 044import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor; 045import org.apache.maven.shared.utils.logging.MessageUtils; 046import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException; 047import org.codehaus.plexus.component.repository.exception.ComponentLookupException; 048 049/** 050 * Rule to enforce that the resolved dependency is also the most recent one of all transitive dependencies. 051 * 052 * @author Geoffrey De Smet 053 * @since 1.1 054 */ 055public class RequireUpperBoundDeps 056 extends AbstractNonCacheableEnforcerRule 057{ 058 private static Log log; 059 060 /** 061 * @since 1.3 062 */ 063 private boolean uniqueVersions; 064 065 /** 066 * Dependencies to ignore. 067 * 068 * @since TBD 069 */ 070 private List<String> excludes = null; 071 072 /** 073 * Dependencies to include. 074 * 075 * @since 3.0.0 076 */ 077 private List<String> includes = null; 078 079 /** 080 * Set to {@code true} if timestamped snapshots should be used. 081 * 082 * @param uniqueVersions 083 * @since 1.3 084 */ 085 public void setUniqueVersions( boolean uniqueVersions ) 086 { 087 this.uniqueVersions = uniqueVersions; 088 } 089 090 /** 091 * Sets dependencies to exclude. 092 * @param excludes a list of {@code groupId:artifactId} names 093 */ 094 public void setExcludes( List<String> excludes ) 095 { 096 this.excludes = excludes; 097 } 098 099 /** 100 * Sets dependencies to include. 101 * 102 * @param includes a list of {@code groupId:artifactId} names 103 */ 104 public void setIncludes( List<String> includes ) 105 { 106 this.includes = includes; 107 } 108 109 // CHECKSTYLE_OFF: LineLength 110 /** 111 * Uses the {@link EnforcerRuleHelper} to populate the values of the 112 * {@link DependencyCollectorBuilder#collectDependencyGraph(ProjectBuildingRequest, ArtifactFilter)} 113 * factory method. <br/> 114 * This method simply exists to hide all the ugly lookup that the {@link EnforcerRuleHelper} has to do. 115 * 116 * @param helper 117 * @return a Dependency Node which is the root of the project's dependency tree 118 * @throws EnforcerRuleException when the build should fail 119 */ 120 // CHECKSTYLE_ON: LineLength 121 private DependencyNode getNode( EnforcerRuleHelper helper ) 122 throws EnforcerRuleException 123 { 124 try 125 { 126 MavenProject project = (MavenProject) helper.evaluate( "${project}" ); 127 MavenSession session = (MavenSession) helper.evaluate( "${session}" ); 128 DependencyCollectorBuilder dependencyCollectorBuilder = 129 helper.getComponent( DependencyCollectorBuilder.class ); 130 ArtifactRepository repository = (ArtifactRepository) helper.evaluate( "${localRepository}" ); 131 132 ProjectBuildingRequest buildingRequest = 133 new DefaultProjectBuildingRequest( session.getProjectBuildingRequest() ); 134 buildingRequest.setProject( project ); 135 buildingRequest.setLocalRepository( repository ); 136 ArtifactFilter filter = ( Artifact a ) -> ( "compile".equalsIgnoreCase( a.getScope () ) 137 || "runtime".equalsIgnoreCase( a.getScope () ) ) 138 && !a.isOptional(); 139 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}