001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.maven.enforcer.rules; 020 021import javax.inject.Inject; 022import javax.inject.Named; 023 024import java.util.ArrayList; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Map.Entry; 029import java.util.Objects; 030 031import org.apache.maven.enforcer.rule.api.EnforcerRuleException; 032import org.apache.maven.execution.MavenSession; 033import org.apache.maven.model.Dependency; 034import org.apache.maven.project.MavenProject; 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 */ 043@Named("reactorModuleConvergence") 044public final class ReactorModuleConvergence extends AbstractStandardEnforcerRule { 045 private static final String MODULE_TEXT = " module: "; 046 047 private boolean ignoreModuleDependencies = false; 048 049 private final MavenSession session; 050 051 @Inject 052 public ReactorModuleConvergence(MavenSession session) { 053 this.session = Objects.requireNonNull(session); 054 } 055 056 @Override 057 public void execute() throws EnforcerRuleException { 058 059 List<MavenProject> sortedProjects = session.getProjectDependencyGraph().getSortedProjects(); 060 if (sortedProjects != null && !sortedProjects.isEmpty()) { 061 checkReactor(sortedProjects); 062 checkParentsInReactor(sortedProjects); 063 checkMissingParentsInReactor(sortedProjects); 064 checkParentsPartOfTheReactor(sortedProjects); 065 if (!isIgnoreModuleDependencies()) { 066 checkDependenciesWithinReactor(sortedProjects); 067 } 068 } 069 } 070 071 private void checkParentsPartOfTheReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException { 072 List<MavenProject> parentsWhichAreNotPartOfTheReactor = existParentsWhichAreNotPartOfTheReactor(sortedProjects); 073 if (!parentsWhichAreNotPartOfTheReactor.isEmpty()) { 074 StringBuilder sb = new StringBuilder().append(System.lineSeparator()); 075 addMessageIfExist(sb); 076 for (MavenProject mavenProject : parentsWhichAreNotPartOfTheReactor) { 077 sb.append(MODULE_TEXT); 078 sb.append(mavenProject.getId()); 079 sb.append(System.lineSeparator()); 080 } 081 throw new EnforcerRuleException( 082 "Module parents have been found which could not be found in the reactor." + sb); 083 } 084 } 085 086 /** 087 * Convenience method to create a user readable message. 088 * 089 * @param sortedProjects The list of reactor projects. 090 * @throws EnforcerRuleException In case of a violation. 091 */ 092 private void checkMissingParentsInReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException { 093 List<MavenProject> modulesWithoutParentsInReactor = existModulesWithoutParentsInReactor(sortedProjects); 094 if (!modulesWithoutParentsInReactor.isEmpty()) { 095 StringBuilder sb = new StringBuilder().append(System.lineSeparator()); 096 addMessageIfExist(sb); 097 for (MavenProject mavenProject : modulesWithoutParentsInReactor) { 098 sb.append(MODULE_TEXT); 099 sb.append(mavenProject.getId()); 100 sb.append(System.lineSeparator()); 101 } 102 throw new EnforcerRuleException("Reactor contains modules without parents." + sb); 103 } 104 } 105 106 private void checkDependenciesWithinReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException { 107 // After we are sure having consistent version we can simply use the first one? 108 String reactorVersion = sortedProjects.get(0).getVersion(); 109 110 Map<MavenProject, List<Dependency>> areThereDependenciesWhichAreNotPartOfTheReactor = 111 areThereDependenciesWhichAreNotPartOfTheReactor(reactorVersion, sortedProjects); 112 if (!areThereDependenciesWhichAreNotPartOfTheReactor.isEmpty()) { 113 StringBuilder sb = new StringBuilder().append(System.lineSeparator()); 114 addMessageIfExist(sb); 115 // CHECKSTYLE_OFF: LineLength 116 for (Entry<MavenProject, List<Dependency>> item : 117 areThereDependenciesWhichAreNotPartOfTheReactor.entrySet()) { 118 sb.append(MODULE_TEXT); 119 sb.append(item.getKey().getId()); 120 sb.append(System.lineSeparator()); 121 for (Dependency dependency : item.getValue()) { 122 String id = 123 dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion(); 124 sb.append(" dependency: "); 125 sb.append(id); 126 sb.append(System.lineSeparator()); 127 } 128 } 129 throw new EnforcerRuleException( 130 "Reactor modules contains dependencies which do not reference the reactor." + sb); 131 // CHECKSTYLE_ON: LineLength 132 } 133 } 134 135 /** 136 * Convenience method to create a user readable message. 137 * 138 * @param sortedProjects The list of reactor projects. 139 * @throws EnforcerRuleException In case of a violation. 140 */ 141 private void checkParentsInReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException { 142 // After we are sure having consistent version we can simply use the first one? 143 String reactorVersion = sortedProjects.get(0).getVersion(); 144 145 List<MavenProject> areParentsFromTheReactor = areParentsFromTheReactor(reactorVersion, sortedProjects); 146 if (!areParentsFromTheReactor.isEmpty()) { 147 StringBuilder sb = new StringBuilder().append(System.lineSeparator()); 148 addMessageIfExist(sb); 149 for (MavenProject mavenProject : areParentsFromTheReactor) { 150 sb.append(" --> "); 151 sb.append(mavenProject.getId()); 152 sb.append(" parent:"); 153 sb.append(mavenProject.getParent().getId()); 154 sb.append(System.lineSeparator()); 155 } 156 throw new EnforcerRuleException("Reactor modules have parents which contain a wrong version." + sb); 157 } 158 } 159 160 /** 161 * Convenience method to create user readable message. 162 * 163 * @param sortedProjects The list of reactor projects. 164 * @throws EnforcerRuleException In case of a violation. 165 */ 166 private void checkReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException { 167 List<MavenProject> consistenceCheckResult = isReactorVersionConsistent(sortedProjects); 168 if (!consistenceCheckResult.isEmpty()) { 169 StringBuilder sb = new StringBuilder().append(System.lineSeparator()); 170 addMessageIfExist(sb); 171 for (MavenProject mavenProject : consistenceCheckResult) { 172 sb.append(" --> "); 173 sb.append(mavenProject.getId()); 174 sb.append(System.lineSeparator()); 175 } 176 throw new EnforcerRuleException("The reactor contains different versions." + sb); 177 } 178 } 179 180 private List<MavenProject> areParentsFromTheReactor(String reactorVersion, List<MavenProject> sortedProjects) { 181 List<MavenProject> result = new ArrayList<>(); 182 183 for (MavenProject mavenProject : sortedProjects) { 184 getLog().debug("Project: " + mavenProject.getId()); 185 if (hasParent(mavenProject)) { 186 if (!mavenProject.isExecutionRoot()) { 187 MavenProject parent = mavenProject.getParent(); 188 if (!reactorVersion.equals(parent.getVersion())) { 189 getLog().debug("The project: " + mavenProject.getId() 190 + " has a parent which version does not match the other elements in reactor"); 191 result.add(mavenProject); 192 } 193 } 194 } else { 195 // This situation is currently ignored, cause it's handled by existModulesWithoutParentsInReactor() 196 } 197 } 198 199 return result; 200 } 201 202 private List<MavenProject> existParentsWhichAreNotPartOfTheReactor(List<MavenProject> sortedProjects) { 203 List<MavenProject> result = new ArrayList<>(); 204 205 for (MavenProject mavenProject : sortedProjects) { 206 getLog().debug("Project: " + mavenProject.getId()); 207 if (hasParent(mavenProject)) { 208 if (!mavenProject.isExecutionRoot()) { 209 MavenProject parent = mavenProject.getParent(); 210 if (!isProjectPartOfTheReactor(parent, sortedProjects)) { 211 result.add(mavenProject); 212 } 213 } 214 } 215 } 216 217 return result; 218 } 219 220 /** 221 * This will check of the groupId/artifactId can be found in any reactor project. The version will be ignored cause 222 * versions are checked before. 223 * 224 * @param project The project which should be checked if it is contained in the sortedProjects. 225 * @param sortedProjects The list of existing projects. 226 * @return true if the project has been found within the list false otherwise. 227 */ 228 private boolean isProjectPartOfTheReactor(MavenProject project, List<MavenProject> sortedProjects) { 229 return isGAPartOfTheReactor(project.getGroupId(), project.getArtifactId(), sortedProjects); 230 } 231 232 private boolean isDependencyPartOfTheReactor(Dependency dependency, List<MavenProject> sortedProjects) { 233 return isGAPartOfTheReactor(dependency.getGroupId(), dependency.getArtifactId(), sortedProjects); 234 } 235 236 /** 237 * This will check if the given <code>groupId/artifactId</code> is part of the current reactor. 238 * 239 * @param groupId The groupId 240 * @param artifactId The artifactId 241 * @param sortedProjects The list of projects within the reactor. 242 * @return true if the groupId/artifactId is part of the reactor false otherwise. 243 */ 244 private boolean isGAPartOfTheReactor(String groupId, String artifactId, List<MavenProject> sortedProjects) { 245 boolean result = false; 246 for (MavenProject mavenProject : sortedProjects) { 247 String parentId = groupId + ":" + artifactId; 248 String projectId = mavenProject.getGroupId() + ":" + mavenProject.getArtifactId(); 249 if (parentId.equals(projectId)) { 250 result = true; 251 } 252 } 253 return result; 254 } 255 256 /** 257 * Assume we have a module which is a child of a multi module build but this child does not have a parent. This 258 * method will exactly search for such cases. 259 * 260 * @param sortedProjects The sorted list of the reactor modules. 261 * @return The resulting list will contain the modules in the reactor which do not have a parent. The list will 262 * never null. If the list is empty no violation have happened. 263 */ 264 private List<MavenProject> existModulesWithoutParentsInReactor(List<MavenProject> sortedProjects) { 265 List<MavenProject> result = new ArrayList<>(); 266 267 for (MavenProject mavenProject : sortedProjects) { 268 getLog().debug("Project: " + mavenProject.getId()); 269 if (!hasParent(mavenProject)) { 270 // TODO: Should add an option to force having a parent? 271 if (mavenProject.isExecutionRoot()) { 272 getLog().debug("The root does not need having a parent."); 273 } else { 274 getLog().debug("The module: " + mavenProject.getId() + " has no parent."); 275 result.add(mavenProject); 276 } 277 } 278 } 279 280 return result; 281 } 282 283 /** 284 * Convenience method to handle adding a dependency to the Map of List. 285 * 286 * @param result The result List which should be handled. 287 * @param project The MavenProject which will be added. 288 * @param dependency The dependency which will be added. 289 */ 290 private void addDep(Map<MavenProject, List<Dependency>> result, MavenProject project, Dependency dependency) { 291 if (result.containsKey(project)) { 292 List<Dependency> list = result.get(project); 293 if (list == null) { 294 list = new ArrayList<>(); 295 } 296 list.add(dependency); 297 result.put(project, list); 298 } else { 299 List<Dependency> list = new ArrayList<>(); 300 list.add(dependency); 301 result.put(project, list); 302 } 303 } 304 305 /** 306 * Go through the list of modules in the builds and check if we have dependencies. If yes we will check every 307 * dependency based on groupId/artifactId if it belongs to the multi module build. In such a case it will be checked 308 * if the version does fit the version in the rest of build. 309 * 310 * @param reactorVersion The version of the reactor. 311 * @param sortedProjects The list of existing projects within this build. 312 * @return List of violations. Never null. If the list is empty than no violation has happened. 313 */ 314 // CHECKSTYLE_OFF: LineLength 315 private Map<MavenProject, List<Dependency>> areThereDependenciesWhichAreNotPartOfTheReactor( 316 String reactorVersion, List<MavenProject> sortedProjects) 317 // CHECKSTYLE_ON: LineLength 318 { 319 Map<MavenProject, List<Dependency>> result = new HashMap<>(); 320 for (MavenProject mavenProject : sortedProjects) { 321 getLog().debug("Project: " + mavenProject.getId()); 322 323 List<Dependency> dependencies = mavenProject.getDependencies(); 324 if (hasDependencies(dependencies)) { 325 for (Dependency dependency : dependencies) { 326 getLog().debug(" -> Dep:" + dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" 327 + dependency.getVersion()); 328 if (isDependencyPartOfTheReactor(dependency, sortedProjects)) { 329 if (!dependency.getVersion().equals(reactorVersion)) { 330 addDep(result, mavenProject, dependency); 331 } 332 } 333 } 334 } 335 } 336 337 return result; 338 } 339 340 /** 341 * This method will check the following situation within a multi-module build. 342 * <pre> 343 * <parent> 344 * <groupId>...</groupId> 345 * <artifactId>...</artifactId> 346 * <version>1.0-SNAPSHOT</version> 347 * </parent> 348 * <version>1.1-SNAPSHOT</version> 349 * </pre> 350 * 351 * @param projectList The sorted list of the reactor modules. 352 * @return The resulting list will contain the modules in the reactor which do the thing in the example above. The 353 * list will never null. If the list is empty no violation have happened. 354 */ 355 private List<MavenProject> isReactorVersionConsistent(List<MavenProject> projectList) { 356 List<MavenProject> result = new ArrayList<>(); 357 358 if (projectList != null && !projectList.isEmpty()) { 359 String version = projectList.get(0).getVersion(); 360 getLog().debug("First version:" + version); 361 for (MavenProject mavenProject : projectList) { 362 getLog().debug(" -> checking " + mavenProject.getId()); 363 if (!version.equals(mavenProject.getVersion())) { 364 result.add(mavenProject); 365 } 366 } 367 } 368 return result; 369 } 370 371 private boolean hasDependencies(List<Dependency> dependencies) { 372 return dependencies != null && !dependencies.isEmpty(); 373 } 374 375 private boolean hasParent(MavenProject mavenProject) { 376 return mavenProject.getParent() != null; 377 } 378 379 public boolean isIgnoreModuleDependencies() { 380 return ignoreModuleDependencies; 381 } 382 383 /** 384 * This will add the given user message to the output. 385 * 386 * @param sb The already initialized exception message part. 387 */ 388 private void addMessageIfExist(StringBuilder sb) { 389 if (!StringUtils.isEmpty(getMessage())) { 390 sb.append(getMessage()); 391 sb.append(System.lineSeparator()); 392 } 393 } 394 395 @Override 396 public String getCacheId() { 397 return String.valueOf(toString().hashCode()); 398 } 399 400 @Override 401 public String toString() { 402 return String.format( 403 "ReactorModuleConvergence[message=%s, ignoreModuleDependencies=%b]", 404 getMessage(), ignoreModuleDependencies); 405 } 406}