DependOnAllProjects.java
package com.github.mikkoi.maven.enforcer.rules;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.maven.enforcer.rule.api.AbstractEnforcerRule;
import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.project.MavenProject;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
/**
* Maven Enforcer Custom Rule.
*/
@Named("dependOnAllProjects")
public class DependOnAllProjects extends AbstractEnforcerRule {
/**
* Constant value: Size of indentation inside a <dependency>> definition.
*/
private static final String INDENT_DEPENDENCY = " ";
/**
* Constant value: Maximum number of parts in a dependency declaration.
*/
private static final int MAX_NUM_PARTS_IN_DEPENDENCY_DECLARATION = 3;
/**
* Constant value for faking boolean parameter false.
*/
private static final String FALSE = "false";
/**
* Constant value for faking boolean parameter true.
*/
private static final String TRUE = "true";
/**
* Inject needed Maven component.
*/
private final MavenSession mavenSession;
/**
* Include by project [groupId:]artifactId[:packagingType].
* Default value: all projects included.
*/
private List<String> includes;
/**
* Exclude by project [groupId:]artifactId[:packagingType].
* Default value: No projects excluded.
* If includes list contains any items, they are evaluated first.
* Then excludes are excluded from them.
*/
private List<String> excludes;
/**
* Error if unknown project in includes/excludes.
* If a wildcard (*) is used in the name, this parameter has no effect.
*/
@SuppressWarnings("unused") // Not actually unused. Set via Plexus/Sisu Container.
private String errorIfUnknownProject;
/**
* Include Maven root project.
*/
@SuppressWarnings("unused") // Not actually unused. Set via Plexus/Sisu Container.
private String includeRootProject;
/**
* Constructor.
*
* @param session Initialized MavenSession object.
*/
@Inject
@SuppressFBWarnings
public DependOnAllProjects(MavenSession session) {
this.mavenSession = Objects.requireNonNull(session);
}
/**
* Set includes.
* @param includes the includes
*/
@Inject
public void setIncludes(@Nullable List<String> includes) {
if (includes == null) {
this.includes = new ArrayList<>();
} else {
this.includes = new ArrayList<>(includes);
}
}
/**
* Set excludes.
* @param excludes the excludes
*/
@Inject
public void setExcludes(@Nullable List<String> excludes) {
if (excludes == null) {
this.excludes = new ArrayList<>();
} else {
this.excludes = new ArrayList<>(excludes);
}
}
/**
* Set errorIfUnknownProject.
* @param errorIfUnknownProject the errorIfUnknownProject
*/
@Inject
public void setErrorIfUnknownProject(String errorIfUnknownProject) {
if (errorIfUnknownProject != null) {
this.errorIfUnknownProject = errorIfUnknownProject;
} else {
this.errorIfUnknownProject = FALSE;
}
}
/**
* Set includeRootProject.
* @param includeRootProject the includeRootProject
*/
@Inject
public void setIncludeRootProject(String includeRootProject) {
if (includeRootProject != null) {
this.includeRootProject = includeRootProject;
} else {
this.includeRootProject = FALSE;
}
}
/**
* Format Dependency object to XML snippet.
* User can simply copy-paste this to the project.
* @param dependency Dependency object
* @param indent Indentation string, e.g. " "
* @return Formatted XML snippet
*/
public static String formatDependency(Dependency dependency, String indent) {
final String newLine = System.lineSeparator();
final StringBuilder sb = new StringBuilder();
sb.append("<dependency>").append(newLine);
sb.append(indent).append(String.format("<groupId>%s</groupId>", dependency.getGroupId()))
.append(newLine);
sb.append(indent)
.append(String.format("<artifactId>%s</artifactId>", dependency.getArtifactId()))
.append(newLine);
if (!"jar".equals(dependency.getType())) {
sb.append(indent).append(String.format("<type>%s</type>", dependency.getType()))
.append(newLine);
}
sb.append("</dependency>");
return sb.toString();
}
/**
* Convert string for matching.
*
* @param s String
* @return converted string
*/
public static String convertStringForMatching(String s) {
String t = s.replace(".", "\\.");
t = t.replace("*", ".*");
if (!t.contains(":")) {
t = ".*:" + t + ":.*";
}
if (Arrays.stream(t.split(":")).count() < MAX_NUM_PARTS_IN_DEPENDENCY_DECLARATION) {
t = t + ":.*";
}
return t;
}
/**
* Compare two MavenProject objects.
* GroupId, ArtifactId, Version and Packaging must match.
*
* @param a MavenProject a
* @param b MavenProject b
* @return true if projects are equal
*/
public static boolean projectsAreEquals(MavenProject a, MavenProject b) {
return a.getGroupId().equals(b.getGroupId())
&& a.getArtifactId().equals(b.getArtifactId()) && a.getVersion().equals(b.getVersion())
&& a.getPackaging().equals(b.getPackaging());
}
/**
* Compare two Dependency objects.
* GroupId, ArtifactId, Version and Type must match.
*
* @param a Dependency a
* @param b Dependency b
* @return true if dependencies are equal
*/
public static boolean dependenciesAreEquals(Dependency a, Dependency b) {
return a.getGroupId().equals(b.getGroupId())
&& a.getArtifactId().equals(b.getArtifactId()) && a.getVersion().equals(b.getVersion())
&& a.getType().equals(b.getType());
}
/**
* Does the list (Iterable) of Dependency objects contain the MavenProject?
*
* @param projects Iterable of Dependency objects
* @param project a Maven project object
* @return true if project is found
*/
public static boolean dependenciesContains(Iterable<Dependency> projects,
MavenProject project) {
for (Dependency p : projects) {
if (dependenciesAreEquals(p, projectToDependency(project))) {
return true;
}
}
return false;
}
/**
* Does any of the projects in Maven Reactor build match with this project name?
* Name must not have a wildcard!
*
* @param projects Iterable of MavenProject objects
* @param projectName Project name, e.g. "artifactId", "groupId:artifactId", "groupId:artifactId:packingType".
* @return Boolean
*/
public static boolean projectsContains(Iterable<MavenProject> projects, String projectName) {
List<String> ids = Arrays.asList(projectName.split(":"));
for (MavenProject project : projects) {
if (ids.size() == 1) {
if (project.getArtifactId().equals(ids.get(0))) {
return true;
}
} else if (ids.size() == 2) {
if (project.getGroupId().equals(ids.get(0))
&& project.getArtifactId().equals(ids.get(1))) {
return true;
}
} else {
assert ids.size() == MAX_NUM_PARTS_IN_DEPENDENCY_DECLARATION;
if (project.getGroupId().equals(ids.get(0))
&& project.getArtifactId().equals(ids.get(1))
&& project.getPackaging().equals(ids.get(2))) {
return true;
}
}
}
return false;
}
/**
* Convert MavenProject to Dependency object.
*
* @param mavenProject MavenProject object
* @return Dependency object
*/
public static Dependency projectToDependency(MavenProject mavenProject) {
Dependency d = new Dependency();
d.setGroupId(mavenProject.getGroupId());
d.setArtifactId(mavenProject.getArtifactId());
d.setVersion(mavenProject.getVersion());
d.setType(mavenProject.getPackaging());
return d;
}
/**
* Match projects with includes and excludes.
* Includes are * by default. Then excludes are excluded from the includes.
*
* @param includes Included projects. Default: *
* @param excludes Excluded projects. Default: none
* @param mavenProject Initialized MavenProject object.
* @return True or false.
*/
public static boolean isProjectIncluded(List<String> includes, List<String> excludes,
MavenProject mavenProject) {
String projectId =
String.format("%s:%s:%s", mavenProject.getGroupId(), mavenProject.getArtifactId(),
mavenProject.getPackaging());
// Match from the end of the id, artifactId alone is enough.
Predicate<String> predicateForProjectId =
s -> projectId.matches(convertStringForMatching(s));
return includes.stream().anyMatch(predicateForProjectId)
&& excludes.stream().noneMatch(predicateForProjectId);
}
/**
* Validate parameters provided via properties
* either on the command line or using configuration element in pom.
*
* @throws EnforcerRuleException if parameter validation fails.
*/
void validateAndPrepareParameters() throws EnforcerRuleException {
getLog().debug("includes=" + includes);
getLog().debug("excludes=" + excludes);
getLog().debug("errorIfUnknownProject=" + errorIfUnknownProject);
getLog().debug("includeRootProject=" + includeRootProject);
final List<MavenProject> reactorProjects =
mavenSession.getProjectDependencyGraph().getSortedProjects();
getLog().debug("reactorProjects=" + reactorProjects);
/* There is a bug in Maven/Sisu/Plexus container, which sets includes to a list with one empty string,
* if the parameter is not set. So we need to check for this case and convert it to an empty list.
* Then we can add the default value of "*".
*/
if ((includes == null) || (includes.size() == 1 && "".equals(includes.get(0)))) {
includes = new ArrayList<>();
}
if (excludes == null) {
excludes = new ArrayList<>();
} else if (excludes.size() == 1 && "".equals(excludes.get(0))) {
excludes = new ArrayList<>();
}
getLog().debug(String.format("Parameter includes.size: %d", includes.size()));
for (String a : includes) {
getLog().debug(String.format("Check include '%s'", a));
if (a == null) {
throw new EnforcerRuleException(
"Failure in parameter 'includes'. String is null");
}
if (a.matches("^[\t\n ]+$")) {
throw new EnforcerRuleException(
String.format("Failure in parameter 'includes'. String contains only whitespace: '%s'", a));
}
List<String> ids = Arrays.asList(a.split(":"));
if (ids.size() > MAX_NUM_PARTS_IN_DEPENDENCY_DECLARATION) {
throw new EnforcerRuleException(
"Failure in parameter 'includes'. String is invalid");
}
/* If there is a wildcard, we cannot check if the project exists in the build.
* So we skip the check in this case.
*/
if (TRUE.equals(errorIfUnknownProject) && !a.contains("*")
&& !projectsContains(reactorProjects, a)) {
throw new EnforcerRuleException(String.format(
"Failure in parameter 'includes'. Project '%s' not found in build", a));
}
}
if (includes.isEmpty()) {
includes.add("*");
}
getLog().debug(String.format("Parameter excludes.size: %d", excludes.size()));
for (String a : excludes) {
getLog().debug(String.format("Check exclude '%s'", a));
if (a == null) {
throw new EnforcerRuleException(
"Failure in parameter 'excludes'. String is null");
}
if (a.matches("^[\t\n ]+$")) {
throw new EnforcerRuleException(
String.format("Failure in parameter 'excludes'. String contains only whitespace: '%s'", a));
}
List<String> ids = Arrays.asList(a.split(":"));
if (ids.size() > MAX_NUM_PARTS_IN_DEPENDENCY_DECLARATION) {
throw new EnforcerRuleException(
"Failure in parameter 'excludes'. String is invalid");
}
/* If there is a wildcard, we cannot check if the project exists in the build.
* So we skip the check in this case.
*/
if (TRUE.equals(errorIfUnknownProject) && !a.contains("*")
&& !projectsContains(reactorProjects, a)) {
throw new EnforcerRuleException(String.format(
"Failure in parameter 'excludes'. Project '%s' not found in build", a));
}
}
if (errorIfUnknownProject == null || errorIfUnknownProject.isEmpty()) {
errorIfUnknownProject = FALSE;
}
if (includeRootProject == null || includeRootProject.isEmpty()) {
includeRootProject = FALSE;
}
if (!TRUE.equals(errorIfUnknownProject) && !FALSE.equals(errorIfUnknownProject)) {
throw new EnforcerRuleException(
String.format("Failure in parameter 'errorIfUnknownProject'. Must be 'true' or 'false': '%s'", errorIfUnknownProject));
}
if (!TRUE.equals(includeRootProject) && !FALSE.equals(includeRootProject)) {
throw new EnforcerRuleException(
String.format("Failure in parameter 'includeRootProject'. Must be 'true' or 'false': '%s'", includeRootProject));
}
getLog().debug("includes(resolved)=" + includes);
getLog().debug("excludes(resolved)=" + excludes);
getLog().debug("errorIfUnknownProject(resolved)=" + errorIfUnknownProject);
getLog().debug("includeRootProject(resolved)=" + includeRootProject);
}
/**
* The rule logic.
* Collect all projects in the build and filter according to includes/excludes.
* Match the list with the dependencies of the current project.
* If the two lists do not match, Raise EnforcerRuleException
*
* @throws EnforcerRuleException if rule fails.
*/
public void dependOnAllProjects() throws EnforcerRuleException {
List<MavenProject> includedProjects = new ArrayList<>();
MavenProject currentProject = mavenSession.getCurrentProject();
getLog().debug(String.format("Current Project: %s:%s", currentProject.getGroupId(),
currentProject.getArtifactId()));
getLog().debug("Iterate through all projects in Maven Dependency Graph, i.e. the build.");
mavenSession.getProjectDependencyGraph().getSortedProjects().forEach(project -> {
String projectId =
String.format("%s:%s:%s", project.getGroupId(), project.getArtifactId(),
project.getVersion());
getLog().debug(" " + projectId);
if (isIncluded(project)) {
if (projectsAreEquals(project, currentProject) || (!TRUE.equals(this.includeRootProject)
&& projectsAreEquals(project, mavenSession.getTopLevelProject()))) {
getLog().debug("Filter out project: "
+ String.format("%s:%s", project.getGroupId(), project.getArtifactId()));
} else {
includedProjects.add(project);
}
}
});
getLog().debug("includedProjects=%s" + includedProjects);
List<MavenProject> missingProjects = new ArrayList<>();
for (MavenProject project : includedProjects) {
@SuppressWarnings("unchecked") List<Dependency> dependencies =
currentProject.getDependencies();
if (!dependenciesContains(dependencies, project)) {
missingProjects.add(project);
}
}
if (!missingProjects.isEmpty()) {
List<String> errors = new ArrayList<>(missingProjects.size());
for (MavenProject missingProject : missingProjects) {
errors.add(String.format("Project '%s:%s' is missing dependency '%s:%s:%s'.",
currentProject.getGroupId(), currentProject.getArtifactId(),
missingProject.getGroupId(), missingProject.getArtifactId(),
missingProject.getPackaging()));
}
StringBuilder sb = new StringBuilder();
sb.append(String.format("Missing definitions from the project '%s:%s':",
currentProject.getGroupId(), currentProject.getArtifactId()));
sb.append(System.lineSeparator());
sb.append("<!-- Created by Maven Enforcer rule dependOnAllProjects --->");
sb.append(System.lineSeparator());
for (MavenProject missingProject : missingProjects) {
sb.append(formatDependency(projectToDependency(missingProject), INDENT_DEPENDENCY));
sb.append(System.lineSeparator());
}
sb.append("<!-- / Created by Maven Enforcer rule dependOnAllProjects --->");
errors.add(sb.toString());
throw new EnforcerRuleException(String.join("\n", errors));
}
getLog().debug("End of iterate");
}
/**
* The main entry point for rule.
*
* @throws EnforcerRuleException if rule fails.
*/
@Override
public void execute() throws EnforcerRuleException {
MavenProject currentProject = mavenSession.getCurrentProject();
getLog().debug(String.format("Current Project: %s:%s", currentProject.getGroupId(),
currentProject.getArtifactId()));
MavenProject topLevelProject = mavenSession.getTopLevelProject();
getLog().debug(String.format("Top Level Project: %s:%s", topLevelProject.getGroupId(),
topLevelProject.getArtifactId()));
validateAndPrepareParameters();
dependOnAllProjects();
}
/**
* String representation of the rule.
* Output is used in verbose Maven logs, can help during investigate problems.
*
* @return rule description
*/
@Override
public String toString() {
return String.format(
"DependOnAllProjects[includes=%s;excludes=%s;includeRootProject=%s;errorIfUnknownProject=%s]",
includes, excludes, includeRootProject, errorIfUnknownProject);
}
/**
* Decide if the project is included or excluded.
*
* @param mavenProject MavenProject
* @return true if included, false if excluded
*/
boolean isIncluded(MavenProject mavenProject) {
boolean r = isProjectIncluded(this.includes, this.excludes, mavenProject);
getLog().debug(String.format("isIncluded(%s:%s:%s:%s): %b", mavenProject.getGroupId(),
mavenProject.getArtifactId(), mavenProject.getVersion(), mavenProject.getPackaging(),
r));
return r;
}
}