package org.apache.maven.plugins.enforcer; | |
/* | |
* Licensed to the Apache Software Foundation (ASF) under one | |
* or more contributor license agreements. See the NOTICE file | |
* distributed with this work for additional information | |
* regarding copyright ownership. The ASF licenses this file | |
* to you under the Apache License, Version 2.0 (the | |
* "License"); you may not use this file except in compliance | |
* with the License. You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, | |
* software distributed under the License is distributed on an | |
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
* KIND, either express or implied. See the License for the | |
* specific language governing permissions and limitations | |
* under the License. | |
*/ | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Map.Entry; | |
import org.apache.commons.lang3.SystemUtils; | |
import org.apache.maven.enforcer.rule.api.EnforcerRuleException; | |
import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper; | |
import org.apache.maven.execution.MavenSession; | |
import org.apache.maven.model.Dependency; | |
import org.apache.maven.plugin.logging.Log; | |
import org.apache.maven.project.MavenProject; | |
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException; | |
import org.codehaus.plexus.util.StringUtils; | |
/** | |
* This rule will check if a multi module build will follow the best practices. | |
* | |
* @author Karl-Heinz Marbaise | |
* @since 1.4 | |
*/ | |
public class ReactorModuleConvergence | |
extends AbstractNonCacheableEnforcerRule | |
{ | |
private boolean ignoreModuleDependencies = false; | |
private Log logger; | |
@Override | |
public void execute( EnforcerRuleHelper helper ) | |
throws EnforcerRuleException | |
{ | |
logger = helper.getLog(); | |
MavenSession session; | |
try | |
{ | |
session = (MavenSession) helper.evaluate( "${session}" ); | |
} | |
catch ( ExpressionEvaluationException eee ) | |
{ | |
throw new EnforcerRuleException( "Unable to retrieve the MavenSession: ", eee ); | |
} | |
List<MavenProject> sortedProjects = session.getProjectDependencyGraph().getSortedProjects(); | |
if ( sortedProjects != null && !sortedProjects.isEmpty() ) | |
{ | |
checkReactor( sortedProjects ); | |
checkParentsInReactor( sortedProjects ); | |
checkMissingParentsInReactor( sortedProjects ); | |
checkParentsPartOfTheReactor( sortedProjects ); | |
if ( !isIgnoreModuleDependencies() ) | |
{ | |
checkDependenciesWithinReactor( sortedProjects ); | |
} | |
} | |
} | |
private void checkParentsPartOfTheReactor( List<MavenProject> sortedProjects ) | |
throws EnforcerRuleException | |
{ | |
List<MavenProject> parentsWhichAreNotPartOfTheReactor = | |
existParentsWhichAreNotPartOfTheReactor( sortedProjects ); | |
if ( !parentsWhichAreNotPartOfTheReactor.isEmpty() ) | |
{ | |
StringBuilder sb = new StringBuilder().append( SystemUtils.LINE_SEPARATOR ); | |
addMessageIfExist( sb ); | |
for ( MavenProject mavenProject : parentsWhichAreNotPartOfTheReactor ) | |
{ | |
sb.append( " module: " ); | |
sb.append( mavenProject.getId() ); | |
sb.append( SystemUtils.LINE_SEPARATOR ); | |
} | |
throw new EnforcerRuleException( "Module parents have been found which could not be found in the reactor." | |
+ sb.toString() ); | |
} | |
} | |
/** | |
* Convenience method to create a user readable message. | |
* | |
* @param sortedProjects The list of reactor projects. | |
* @throws EnforcerRuleException In case of a violation. | |
*/ | |
private void checkMissingParentsInReactor( List<MavenProject> sortedProjects ) | |
throws EnforcerRuleException | |
{ | |
List<MavenProject> modulesWithoutParentsInReactor = existModulesWithoutParentsInReactor( sortedProjects ); | |
if ( !modulesWithoutParentsInReactor.isEmpty() ) | |
{ | |
StringBuilder sb = new StringBuilder().append( SystemUtils.LINE_SEPARATOR ); | |
addMessageIfExist( sb ); | |
for ( MavenProject mavenProject : modulesWithoutParentsInReactor ) | |
{ | |
sb.append( " module: " ); | |
sb.append( mavenProject.getId() ); | |
sb.append( SystemUtils.LINE_SEPARATOR ); | |
} | |
throw new EnforcerRuleException( "Reactor contains modules without parents." + sb.toString() ); | |
} | |
} | |
private void checkDependenciesWithinReactor( List<MavenProject> sortedProjects ) | |
throws EnforcerRuleException | |
{ | |
// After we are sure having consistent version we can simply use the first one? | |
String reactorVersion = sortedProjects.get( 0 ).getVersion(); | |
Map<MavenProject, List<Dependency>> areThereDependenciesWhichAreNotPartOfTheReactor = | |
areThereDependenciesWhichAreNotPartOfTheReactor( reactorVersion, sortedProjects ); | |
if ( !areThereDependenciesWhichAreNotPartOfTheReactor.isEmpty() ) | |
{ | |
StringBuilder sb = new StringBuilder().append( SystemUtils.LINE_SEPARATOR ); | |
addMessageIfExist( sb ); | |
// CHECKSTYLE_OFF: LineLength | |
for ( Entry<MavenProject, List<Dependency>> item : areThereDependenciesWhichAreNotPartOfTheReactor.entrySet() ) | |
{ | |
sb.append( " module: " ); | |
sb.append( item.getKey().getId() ); | |
sb.append( SystemUtils.LINE_SEPARATOR ); | |
for ( Dependency dependency : item.getValue() ) | |
{ | |
String id = | |
dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion(); | |
sb.append( " dependency: " ); | |
sb.append( id ); | |
sb.append( SystemUtils.LINE_SEPARATOR ); | |
} | |
} | |
throw new EnforcerRuleException( | |
"Reactor modules contains dependencies which do not reference the reactor." | |
+ sb.toString() ); | |
// CHECKSTYLE_ON: LineLength | |
} | |
} | |
/** | |
* Convenience method to create a user readable message. | |
* | |
* @param sortedProjects The list of reactor projects. | |
* @throws EnforcerRuleException In case of a violation. | |
*/ | |
private void checkParentsInReactor( List<MavenProject> sortedProjects ) | |
throws EnforcerRuleException | |
{ | |
// After we are sure having consistent version we can simply use the first one? | |
String reactorVersion = sortedProjects.get( 0 ).getVersion(); | |
List<MavenProject> areParentsFromTheReactor = areParentsFromTheReactor( reactorVersion, sortedProjects ); | |
if ( !areParentsFromTheReactor.isEmpty() ) | |
{ | |
StringBuilder sb = new StringBuilder().append( SystemUtils.LINE_SEPARATOR ); | |
addMessageIfExist( sb ); | |
for ( MavenProject mavenProject : areParentsFromTheReactor ) | |
{ | |
sb.append( " --> " ); | |
sb.append( mavenProject.getId() ); | |
sb.append( " parent:" ); | |
sb.append( mavenProject.getParent().getId() ); | |
sb.append( SystemUtils.LINE_SEPARATOR ); | |
} | |
throw new EnforcerRuleException( "Reactor modules have parents which contain a wrong version." | |
+ sb.toString() ); | |
} | |
} | |
/** | |
* Convenience method to create user readable message. | |
* | |
* @param sortedProjects The list of reactor projects. | |
* @throws EnforcerRuleException In case of a violation. | |
*/ | |
private void checkReactor( List<MavenProject> sortedProjects ) | |
throws EnforcerRuleException | |
{ | |
List<MavenProject> consistenceCheckResult = isReactorVersionConsistent( sortedProjects ); | |
if ( !consistenceCheckResult.isEmpty() ) | |
{ | |
StringBuilder sb = new StringBuilder().append( SystemUtils.LINE_SEPARATOR ); | |
addMessageIfExist( sb ); | |
for ( MavenProject mavenProject : consistenceCheckResult ) | |
{ | |
sb.append( " --> " ); | |
sb.append( mavenProject.getId() ); | |
sb.append( SystemUtils.LINE_SEPARATOR ); | |
} | |
throw new EnforcerRuleException( "The reactor contains different versions." + sb.toString() ); | |
} | |
} | |
private List<MavenProject> areParentsFromTheReactor( String reactorVersion, List<MavenProject> sortedProjects ) | |
{ | |
List<MavenProject> result = new ArrayList<MavenProject>(); | |
for ( MavenProject mavenProject : sortedProjects ) | |
{ | |
logger.debug( "Project: " + mavenProject.getId() ); | |
if ( hasParent( mavenProject ) ) | |
{ | |
if ( !mavenProject.isExecutionRoot() ) | |
{ | |
MavenProject parent = mavenProject.getParent(); | |
if ( !reactorVersion.equals( parent.getVersion() ) ) | |
{ | |
logger.debug( "The project: " + mavenProject.getId() | |
+ " has a parent which version does not match the other elements in reactor" ); | |
result.add( mavenProject ); | |
} | |
} | |
} | |
else | |
{ | |
// This situation is currently ignored, cause it's handled by existModulesWithoutParentsInReactor() | |
} | |
} | |
return result; | |
} | |
private List<MavenProject> existParentsWhichAreNotPartOfTheReactor( List<MavenProject> sortedProjects ) | |
{ | |
List<MavenProject> result = new ArrayList<MavenProject>(); | |
for ( MavenProject mavenProject : sortedProjects ) | |
{ | |
logger.debug( "Project: " + mavenProject.getId() ); | |
if ( hasParent( mavenProject ) ) | |
{ | |
if ( !mavenProject.isExecutionRoot() ) | |
{ | |
MavenProject parent = mavenProject.getParent(); | |
if ( !isProjectPartOfTheReactor( parent, sortedProjects ) ) | |
{ | |
result.add( mavenProject ); | |
} | |
} | |
} | |
} | |
return result; | |
} | |
/** | |
* This will check of the groupId/artifactId can be found in any reactor project. The version will be ignored cause | |
* versions are checked before. | |
* | |
* @param project The project which should be checked if it is contained in the sortedProjects. | |
* @param sortedProjects The list of existing projects. | |
* @return true if the project has been found within the list false otherwise. | |
*/ | |
private boolean isProjectPartOfTheReactor( MavenProject project, List<MavenProject> sortedProjects ) | |
{ | |
return isGAPartOfTheReactor( project.getGroupId(), project.getArtifactId(), sortedProjects ); | |
} | |
private boolean isDependencyPartOfTheReactor( Dependency dependency, List<MavenProject> sortedProjects ) | |
{ | |
return isGAPartOfTheReactor( dependency.getGroupId(), dependency.getArtifactId(), sortedProjects ); | |
} | |
/** | |
* This will check if the given <code>groupId/artifactId</code> is part of the current reactor. | |
* | |
* @param groupId The groupId | |
* @param artifactId The artifactId | |
* @param sortedProjects The list of projects within the reactor. | |
* @return true if the groupId/artifactId is part of the reactor false otherwise. | |
*/ | |
private boolean isGAPartOfTheReactor( String groupId, String artifactId, List<MavenProject> sortedProjects ) | |
{ | |
boolean result = false; | |
for ( MavenProject mavenProject : sortedProjects ) | |
{ | |
String parentId = groupId + ":" + artifactId; | |
String projectId = mavenProject.getGroupId() + ":" + mavenProject.getArtifactId(); | |
if ( parentId.equals( projectId ) ) | |
{ | |
result = true; | |
} | |
} | |
return result; | |
} | |
/** | |
* Assume we have a module which is a child of a multi module build but this child does not have a parent. This | |
* method will exactly search for such cases. | |
* | |
* @param sortedProjects The sorted list of the reactor modules. | |
* @return The resulting list will contain the modules in the reactor which do not have a parent. The list will | |
* never null. If the list is empty no violation have happened. | |
*/ | |
private List<MavenProject> existModulesWithoutParentsInReactor( List<MavenProject> sortedProjects ) | |
{ | |
List<MavenProject> result = new ArrayList<MavenProject>(); | |
for ( MavenProject mavenProject : sortedProjects ) | |
{ | |
logger.debug( "Project: " + mavenProject.getId() ); | |
if ( !hasParent( mavenProject ) ) | |
{ | |
// TODO: Should add an option to force having a parent? | |
if ( mavenProject.isExecutionRoot() ) | |
{ | |
logger.debug( "The root does not need having a parent." ); | |
} | |
else | |
{ | |
logger.debug( "The module: " + mavenProject.getId() + " has no parent." ); | |
result.add( mavenProject ); | |
} | |
} | |
} | |
return result; | |
} | |
/** | |
* Convenience method to handle adding a dependency to the Map of List. | |
* | |
* @param result The result List which should be handled. | |
* @param project The MavenProject which will be added. | |
* @param dependency The dependency which will be added. | |
*/ | |
private void addDep( Map<MavenProject, List<Dependency>> result, MavenProject project, Dependency dependency ) | |
{ | |
if ( result.containsKey( project ) ) | |
{ | |
List<Dependency> list = result.get( project ); | |
if ( list == null ) | |
{ | |
list = new ArrayList<Dependency>(); | |
} | |
list.add( dependency ); | |
result.put( project, list ); | |
} | |
else | |
{ | |
List<Dependency> list = new ArrayList<Dependency>(); | |
list.add( dependency ); | |
result.put( project, list ); | |
} | |
} | |
/** | |
* Go through the list of modules in the builds and check if we have dependencies. If yes we will check every | |
* dependency based on groupId/artifactId if it belongs to the multi module build. In such a case it will be checked | |
* if the version does fit the version in the rest of build. | |
* | |
* @param reactorVersion The version of the reactor. | |
* @param sortedProjects The list of existing projects within this build. | |
* @return List of violations. Never null. If the list is empty than no violation has happened. | |
*/ | |
// CHECKSTYLE_OFF: LineLength | |
private Map<MavenProject, List<Dependency>> areThereDependenciesWhichAreNotPartOfTheReactor( String reactorVersion, | |
List<MavenProject> sortedProjects ) | |
// CHECKSTYLE_ON: LineLength | |
{ | |
Map<MavenProject, List<Dependency>> result = new HashMap<MavenProject, List<Dependency>>(); | |
for ( MavenProject mavenProject : sortedProjects ) | |
{ | |
logger.debug( "Project: " + mavenProject.getId() ); | |
List<Dependency> dependencies = mavenProject.getDependencies(); | |
if ( hasDependencies( dependencies ) ) | |
{ | |
for ( Dependency dependency : dependencies ) | |
{ | |
logger.debug( " -> Dep:" + dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" | |
+ dependency.getVersion() ); | |
if ( isDependencyPartOfTheReactor( dependency, sortedProjects ) ) | |
{ | |
if ( !dependency.getVersion().equals( reactorVersion ) ) | |
{ | |
addDep( result, mavenProject, dependency ); | |
} | |
} | |
} | |
} | |
} | |
return result; | |
} | |
/** | |
* This method will check the following situation within a multi-module build. | |
* | |
* <pre> | |
* <parent> | |
* <groupId>...</groupId> | |
* <artifactId>...</artifactId> | |
* <version>1.0-SNAPSHOT</version> | |
* </parent> | |
* | |
* <version>1.1-SNAPSHOT</version> | |
* </pre> | |
* | |
* @param projectList The sorted list of the reactor modules. | |
* @return The resulting list will contain the modules in the reactor which do the thing in the example above. The | |
* list will never null. If the list is empty no violation have happened. | |
*/ | |
private List<MavenProject> isReactorVersionConsistent( List<MavenProject> projectList ) | |
{ | |
List<MavenProject> result = new ArrayList<MavenProject>(); | |
if ( projectList != null && !projectList.isEmpty() ) | |
{ | |
String version = projectList.get( 0 ).getVersion(); | |
logger.debug( "First version:" + version ); | |
for ( MavenProject mavenProject : projectList ) | |
{ | |
logger.debug( " -> checking " + mavenProject.getId() ); | |
if ( !version.equals( mavenProject.getVersion() ) ) | |
{ | |
result.add( mavenProject ); | |
} | |
} | |
} | |
return result; | |
} | |
private boolean hasDependencies( List<Dependency> dependencies ) | |
{ | |
return dependencies != null && !dependencies.isEmpty(); | |
} | |
private boolean hasParent( MavenProject mavenProject ) | |
{ | |
return mavenProject.getParent() != null; | |
} | |
public boolean isIgnoreModuleDependencies() | |
{ | |
return ignoreModuleDependencies; | |
} | |
public void setIgnoreModuleDependencies( boolean ignoreModuleDependencies ) | |
{ | |
this.ignoreModuleDependencies = ignoreModuleDependencies; | |
} | |
/** | |
* This will add the given user message to the output. | |
* | |
* @param sb The already initialized exception message part. | |
*/ | |
private void addMessageIfExist( StringBuilder sb ) | |
{ | |
if ( !StringUtils.isEmpty( getMessage() ) ) | |
{ | |
sb.append( getMessage() ); | |
sb.append( SystemUtils.LINE_SEPARATOR ); | |
} | |
} | |
} |