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