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.HashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Map.Entry; 027 028import org.apache.commons.lang3.SystemUtils; 029import org.apache.maven.enforcer.rule.api.EnforcerRuleException; 030import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper; 031import org.apache.maven.execution.MavenSession; 032import org.apache.maven.model.Dependency; 033import org.apache.maven.plugin.logging.Log; 034import org.apache.maven.project.MavenProject; 035import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException; 036import org.codehaus.plexus.util.StringUtils; 037 038/** 039 * This rule will check if a multi module build will follow the best practices. 040 * 041 * @author Karl-Heinz Marbaise 042 * @since 1.4 043 */ 044public class ReactorModuleConvergence 045 extends AbstractNonCacheableEnforcerRule 046{ 047 private boolean ignoreModuleDependencies = false; 048 049 private Log logger; 050 051 @Override 052 public void execute( EnforcerRuleHelper helper ) 053 throws EnforcerRuleException 054 { 055 logger = helper.getLog(); 056 057 MavenSession session; 058 try 059 { 060 session = (MavenSession) helper.evaluate( "${session}" ); 061 } 062 catch ( ExpressionEvaluationException eee ) 063 { 064 throw new EnforcerRuleException( "Unable to retrieve the MavenSession: ", eee ); 065 } 066 067 List<MavenProject> sortedProjects = session.getProjectDependencyGraph().getSortedProjects(); 068 if ( sortedProjects != null && !sortedProjects.isEmpty() ) 069 { 070 checkReactor( sortedProjects ); 071 checkParentsInReactor( sortedProjects ); 072 checkMissingParentsInReactor( sortedProjects ); 073 checkParentsPartOfTheReactor( sortedProjects ); 074 if ( !isIgnoreModuleDependencies() ) 075 { 076 checkDependenciesWithinReactor( sortedProjects ); 077 } 078 } 079 080 } 081 082 private void checkParentsPartOfTheReactor( List<MavenProject> sortedProjects ) 083 throws EnforcerRuleException 084 { 085 List<MavenProject> parentsWhichAreNotPartOfTheReactor = 086 existParentsWhichAreNotPartOfTheReactor( sortedProjects ); 087 if ( !parentsWhichAreNotPartOfTheReactor.isEmpty() ) 088 { 089 StringBuilder sb = new StringBuilder().append( SystemUtils.LINE_SEPARATOR ); 090 addMessageIfExist( sb ); 091 for ( MavenProject mavenProject : parentsWhichAreNotPartOfTheReactor ) 092 { 093 sb.append( " module: " ); 094 sb.append( mavenProject.getId() ); 095 sb.append( SystemUtils.LINE_SEPARATOR ); 096 } 097 throw new EnforcerRuleException( "Module parents have been found which could not be found in the reactor." 098 + sb.toString() ); 099 } 100 } 101 102 /** 103 * Convenience method to create a user readable message. 104 * 105 * @param sortedProjects The list of reactor projects. 106 * @throws EnforcerRuleException In case of a violation. 107 */ 108 private void checkMissingParentsInReactor( List<MavenProject> sortedProjects ) 109 throws EnforcerRuleException 110 { 111 List<MavenProject> modulesWithoutParentsInReactor = existModulesWithoutParentsInReactor( sortedProjects ); 112 if ( !modulesWithoutParentsInReactor.isEmpty() ) 113 { 114 StringBuilder sb = new StringBuilder().append( SystemUtils.LINE_SEPARATOR ); 115 addMessageIfExist( sb ); 116 for ( MavenProject mavenProject : modulesWithoutParentsInReactor ) 117 { 118 sb.append( " module: " ); 119 sb.append( mavenProject.getId() ); 120 sb.append( SystemUtils.LINE_SEPARATOR ); 121 } 122 throw new EnforcerRuleException( "Reactor contains modules without parents." + sb.toString() ); 123 } 124 } 125 126 private void checkDependenciesWithinReactor( List<MavenProject> sortedProjects ) 127 throws EnforcerRuleException 128 { 129 // After we are sure having consistent version we can simply use the first one? 130 String reactorVersion = sortedProjects.get( 0 ).getVersion(); 131 132 Map<MavenProject, List<Dependency>> areThereDependenciesWhichAreNotPartOfTheReactor = 133 areThereDependenciesWhichAreNotPartOfTheReactor( reactorVersion, sortedProjects ); 134 if ( !areThereDependenciesWhichAreNotPartOfTheReactor.isEmpty() ) 135 { 136 StringBuilder sb = new StringBuilder().append( SystemUtils.LINE_SEPARATOR ); 137 addMessageIfExist( sb ); 138 // CHECKSTYLE_OFF: LineLength 139 for ( Entry<MavenProject, List<Dependency>> item : areThereDependenciesWhichAreNotPartOfTheReactor.entrySet() ) 140 { 141 sb.append( " module: " ); 142 sb.append( item.getKey().getId() ); 143 sb.append( SystemUtils.LINE_SEPARATOR ); 144 for ( Dependency dependency : item.getValue() ) 145 { 146 String id = 147 dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion(); 148 sb.append( " dependency: " ); 149 sb.append( id ); 150 sb.append( SystemUtils.LINE_SEPARATOR ); 151 } 152 } 153 throw new EnforcerRuleException( 154 "Reactor modules contains dependencies which do not reference the reactor." 155 + sb.toString() ); 156 // CHECKSTYLE_ON: LineLength 157 } 158 } 159 160 /** 161 * Convenience method to create a user readable message. 162 * 163 * @param sortedProjects The list of reactor projects. 164 * @throws EnforcerRuleException In case of a violation. 165 */ 166 private void checkParentsInReactor( List<MavenProject> sortedProjects ) 167 throws EnforcerRuleException 168 { 169 // After we are sure having consistent version we can simply use the first one? 170 String reactorVersion = sortedProjects.get( 0 ).getVersion(); 171 172 List<MavenProject> areParentsFromTheReactor = areParentsFromTheReactor( reactorVersion, sortedProjects ); 173 if ( !areParentsFromTheReactor.isEmpty() ) 174 { 175 StringBuilder sb = new StringBuilder().append( SystemUtils.LINE_SEPARATOR ); 176 addMessageIfExist( sb ); 177 for ( MavenProject mavenProject : areParentsFromTheReactor ) 178 { 179 sb.append( " --> " ); 180 sb.append( mavenProject.getId() ); 181 sb.append( " parent:" ); 182 sb.append( mavenProject.getParent().getId() ); 183 sb.append( SystemUtils.LINE_SEPARATOR ); 184 } 185 throw new EnforcerRuleException( "Reactor modules have parents which contain a wrong version." 186 + sb.toString() ); 187 } 188 } 189 190 /** 191 * Convenience method to create user readable message. 192 * 193 * @param sortedProjects The list of reactor projects. 194 * @throws EnforcerRuleException In case of a violation. 195 */ 196 private void checkReactor( List<MavenProject> sortedProjects ) 197 throws EnforcerRuleException 198 { 199 List<MavenProject> consistenceCheckResult = isReactorVersionConsistent( sortedProjects ); 200 if ( !consistenceCheckResult.isEmpty() ) 201 { 202 StringBuilder sb = new StringBuilder().append( SystemUtils.LINE_SEPARATOR ); 203 addMessageIfExist( sb ); 204 for ( MavenProject mavenProject : consistenceCheckResult ) 205 { 206 sb.append( " --> " ); 207 sb.append( mavenProject.getId() ); 208 sb.append( SystemUtils.LINE_SEPARATOR ); 209 } 210 throw new EnforcerRuleException( "The reactor contains different versions." + sb.toString() ); 211 } 212 } 213 214 private List<MavenProject> areParentsFromTheReactor( String reactorVersion, List<MavenProject> sortedProjects ) 215 { 216 List<MavenProject> result = new ArrayList<MavenProject>(); 217 218 for ( MavenProject mavenProject : sortedProjects ) 219 { 220 logger.debug( "Project: " + mavenProject.getId() ); 221 if ( hasParent( mavenProject ) ) 222 { 223 if ( !mavenProject.isExecutionRoot() ) 224 { 225 MavenProject parent = mavenProject.getParent(); 226 if ( !reactorVersion.equals( parent.getVersion() ) ) 227 { 228 logger.debug( "The project: " + mavenProject.getId() 229 + " has a parent which version does not match the other elements in reactor" ); 230 result.add( mavenProject ); 231 } 232 } 233 } 234 else 235 { 236 // This situation is currently ignored, cause it's handled by existModulesWithoutParentsInReactor() 237 } 238 } 239 240 return result; 241 } 242 243 private List<MavenProject> existParentsWhichAreNotPartOfTheReactor( List<MavenProject> sortedProjects ) 244 { 245 List<MavenProject> result = new ArrayList<MavenProject>(); 246 247 for ( MavenProject mavenProject : sortedProjects ) 248 { 249 logger.debug( "Project: " + mavenProject.getId() ); 250 if ( hasParent( mavenProject ) ) 251 { 252 if ( !mavenProject.isExecutionRoot() ) 253 { 254 MavenProject parent = mavenProject.getParent(); 255 if ( !isProjectPartOfTheReactor( parent, sortedProjects ) ) 256 { 257 result.add( mavenProject ); 258 } 259 } 260 } 261 } 262 263 return result; 264 } 265 266 /** 267 * This will check of the groupId/artifactId can be found in any reactor project. The version will be ignored cause 268 * versions are checked before. 269 * 270 * @param project The project which should be checked if it is contained in the sortedProjects. 271 * @param sortedProjects The list of existing projects. 272 * @return true if the project has been found within the list false otherwise. 273 */ 274 private boolean isProjectPartOfTheReactor( MavenProject project, List<MavenProject> sortedProjects ) 275 { 276 return isGAPartOfTheReactor( project.getGroupId(), project.getArtifactId(), sortedProjects ); 277 } 278 279 private boolean isDependencyPartOfTheReactor( Dependency dependency, List<MavenProject> sortedProjects ) 280 { 281 return isGAPartOfTheReactor( dependency.getGroupId(), dependency.getArtifactId(), sortedProjects ); 282 } 283 284 /** 285 * This will check if the given <code>groupId/artifactId</code> is part of the current reactor. 286 * 287 * @param groupId The groupId 288 * @param artifactId The artifactId 289 * @param sortedProjects The list of projects within the reactor. 290 * @return true if the groupId/artifactId is part of the reactor false otherwise. 291 */ 292 private boolean isGAPartOfTheReactor( String groupId, String artifactId, List<MavenProject> sortedProjects ) 293 { 294 boolean result = false; 295 for ( MavenProject mavenProject : sortedProjects ) 296 { 297 String parentId = groupId + ":" + artifactId; 298 String projectId = mavenProject.getGroupId() + ":" + mavenProject.getArtifactId(); 299 if ( parentId.equals( projectId ) ) 300 { 301 result = true; 302 } 303 } 304 return result; 305 } 306 307 /** 308 * Assume we have a module which is a child of a multi module build but this child does not have a parent. This 309 * method will exactly search for such cases. 310 * 311 * @param sortedProjects The sorted list of the reactor modules. 312 * @return The resulting list will contain the modules in the reactor which do not have a parent. The list will 313 * never null. If the list is empty no violation have happened. 314 */ 315 private List<MavenProject> existModulesWithoutParentsInReactor( List<MavenProject> sortedProjects ) 316 { 317 List<MavenProject> result = new ArrayList<MavenProject>(); 318 319 for ( MavenProject mavenProject : sortedProjects ) 320 { 321 logger.debug( "Project: " + mavenProject.getId() ); 322 if ( !hasParent( mavenProject ) ) 323 { 324 // TODO: Should add an option to force having a parent? 325 if ( mavenProject.isExecutionRoot() ) 326 { 327 logger.debug( "The root does not need having a parent." ); 328 } 329 else 330 { 331 logger.debug( "The module: " + mavenProject.getId() + " has no parent." ); 332 result.add( mavenProject ); 333 } 334 } 335 } 336 337 return result; 338 } 339 340 /** 341 * Convenience method to handle adding a dependency to the Map of List. 342 * 343 * @param result The result List which should be handled. 344 * @param project The MavenProject which will be added. 345 * @param dependency The dependency which will be added. 346 */ 347 private void addDep( Map<MavenProject, List<Dependency>> result, MavenProject project, Dependency dependency ) 348 { 349 if ( result.containsKey( project ) ) 350 { 351 List<Dependency> list = result.get( project ); 352 if ( list == null ) 353 { 354 list = new ArrayList<Dependency>(); 355 } 356 list.add( dependency ); 357 result.put( project, list ); 358 } 359 else 360 { 361 List<Dependency> list = new ArrayList<Dependency>(); 362 list.add( dependency ); 363 result.put( project, list ); 364 } 365 } 366 367 /** 368 * Go through the list of modules in the builds and check if we have dependencies. If yes we will check every 369 * dependency based on groupId/artifactId if it belongs to the multi module build. In such a case it will be checked 370 * if the version does fit the version in the rest of build. 371 * 372 * @param reactorVersion The version of the reactor. 373 * @param sortedProjects The list of existing projects within this build. 374 * @return List of violations. Never null. If the list is empty than no violation has happened. 375 */ 376 // CHECKSTYLE_OFF: LineLength 377 private Map<MavenProject, List<Dependency>> areThereDependenciesWhichAreNotPartOfTheReactor( String reactorVersion, 378 List<MavenProject> sortedProjects ) 379 // CHECKSTYLE_ON: LineLength 380 { 381 Map<MavenProject, List<Dependency>> result = new HashMap<MavenProject, List<Dependency>>(); 382 for ( MavenProject mavenProject : sortedProjects ) 383 { 384 logger.debug( "Project: " + mavenProject.getId() ); 385 386 List<Dependency> dependencies = mavenProject.getDependencies(); 387 if ( hasDependencies( dependencies ) ) 388 { 389 for ( Dependency dependency : dependencies ) 390 { 391 logger.debug( " -> Dep:" + dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" 392 + dependency.getVersion() ); 393 if ( isDependencyPartOfTheReactor( dependency, sortedProjects ) ) 394 { 395 if ( !dependency.getVersion().equals( reactorVersion ) ) 396 { 397 addDep( result, mavenProject, dependency ); 398 } 399 } 400 } 401 } 402 } 403 404 return result; 405 } 406 407 /** 408 * This method will check the following situation within a multi-module build. 409 * 410 * <pre> 411 * <parent> 412 * <groupId>...</groupId> 413 * <artifactId>...</artifactId> 414 * <version>1.0-SNAPSHOT</version> 415 * </parent> 416 * 417 * <version>1.1-SNAPSHOT</version> 418 * </pre> 419 * 420 * @param projectList The sorted list of the reactor modules. 421 * @return The resulting list will contain the modules in the reactor which do the thing in the example above. The 422 * list will never null. If the list is empty no violation have happened. 423 */ 424 private List<MavenProject> isReactorVersionConsistent( List<MavenProject> projectList ) 425 { 426 List<MavenProject> result = new ArrayList<MavenProject>(); 427 428 if ( projectList != null && !projectList.isEmpty() ) 429 { 430 String version = projectList.get( 0 ).getVersion(); 431 logger.debug( "First version:" + version ); 432 for ( MavenProject mavenProject : projectList ) 433 { 434 logger.debug( " -> checking " + mavenProject.getId() ); 435 if ( !version.equals( mavenProject.getVersion() ) ) 436 { 437 result.add( mavenProject ); 438 } 439 } 440 } 441 return result; 442 } 443 444 private boolean hasDependencies( List<Dependency> dependencies ) 445 { 446 return dependencies != null && !dependencies.isEmpty(); 447 } 448 449 private boolean hasParent( MavenProject mavenProject ) 450 { 451 return mavenProject.getParent() != null; 452 } 453 454 public boolean isIgnoreModuleDependencies() 455 { 456 return ignoreModuleDependencies; 457 } 458 459 public void setIgnoreModuleDependencies( boolean ignoreModuleDependencies ) 460 { 461 this.ignoreModuleDependencies = ignoreModuleDependencies; 462 } 463 464 /** 465 * This will add the given user message to the output. 466 * 467 * @param sb The already initialized exception message part. 468 */ 469 private void addMessageIfExist( StringBuilder sb ) 470 { 471 if ( !StringUtils.isEmpty( getMessage() ) ) 472 { 473 sb.append( getMessage() ); 474 sb.append( SystemUtils.LINE_SEPARATOR ); 475 } 476 } 477 478}