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 // CHECKSTYLE_OFF: LineLength 079 /** 080 * Uses the {@link EnforcerRuleHelper} to populate the values of the 081 * {@link DependencyTreeBuilder#buildDependencyTree(MavenProject, ArtifactRepository, ArtifactFactory, ArtifactMetadataSource, ArtifactFilter, ArtifactCollector)} 082 * factory method. <br/> 083 * This method simply exists to hide all the ugly lookup that the {@link EnforcerRuleHelper} has to do. 084 * 085 * @param helper 086 * @return a Dependency Node which is the root of the project's dependency tree 087 * @throws EnforcerRuleException when the build should fail 088 */ 089 // CHECKSTYLE_ON: LineLength 090 private DependencyNode getNode( EnforcerRuleHelper helper ) 091 throws EnforcerRuleException 092 { 093 try 094 { 095 MavenProject project = (MavenProject) helper.evaluate( "${project}" ); 096 DependencyTreeBuilder dependencyTreeBuilder = 097 (DependencyTreeBuilder) helper.getComponent( DependencyTreeBuilder.class ); 098 ArtifactRepository repository = (ArtifactRepository) helper.evaluate( "${localRepository}" ); 099 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}