blob: c268052bc6274824f53074e5664cde67206fb609 [file] [log] [blame]
package org.apache.sling.maven.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.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import org.apache.maven.RepositoryUtils;
import org.apache.maven.enforcer.rule.api.EnforcerRule2;
import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.enforcer.AbstractNonCacheableEnforcerRule;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.utils.logging.MessageBuilder;
import org.apache.maven.shared.utils.logging.MessageUtils;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.collection.DependencyCollectionContext;
import org.eclipse.aether.collection.DependencySelector;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.graph.DependencyVisitor;
import org.eclipse.aether.graph.Exclusion;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResolutionException;
import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.util.graph.selector.AndDependencySelector;
import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector;
import org.eclipse.aether.util.graph.selector.OptionalDependencySelector;
import org.eclipse.aether.util.graph.selector.ScopeDependencySelector;
import org.eclipse.aether.util.graph.selector.StaticDependencySelector;
import org.eclipse.aether.util.graph.visitor.PathRecordingDependencyVisitor;
import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor;
/** Checks that the runtime classpath (e.g. used by Maven Plugins via the
* <a href="https://maven.apache.org/guides/mini/guide-maven-classloading.html#3-plugin-classloaders">Plugin Classloader</a>) contains all
* provided dependencies (also the transitive ones).
*
* As those are not transitively inherited they need to be declared explicitly in the pom.xml of the using Maven project.
*
* This check is useful to make sure that a Maven Plugin has access to all necessary classes at run time.
*/
public class RequireProvidedDependenciesInRuntimeClasspath
extends AbstractNonCacheableEnforcerRule implements EnforcerRule2 {
/**
* Specify the banned dependencies. This is a list of artifacts in format {@code <groupId>[:<artifactId>[:<extension>[:<classifier>]]]}.
* Excluded dependencies are not traversed, i.e. their transitive dependencies are not considered.
*
* @see {@link #setExcludes(List)} */
private List<String> excludes = null;
/**
* Whether to include optional dependencies in the check. Default = false.
*
* @see {@link #setIncludeOptionalDependencies(boolean)}
*/
private boolean includeOptionals = false;
/**
* Whether to include direct (i.e. non transitive) provided dependencies in the check. Default = false.
*
* @see {@link #setIncludeDirectDependencies(boolean)}
*/
private boolean includeDirects = false;
@SuppressWarnings("unchecked")
@Override
public void execute(@Nonnull EnforcerRuleHelper helper) throws EnforcerRuleException {
MavenProject project;
DefaultRepositorySystemSession newRepoSession;
RepositorySystem repoSystem;
List<RemoteRepository> remoteRepositories;
try {
project = (MavenProject) helper.evaluate("${project}");
// get a new session to be able to tweak the dependency selector
newRepoSession = new DefaultRepositorySystemSession(
(RepositorySystemSession) helper.evaluate("${repositorySystemSession}"));
remoteRepositories = (List<RemoteRepository>) helper.evaluate("${project.remoteProjectRepositories}");
repoSystem = helper.getComponent(RepositorySystem.class);
} catch (ExpressionEvaluationException eee) {
throw new EnforcerRuleException("Unable to retrieve Maven project or repository system sesssion", eee);
} catch (ComponentLookupException cle) {
throw new EnforcerRuleException("Unable to retrieve component RepositorySystem", cle);
}
Log log = helper.getLog();
Collection<DependencySelector> depSelectors = new ArrayList<>();
depSelectors.add(new ScopeDependencySelector("test")); // exclude transitive and direct "test" dependencies of the rootDependency (i.e. the current project)
// add also the exclude patterns
if (excludes != null && !excludes.isEmpty()) {
Collection<Exclusion> exclusions = excludes.stream().map(RequireProvidedDependenciesInRuntimeClasspath::convertPatternToExclusion).collect(Collectors.toCollection(LinkedList::new));
exclusions.add(new Exclusion("*", "*", "*", "pom"));
depSelectors.add(new ExclusionDependencySelector(exclusions));
}
if (!includeOptionals) {
depSelectors.add(new OptionalDependencySelector());
}
if (!includeDirects) {
depSelectors.add(new LevelAndScopeExclusionSelector(1, "provided"));
}
newRepoSession.setDependencySelector(new AndDependencySelector(depSelectors));
// use the ones for https://maven.apache.org/guides/mini/guide-maven-classloading.html#3-plugin-classloaders
@SuppressWarnings("deprecation")
List<org.eclipse.aether.artifact.Artifact> runtimeArtifacts = project.getRuntimeArtifacts().stream().map(RepositoryUtils::toArtifact).collect(Collectors.toList());
if (log.isDebugEnabled()) {
log.debug("Collected " + runtimeArtifacts.size()+ " runtime dependencies ");
for (Artifact runtimeArtifact : runtimeArtifacts) {
log.debug(runtimeArtifact.toString());
}
}
Dependency rootDependency = RepositoryUtils.toDependency(project.getArtifact(), null);
try {
Map<org.eclipse.aether.artifact.Artifact, List<List<DependencyNode>>> artifactMap = collectTransitiveDependencies(
rootDependency, repoSystem, newRepoSession, remoteRepositories, log);
if (log.isDebugEnabled()) {
log.debug("Collected " + artifactMap.size()+ " transitive dependencies: ");
for (Entry<org.eclipse.aether.artifact.Artifact, List<List<DependencyNode>>> artifactResult : artifactMap.entrySet()) {
log.debug(artifactResult.getKey().toString()
+ " (" + dumpPaths(artifactResult.getValue()) + ")");
}
}
int numViolations = checkForMissingArtifacts(artifactMap, runtimeArtifacts, log);
if (numViolations > 0) {
throw new EnforcerRuleException("Found " + numViolations + " missing runtime dependencies. Look at the errors emitted above for the details.");
}
} catch (DependencyResolutionException e) {
// draw graph
StringWriter writer = new StringWriter();
DependencyVisitor depVisitor = new TreeDependencyVisitor(
new DependencyVisitorPrinter(new PrintWriter(writer)));
e.getResult().getRoot().accept(depVisitor);
throw new EnforcerRuleException("Could not retrieve dependency metadata for project : "
+ e.getMessage() + ". Partial dependency tree: " + writer.toString(), e);
}
}
/**
*
* @param pattern string in in the format {@code <groupId>[:<artifactId>[:<extension>[:<classifier>]]]}
* @return the exclusion
*/
private static Exclusion convertPatternToExclusion(String pattern) {
String[] parts = pattern.split(":");
if (parts.length > 4) {
throw new IllegalArgumentException("Pattern must contain at most three colons, but contains " + parts + ": " + pattern);
}
String groupId = parts[0];
String artifactId = "*";
String extension = "*";
String classifier = "*";
if (parts.length > 1) {
artifactId = parts[1];
}
if (parts.length > 2) {
extension = parts[2];
}
if (parts.length > 3) {
classifier = parts[3];
}
return new Exclusion(groupId, artifactId, classifier, extension);
}
private static final class LevelAndScopeExclusionSelector implements DependencySelector {
private final int targetLevel;
private final String targetScope;
private final int currentLevel;
private static final DependencySelector ALL_SELECTOR = new StaticDependencySelector(true);
public LevelAndScopeExclusionSelector(int targetLevel, String targetScope) {
this(targetLevel, targetScope, 0);
}
private LevelAndScopeExclusionSelector(int targetLevel, String targetScope, int currentLevel) {
this.targetLevel = targetLevel;
this.targetScope = Objects.requireNonNull(targetScope);
this.currentLevel = currentLevel;
}
@Override
public boolean selectDependency(Dependency dependency) {
if (currentLevel == targetLevel) {
return !targetScope.equals(dependency.getScope());
}
return true;
}
@Override
public DependencySelector deriveChildSelector(DependencyCollectionContext context) {
if (currentLevel < targetLevel) {
return new LevelAndScopeExclusionSelector(targetLevel, targetScope, currentLevel+1);
} else {
// org.eclipse.aether:aether-util:jar:0.9.0.M2 used at runtime doesn't yet support null for no restrictions
return ALL_SELECTOR;
}
}
}
private static final class DependencyVisitorPrinter implements DependencyVisitor {
private final PrintWriter printWriter;
private String indent = "";
DependencyVisitorPrinter(PrintWriter printWriter) {
this.printWriter = printWriter;
}
@Override
public boolean visitEnter(DependencyNode dependencyNode) {
String scope;
if (dependencyNode.getDependency() != null) {
scope = " (" + dependencyNode.getDependency().getScope() + ")";
} else {
scope = "";
}
printWriter.println(indent + dependencyNode.getArtifact() + scope);
indent += " ";
return true;
}
@Override
public boolean visitLeave(DependencyNode dependencyNode) {
indent = indent.substring(0, indent.length() - 4);
return true;
}
}
protected Map<Artifact, List<List<DependencyNode>>> collectTransitiveDependencies(
org.eclipse.aether.graph.Dependency rootDependency,
RepositorySystem repoSystem, RepositorySystemSession repoSession,
List<RemoteRepository> remoteRepositories, Log log)
throws DependencyResolutionException {
CollectRequest collectRequest = new CollectRequest(rootDependency, remoteRepositories);
DependencyRequest req = new DependencyRequest(collectRequest, null);
DependencyResult resolutionResult = repoSystem.resolveDependencies(repoSession, req);
if (log.isDebugEnabled()) {
// draw full dependency graph
StringWriter writer = new StringWriter();
DependencyVisitor depVisitor = new TreeDependencyVisitor(
new DependencyVisitorPrinter(new PrintWriter(writer)));
resolutionResult.getRoot().accept(depVisitor);
log.debug("dependency tree: " + writer.toString());
}
// generate a map with key = artifact, value = all paths to it
return resolutionResult.getArtifactResults().stream()
.filter(a -> !areArtifactsEqualDisregardingVersion(a.getArtifact(), rootDependency.getArtifact())) // remove rootDependency itself
.collect(Collectors.toMap(ArtifactResult::getArtifact, a -> getPathsForDependency(resolutionResult.getRoot(), a.getArtifact())));
}
protected int checkForMissingArtifacts(Map<Artifact, List<List<DependencyNode>>> artifactMap, List<Artifact> runtimeArtifacts,
Log log) {
int numViolations = 0;
for (Entry<Artifact, List<List<DependencyNode>>> artifactResult : artifactMap.entrySet()) {
if (!isArtifactContainedInList(artifactResult.getKey(), runtimeArtifacts)) {
MessageBuilder msgBuilder = MessageUtils.buffer();
log.error(msgBuilder.a("Provided dependency ").strong(artifactResult.getKey()).mojo(" (" + dumpPaths(artifactResult.getValue()) + ")").a(" not found as runtime dependency!").toString());
numViolations++;
}
}
return numViolations;
}
private static String dumpPaths(List<List<DependencyNode>> paths) {
String via = paths.stream()
.map(RequireProvidedDependenciesInRuntimeClasspath::dumpPath)
.collect(Collectors.joining(" and "));
if (via.isEmpty()) {
return "direct";
} else {
return "via " + via;
}
}
private static String dumpPath(List<DependencyNode> path) {
if (path.size() <= 2) {
return "";
}
return path.stream()
.skip(1) // first entry is the project itself
.limit(path.size() - 2l) // last entry is the dependency (which is logged separately)
.map(n -> n.getArtifact().toString())
.collect(Collectors.joining(" -> "));
}
protected static boolean isArtifactContainedInList(Artifact artifact,
List<Artifact> artifacts) {
for (Artifact artifactInList : artifacts) {
if (areArtifactsEqualDisregardingVersion(artifact, artifactInList)) {
return true;
}
}
return false;
}
/**
*
* @param artifact1 one artifact
* @param artifact2 the other artifact
* @return {@code true} in case both artifact's groupId, artifactId, classifier and extension are equals, otherwise {@code false}
*/
protected static boolean areArtifactsEqualDisregardingVersion(Artifact artifact1, Artifact artifact2) {
return (artifact1.getArtifactId().equals(artifact2.getArtifactId())
&& artifact1.getGroupId().equals(artifact2.getGroupId())
&& Objects.toString(artifact2.getClassifier(), "").equals(Objects.toString(artifact1.getClassifier(), ""))
&& artifact1.getExtension().equals(artifact2.getExtension()));
}
private static List<List<DependencyNode>> getPathsForDependency(DependencyNode root, Artifact artifact) {
PathRecordingDependencyVisitor visitor = new PathRecordingDependencyVisitor(new SingleArtifactFilter(artifact));
root.accept(visitor);
return visitor.getPaths();
}
private static final class SingleArtifactFilter implements DependencyFilter {
private final org.eclipse.aether.artifact.Artifact artifact;
public SingleArtifactFilter(org.eclipse.aether.artifact.Artifact artifact) {
this.artifact = artifact;
}
@Override
public boolean accept(DependencyNode node, List<DependencyNode> parents) {
return node.getDependency().getArtifact().equals(artifact);
}
}
public void setExcludes(List<String> theExcludes) {
this.excludes = theExcludes;
}
public void setIncludeOptionalDependencies(boolean includeOptionals) {
this.includeOptionals = includeOptionals;
}
public void setIncludeDirectDependencies(boolean includeDirects) {
this.includeDirects = includeDirects;
}
}