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; 047import org.codehaus.plexus.i18n.I18N; 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 private static I18N i18n; 061 062 /** 063 * @since 1.3 064 */ 065 private boolean uniqueVersions; 066 067 /** 068 * Set to {@code true} if timestamped snapshots should be used. 069 * 070 * @param uniqueVersions 071 * @since 1.3 072 */ 073 public void setUniqueVersions( boolean uniqueVersions ) 074 { 075 this.uniqueVersions = uniqueVersions; 076 } 077 078 /** 079 * Uses the {@link EnforcerRuleHelper} to populate the values of the 080 * {@link DependencyTreeBuilder#buildDependencyTree(MavenProject, ArtifactRepository, ArtifactFactory, ArtifactMetadataSource, ArtifactFilter, ArtifactCollector)} 081 * factory method. <br/> 082 * This method simply exists to hide all the ugly lookup that the {@link EnforcerRuleHelper} has to do. 083 * 084 * @param helper 085 * @return a Dependency Node which is the root of the project's dependency tree 086 * @throws EnforcerRuleException when the build should fail 087 */ 088 private DependencyNode getNode( EnforcerRuleHelper helper ) 089 throws EnforcerRuleException 090 { 091 try 092 { 093 MavenProject project = (MavenProject) helper.evaluate( "${project}" ); 094 DependencyTreeBuilder dependencyTreeBuilder = 095 (DependencyTreeBuilder) helper.getComponent( DependencyTreeBuilder.class ); 096 ArtifactRepository repository = (ArtifactRepository) helper.evaluate( "${localRepository}" ); 097 ArtifactFactory factory = (ArtifactFactory) helper.getComponent( ArtifactFactory.class ); 098 ArtifactMetadataSource metadataSource = 099 (ArtifactMetadataSource) helper.getComponent( ArtifactMetadataSource.class ); 100 ArtifactCollector collector = (ArtifactCollector) helper.getComponent( ArtifactCollector.class ); 101 ArtifactFilter filter = null; // we need to evaluate all scopes 102 DependencyNode node = 103 dependencyTreeBuilder.buildDependencyTree( project, repository, factory, metadataSource, filter, 104 collector ); 105 return node; 106 } 107 catch ( ExpressionEvaluationException e ) 108 { 109 throw new EnforcerRuleException( "Unable to lookup an expression " + e.getLocalizedMessage(), e ); 110 } 111 catch ( ComponentLookupException e ) 112 { 113 throw new EnforcerRuleException( "Unable to lookup a component " + e.getLocalizedMessage(), e ); 114 } 115 catch ( DependencyTreeBuilderException e ) 116 { 117 throw new EnforcerRuleException( "Could not build dependency tree " + e.getLocalizedMessage(), e ); 118 } 119 } 120 121 public void execute( EnforcerRuleHelper helper ) 122 throws EnforcerRuleException 123 { 124 if ( log == null ) 125 { 126 log = helper.getLog(); 127 } 128 try 129 { 130 if ( i18n == null ) 131 { 132 i18n = (I18N) helper.getComponent( I18N.class ); 133 } 134 DependencyNode node = getNode( helper ); 135 RequireUpperBoundDepsVisitor visitor = new RequireUpperBoundDepsVisitor(); 136 visitor.setUniqueVersions( uniqueVersions ); 137 node.accept( visitor ); 138 List<String> errorMessages = buildErrorMessages( visitor.getConflicts() ); 139 if ( errorMessages.size() > 0 ) 140 { 141 throw new EnforcerRuleException( "Failed while enforcing RequireUpperBoundDeps. The error(s) are " 142 + errorMessages ); 143 } 144 } 145 catch ( ComponentLookupException e ) 146 { 147 throw new EnforcerRuleException( "Unable to lookup a component " + e.getLocalizedMessage(), e ); 148 } 149 catch ( Exception e ) 150 { 151 throw new EnforcerRuleException( e.getLocalizedMessage(), e ); 152 } 153 } 154 155 private List<String> buildErrorMessages( List<List<DependencyNode>> conflicts ) 156 { 157 List<String> errorMessages = new ArrayList<String>( conflicts.size() ); 158 for ( List<DependencyNode> conflict : conflicts ) 159 { 160 errorMessages.add( buildErrorMessage( conflict ) ); 161 } 162 return errorMessages; 163 } 164 165 private String buildErrorMessage( List<DependencyNode> conflict ) 166 { 167 StringBuilder errorMessage = new StringBuilder(); 168 errorMessage.append( "\nRequire upper bound dependencies error for " 169 + getFullArtifactName( conflict.get( 0 ), false ) + " paths to dependency are:\n" ); 170 if ( conflict.size() > 0 ) 171 { 172 errorMessage.append( buildTreeString( conflict.get( 0 ) ) ); 173 } 174 for ( DependencyNode node : conflict.subList( 1, conflict.size() ) ) 175 { 176 errorMessage.append( "and\n" ); 177 errorMessage.append( buildTreeString( node ) ); 178 } 179 return errorMessage.toString(); 180 } 181 182 private StringBuilder buildTreeString( DependencyNode node ) 183 { 184 List<String> loc = new ArrayList<String>(); 185 DependencyNode currentNode = node; 186 while ( currentNode != null ) 187 { 188 StringBuilder line = new StringBuilder( getFullArtifactName( currentNode, false ) ); 189 190 if ( currentNode.getPremanagedVersion() != null ) 191 { 192 line.append( " (managed) <-- " ); 193 line.append( getFullArtifactName( currentNode, true ) ); 194 } 195 196 loc.add( line.toString() ); 197 currentNode = currentNode.getParent(); 198 } 199 Collections.reverse( loc ); 200 StringBuilder builder = new StringBuilder(); 201 for ( int i = 0; i < loc.size(); i++ ) 202 { 203 for ( int j = 0; j < i; j++ ) 204 { 205 builder.append( " " ); 206 } 207 builder.append( "+-" ).append( loc.get( i ) ); 208 builder.append( "\n" ); 209 } 210 return builder; 211 } 212 213 private String getFullArtifactName( DependencyNode node, boolean usePremanaged ) 214 { 215 Artifact artifact = node.getArtifact(); 216 217 String version = node.getPremanagedVersion(); 218 if ( !usePremanaged || version == null ) 219 { 220 version = uniqueVersions ? artifact.getVersion() : artifact.getBaseVersion(); 221 } 222 return artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + version; 223 } 224 225 private static class RequireUpperBoundDepsVisitor 226 implements DependencyNodeVisitor 227 { 228 229 private boolean uniqueVersions; 230 231 public void setUniqueVersions( boolean uniqueVersions ) 232 { 233 this.uniqueVersions = uniqueVersions; 234 } 235 236 private Map<String, List<DependencyNodeHopCountPair>> keyToPairsMap = 237 new LinkedHashMap<String, List<DependencyNodeHopCountPair>>(); 238 239 public boolean visit( DependencyNode node ) 240 { 241 DependencyNodeHopCountPair pair = new DependencyNodeHopCountPair( node ); 242 String key = pair.constructKey(); 243 List<DependencyNodeHopCountPair> pairs = keyToPairsMap.get( key ); 244 if ( pairs == null ) 245 { 246 pairs = new ArrayList<DependencyNodeHopCountPair>(); 247 keyToPairsMap.put( key, pairs ); 248 } 249 pairs.add( pair ); 250 Collections.sort( pairs ); 251 return true; 252 } 253 254 public boolean endVisit( DependencyNode node ) 255 { 256 return true; 257 } 258 259 public List<List<DependencyNode>> getConflicts() 260 { 261 List<List<DependencyNode>> output = new ArrayList<List<DependencyNode>>(); 262 for ( List<DependencyNodeHopCountPair> pairs : keyToPairsMap.values() ) 263 { 264 if ( containsConflicts( pairs ) ) 265 { 266 List<DependencyNode> outputSubList = new ArrayList<DependencyNode>( pairs.size() ); 267 for ( DependencyNodeHopCountPair pair : pairs ) 268 { 269 outputSubList.add( pair.getNode() ); 270 } 271 output.add( outputSubList ); 272 } 273 } 274 return output; 275 } 276 277 @SuppressWarnings( "unchecked" ) 278 private boolean containsConflicts( List<DependencyNodeHopCountPair> pairs ) 279 { 280 DependencyNodeHopCountPair resolvedPair = pairs.get( 0 ); 281 282 // search for artifact with lowest hopCount 283 for ( DependencyNodeHopCountPair hopPair : pairs.subList( 1, pairs.size() ) ) 284 { 285 if ( hopPair.getHopCount() < resolvedPair.getHopCount() ) 286 { 287 resolvedPair = hopPair; 288 } 289 } 290 291 ArtifactVersion resolvedVersion = resolvedPair.extractArtifactVersion( uniqueVersions, false ); 292 293 for ( DependencyNodeHopCountPair pair : pairs ) 294 { 295 ArtifactVersion version = pair.extractArtifactVersion( uniqueVersions, true ); 296 if ( resolvedVersion.compareTo( version ) < 0 ) 297 { 298 return true; 299 } 300 } 301 return false; 302 } 303 304 } 305 306 private static class DependencyNodeHopCountPair 307 implements Comparable<DependencyNodeHopCountPair> 308 { 309 310 private DependencyNode node; 311 312 private int hopCount; 313 314 private DependencyNodeHopCountPair( DependencyNode node ) 315 { 316 this.node = node; 317 countHops(); 318 } 319 320 private void countHops() 321 { 322 hopCount = 0; 323 DependencyNode parent = node.getParent(); 324 while ( parent != null ) 325 { 326 hopCount++; 327 parent = parent.getParent(); 328 } 329 } 330 331 private String constructKey() 332 { 333 Artifact artifact = node.getArtifact(); 334 return artifact.getGroupId() + ":" + artifact.getArtifactId(); 335 } 336 337 public DependencyNode getNode() 338 { 339 return node; 340 } 341 342 private ArtifactVersion extractArtifactVersion( boolean uniqueVersions, boolean usePremanagedVersion ) 343 { 344 if ( usePremanagedVersion && node.getPremanagedVersion() != null ) 345 { 346 return new DefaultArtifactVersion( node.getPremanagedVersion() ); 347 } 348 349 Artifact artifact = node.getArtifact(); 350 String version = uniqueVersions ? artifact.getVersion() : artifact.getBaseVersion(); 351 if ( version != null ) 352 { 353 return new DefaultArtifactVersion( version ); 354 } 355 try 356 { 357 return artifact.getSelectedVersion(); 358 } 359 catch ( OverConstrainedVersionException e ) 360 { 361 throw new RuntimeException( "Version ranges problem with " + node.getArtifact(), e ); 362 } 363 } 364 365 public int getHopCount() 366 { 367 return hopCount; 368 } 369 370 public int compareTo( DependencyNodeHopCountPair other ) 371 { 372 return Integer.valueOf( hopCount ).compareTo( Integer.valueOf( other.getHopCount() ) ); 373 } 374 } 375 376}