diff --git a/pom.xml b/pom.xml index e11ebb7..9b4dd42 100644 --- a/pom.xml +++ b/pom.xml @@ -146,7 +146,6 @@ <configuration> <projectsDirectory>${basedir}/src/it</projectsDirectory> <cloneProjectsTo>${project.build.directory}/it</cloneProjectsTo> - <localRepositoryPath>${project.build.directory}/it-repo</localRepositoryPath> <mavenHome>${env.MAVEN_HOME}</mavenHome> <debug>true</debug> <ignoreFailures>true</ignoreFailures> diff --git a/src/it/ban-log4j-purge/pom.xml b/src/it/ban-log4j-purge/pom.xml new file mode 100644 index 0000000..5814e4d --- /dev/null +++ b/src/it/ban-log4j-purge/pom.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>com.inteligr8</groupId> + <artifactId>ban-maven-plugin-log4j-old</artifactId> + <version>@pom.version@</version> + <packaging>jar</packaging> + + <name>Log4j Ban Plugin Tests</name> + + <dependencies> + <dependency> + <groupId>log4j</groupId> + <artifactId>log4j</artifactId> + <version>1.2.17</version> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.11.0</version> + </plugin> + <plugin> + <groupId>${project.groupId}</groupId> + <artifactId>ban-maven-plugin</artifactId> + <version>@pom.version@</version> + <extensions>true</extensions> + <configuration> + <includes> + <artifact> + <groupId>log4j</groupId> + <artifactId>log4j</artifactId> + </artifact> + </includes> + <excludes> + <artifact>log4j:log.+:[1.2.17,)</artifact> + </excludes> + </configuration> + <executions> + <execution> + <id>purge</id> + <phase>prepare-package</phase> + <goals><goal>purge-repo</goal></goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project> diff --git a/src/main/java/com/inteligr8/maven/ban/BanConfigurationParser.java b/src/main/java/com/inteligr8/maven/ban/BanConfigurationParser.java index a2ac463..8972f13 100644 --- a/src/main/java/com/inteligr8/maven/ban/BanConfigurationParser.java +++ b/src/main/java/com/inteligr8/maven/ban/BanConfigurationParser.java @@ -21,20 +21,16 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; -import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; import org.apache.maven.artifact.versioning.VersionRange; import org.apache.maven.model.Plugin; import org.codehaus.plexus.util.xml.Xpp3Dom; -import org.eclipse.aether.artifact.Artifact; -import org.eclipse.aether.graph.DependencyFilter; -import org.eclipse.aether.graph.DependencyNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.inteligr8.maven.model.ArtifactFilter; -public class BanConfigurationParser implements DependencyFilter { +public class BanConfigurationParser { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final Pattern artifactPattern = Pattern.compile("^([^:]+):([^:]+)(:([^:]+))?$"); @@ -42,7 +38,6 @@ public class BanConfigurationParser implements DependencyFilter { private final List<ArtifactFilter> includeArtifacts; private final List<ArtifactFilter> excludeArtifacts; - private boolean failFast = false; public BanConfigurationParser(Plugin plugin) { Xpp3Dom rootDom = (Xpp3Dom) plugin.getConfiguration(); @@ -69,8 +64,12 @@ public class BanConfigurationParser implements DependencyFilter { } } - public void setFailFast(boolean failFast) { - this.failFast = failFast; + public List<ArtifactFilter> getIncludeArtifacts() { + return this.includeArtifacts; + } + + public List<ArtifactFilter> getExcludeArtifacts() { + return this.excludeArtifacts; } private List<ArtifactFilter> parseArtifacts(Xpp3Dom artifactsDom) { @@ -92,7 +91,7 @@ public class BanConfigurationParser implements DependencyFilter { filter.setGroupIdRegex(StringUtils.trimToNull(matcher.group(1))); } - if (this.notRegexPattern.matcher(matcher.group(1)).matches()) { + if (this.notRegexPattern.matcher(matcher.group(2)).matches()) { filter.setArtifactId(StringUtils.trimToNull(matcher.group(2))); } else { filter.setArtifactIdRegex(StringUtils.trimToNull(matcher.group(2))); @@ -129,57 +128,4 @@ public class BanConfigurationParser implements DependencyFilter { return childDom == null ? null : StringUtils.trimToNull(childDom.getValue()); } - @Override - public boolean accept(DependencyNode node, List<DependencyNode> parents) { - this.logger.debug("Evaluating dependency '{}'", node); - boolean ban = false; - - for (ArtifactFilter afilter : this.includeArtifacts) { - Artifact depArtifact = node.getArtifact(); - if (this.matches(afilter.getGroupId(), afilter.getGroupIdRegex(), depArtifact.getGroupId()) && - this.matches(afilter.getArtifactId(), afilter.getArtifactIdRegex(), depArtifact.getArtifactId()) && - this.withinRange(afilter.getVersionRange(), depArtifact.getVersion())) { - this.logger.debug("The dependency '{}' matches the ban inclusion filter", depArtifact); - ban = true; - break; - } - } - - if (!ban) - return false; - - for (ArtifactFilter afilter : this.excludeArtifacts) { - Artifact depArtifact = node.getArtifact(); - if (this.matches(afilter.getGroupId(), afilter.getGroupIdRegex(), depArtifact.getGroupId()) && - this.matches(afilter.getArtifactId(), afilter.getArtifactIdRegex(), depArtifact.getArtifactId()) && - this.withinRange(afilter.getVersionRange(), depArtifact.getVersion())) { - this.logger.debug("The dependency '{}' matches the ban exlusion filter", depArtifact); - return false; - } - } - - if (this.failFast) { - // plugin resolution downloads banned dependencies unless we fail now; not later - throw new RuntimeException("Banned dependency detected: " + node + " => " + parents); - } else { - return true; - } - } - - private boolean matches(String exactFilter, String regexFilter, String value) { - if (exactFilter == null && regexFilter == null) { - return true; - } else if (exactFilter != null) { - return exactFilter.equals(value); - } else { - Pattern filterPattern = Pattern.compile(regexFilter); - Matcher matcher = filterPattern.matcher(value); - return matcher.matches(); - } - } - - private boolean withinRange(VersionRange versionRange, String version) { - return versionRange == null || versionRange.containsVersion(new DefaultArtifactVersion(version)); - } - } diff --git a/src/main/java/com/inteligr8/maven/ban/BanDependencyFilter.java b/src/main/java/com/inteligr8/maven/ban/BanDependencyFilter.java new file mode 100644 index 0000000..883958d --- /dev/null +++ b/src/main/java/com/inteligr8/maven/ban/BanDependencyFilter.java @@ -0,0 +1,102 @@ +/* + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package com.inteligr8.maven.ban; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.apache.maven.artifact.versioning.VersionRange; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.graph.DependencyFilter; +import org.eclipse.aether.graph.DependencyNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.inteligr8.maven.model.ArtifactFilter; + +public class BanDependencyFilter implements DependencyFilter { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private final List<ArtifactFilter> includeArtifacts; + private final List<ArtifactFilter> excludeArtifacts; + private boolean failFast = false; + + public BanDependencyFilter(List<ArtifactFilter> includeArtifacts, List<ArtifactFilter> excludeArtifacts) { + this.includeArtifacts = includeArtifacts; + this.excludeArtifacts = excludeArtifacts; + } + + public void setFailFast(boolean failFast) { + this.failFast = failFast; + } + + @Override + public boolean accept(DependencyNode node, List<DependencyNode> parents) { + this.logger.debug("Evaluating dependency '{}'", node); + Artifact depArtifact = node.getArtifact(); + boolean ban = false; + + for (ArtifactFilter afilter : this.includeArtifacts) { + if (this.matches(afilter.getGroupId(), afilter.getGroupIdRegex(), depArtifact.getGroupId()) && + this.matches(afilter.getArtifactId(), afilter.getArtifactIdRegex(), depArtifact.getArtifactId()) && + this.withinRange(afilter.getVersionRange(), depArtifact.getVersion())) { + this.logger.debug("The dependency '{}' matches the ban inclusion filter", depArtifact); + ban = true; + break; + } + } + + if (!ban) + return false; + + for (ArtifactFilter afilter : this.excludeArtifacts) { + if (this.matches(afilter.getGroupId(), afilter.getGroupIdRegex(), depArtifact.getGroupId()) && + this.matches(afilter.getArtifactId(), afilter.getArtifactIdRegex(), depArtifact.getArtifactId()) && + this.withinRange(afilter.getVersionRange(), depArtifact.getVersion())) { + this.logger.debug("The dependency '{}' matches the ban exlusion filter", depArtifact); + return false; + } + } + + if (this.failFast) { + // plugin resolution downloads banned dependencies unless we fail now; not later + throw new RuntimeException("Banned dependency detected: " + node + " => " + parents); + } else { + return true; + } + } + + private boolean matches(String exactFilter, String regexFilter, String value) { + if (exactFilter == null && regexFilter == null) { + return true; + } else if (exactFilter != null) { + return exactFilter.equals(value); + } else { + Pattern filterPattern = Pattern.compile(regexFilter); + Matcher matcher = filterPattern.matcher(value); + return matcher.matches(); + } + } + + private boolean withinRange(VersionRange versionRange, String version) { + boolean within = versionRange == null || versionRange.containsVersion(new DefaultArtifactVersion(version)); + this.logger.debug("Tested version range: {} <=> {}: {}", versionRange, version, within); + return within; + } + +} diff --git a/src/main/java/com/inteligr8/maven/ban/BanExtension.java b/src/main/java/com/inteligr8/maven/ban/BanExtension.java index 30328f6..65579dc 100644 --- a/src/main/java/com/inteligr8/maven/ban/BanExtension.java +++ b/src/main/java/com/inteligr8/maven/ban/BanExtension.java @@ -44,9 +44,9 @@ import org.slf4j.LoggerFactory; @Singleton public class BanExtension extends AbstractMavenLifecycleParticipant { - private static final String THIS_PLUGIN_GROUP_ID = "com.inteligr8"; - private static final String THIS_PLUGIN_ARTIFACT_ID = "ban-maven-plugin"; - private static final String THIS_PLUGIN_KEY = THIS_PLUGIN_GROUP_ID + ":" + THIS_PLUGIN_ARTIFACT_ID; + public static final String THIS_PLUGIN_GROUP_ID = "com.inteligr8"; + public static final String THIS_PLUGIN_ARTIFACT_ID = "ban-maven-plugin"; + public static final String THIS_PLUGIN_KEY = THIS_PLUGIN_GROUP_ID + ":" + THIS_PLUGIN_ARTIFACT_ID; private Logger logger = LoggerFactory.getLogger(this.getClass()); @@ -62,15 +62,17 @@ public class BanExtension extends AbstractMavenLifecycleParticipant { BanConfigurationParser config = this.getConfiguration(project); if (config == null) return; - config.setFailFast(true); + + BanDependencyFilter depFilter = new BanDependencyFilter(config.getIncludeArtifacts(), config.getExcludeArtifacts()); + depFilter.setFailFast(true); try { for (Plugin plugin : project.getBuildPlugins()) { this.logger.debug("Evaluating plugin dependencies: {}", plugin); Artifact artifact = new DefaultArtifact(plugin.getId()); - DependencyNode depNodeRoot = this.pluginDepResolver.resolve(plugin, artifact, config, project.getRemotePluginRepositories(), session.getRepositorySession()); - List<Dependency> bannedDependencies = this.crawlDependencyTree(depNodeRoot, config); + DependencyNode depNodeRoot = this.pluginDepResolver.resolve(plugin, artifact, depFilter, project.getRemotePluginRepositories(), session.getRepositorySession()); + List<Dependency> bannedDependencies = this.crawlDependencyTree(depNodeRoot, depFilter); if (!bannedDependencies.isEmpty()) throw new MavenExecutionException("Banned dependencies were detected in plugin '" + plugin + "': " + bannedDependencies, project.getFile()); } @@ -78,10 +80,10 @@ public class BanExtension extends AbstractMavenLifecycleParticipant { throw new MavenExecutionException(pre.getMessage(), pre); } - config.setFailFast(false); + depFilter.setFailFast(false); DefaultDependencyResolutionRequest request = new DefaultDependencyResolutionRequest(project, session.getRepositorySession()); - request.setResolutionFilter(config); + request.setResolutionFilter(depFilter); try { DependencyResolutionResult result = this.projDepResolver.resolve(request); diff --git a/src/main/java/com/inteligr8/maven/ban/PurgeRepoMojo.java b/src/main/java/com/inteligr8/maven/ban/PurgeRepoMojo.java new file mode 100644 index 0000000..75a21d5 --- /dev/null +++ b/src/main/java/com/inteligr8/maven/ban/PurgeRepoMojo.java @@ -0,0 +1,263 @@ +/* + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package com.inteligr8.maven.ban; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.inject.Inject; + +import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.apache.maven.artifact.versioning.VersionRange; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Plugin; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.component.annotations.Component; + +import com.inteligr8.maven.model.ArtifactFilter; + +@Mojo( name = "purge-repo", threadSafe = true ) +@Component( role = org.apache.maven.plugin.Mojo.class ) +public class PurgeRepoMojo extends AbstractMojo { + + @Inject + private MavenSession session; + + @Parameter(name = "skip", defaultValue = "false") + private boolean skip = false; + + @Parameter(name = "dryRun", defaultValue = "false") + private boolean dryRun = false; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + if (this.skip) { + this.getLog().debug("Skipping purge of banned artifacts"); + } else { + this.getLog().info("Purging banned artifacts from local repository: " + this.session.getLocalRepository().getBasedir()); + + try { + this.purge(); + } catch (IOException ie) { + throw new MojoFailureException(ie.getMessage(), ie); + } + } + } + + private void purge() throws MojoFailureException, IOException { + List<Path> includePaths = new LinkedList<>(); + + BanConfigurationParser config = this.getConfiguration(this.session.getCurrentProject()); + + for (ArtifactFilter afilter : config.getIncludeArtifacts()) { + if (afilter.getGroupId() == null) { + this.getLog().warn("The purge does not support wildcard group ID specifications, so skipping it: " + afilter); + continue; + } + + Path groupPath = this.getGroupPath(afilter); + List<Path> artifactPaths = this.getArtifactPaths(groupPath, afilter); + for (Path artifactPath : artifactPaths) + includePaths.addAll(this.getVersionPaths(artifactPath, afilter.getVersionRange())); + } + + this.getLog().debug("May be purging all files in " + includePaths.size() + " paths"); + String regexDirectorySeparator = String.valueOf(File.separatorChar); + if (File.separatorChar == '\\') + regexDirectorySeparator += "\\"; + + for (ArtifactFilter afilter : config.getExcludeArtifacts()) { + StringBuilder regex = new StringBuilder(); + + if (afilter.getGroupId() != null) { + regex.append('^').append(this.getGroupPath(afilter)); + } else if (afilter.getGroupIdRegex() != null) { + regex.append(afilter.getGroupIdRegex().replace("\\.", regexDirectorySeparator)); + if (regex.charAt(0) != '^') + regex.insert(0, '^'); + } else { + regex.append("^.+"); + } + + regex.append(regexDirectorySeparator); + + if (afilter.getArtifactId() != null) { + regex.append(afilter.getArtifactId()); + } else if (afilter.getArtifactIdRegex() != null) { + regex.append(afilter.getArtifactIdRegex()); + } else { + regex.append("[^").append(regexDirectorySeparator).append("]+"); + } + + Pattern pattern = Pattern.compile(regex.toString()); + + Iterator<Path> i = includePaths.iterator(); + while (i.hasNext()) { + Path path = i.next(); + + Matcher matcher = pattern.matcher(path.toString()); + if (!matcher.find()) + continue; + + // group/artifact match; now for version + if (afilter.getVersionRange() == null) { + i.remove(); + } else { + String version = path.getFileName().toString(); + if (afilter.getVersionRange().containsVersion(new DefaultArtifactVersion(version))) + i.remove(); + } + } + } + + Path repoPath = this.getRepositoryPath(); + if (this.dryRun) { + this.getLog().info("DRYRUN: Would have deleted certain paths from local Maven cache: " + repoPath); + this.getLog().info("DRYRUN: Would have deleted these paths: " + includePaths); + } else { + for (Path path : includePaths) { + Path fullpath = repoPath.resolve(path); + if (Files.exists(fullpath)) { + this.getLog().info("Deleting version from Maven cache: " + path); + Files.walkFileTree(fullpath, new DeleteDirectoryVisitor()); + } else { + // this will probably never happen + this.getLog().debug("Maven cache does not exist: " + path); + } + } + } + } + + private BanConfigurationParser getConfiguration(MavenProject project) throws MojoFailureException { + Plugin plugin = project.getPlugin(BanExtension.THIS_PLUGIN_KEY); + if (plugin == null) + throw new MojoFailureException("The plugin is executing but it cannot be found"); + return new BanConfigurationParser(plugin); + } + + private Path getGroupPath(ArtifactFilter afilter) { + String[] pathElements = afilter.getGroupId().split("\\."); + Path groupPath = Paths.get(""); + for (String pathElement : pathElements) + groupPath = groupPath.resolve(pathElement); + return groupPath; + } + + private List<Path> getArtifactPaths(Path groupPath, ArtifactFilter afilter) throws IOException { + if (afilter.getArtifactId() != null) + return Arrays.asList(groupPath.resolve(afilter.getArtifactId())); + + Pattern artifactPattern = afilter.getArtifactIdRegex() == null ? null : Pattern.compile(afilter.getArtifactIdRegex()); + Path repoPath = this.getRepositoryPath(); + List<Path> paths = new LinkedList<>(); + + if (artifactPattern == null) + this.getLog().debug("All artifact directories in '" + groupPath + "' qualify as included"); + + Files.list(repoPath.resolve(groupPath)).forEach(new Consumer<Path>() { + @Override + public void accept(Path t) { + if (artifactPattern == null) { + paths.add(repoPath.relativize(t)); + } else { + Matcher matcher = artifactPattern.matcher(t.getFileName().toString()); + if (matcher.matches()) { + getLog().debug("The artifact directory '" + t.getFileName() + "' qualifies as included"); + paths.add(repoPath.relativize(t)); + } + } + } + }); + + return paths; + } + + private List<Path> getVersionPaths(Path artifactPath, VersionRange versionRange) throws IOException { + Path repoPath = this.getRepositoryPath(); + List<Path> paths = new LinkedList<>(); + + if (versionRange == null) + this.getLog().debug("All artifact version directories in '" + artifactPath + "' qualify as included"); + + Files.list(repoPath.resolve(artifactPath)).forEach(new Consumer<Path>() { + @Override + public void accept(Path t) { + if (versionRange == null) { + paths.add(repoPath.relativize(t)); + } else { + ArtifactVersion artifactVersion = new DefaultArtifactVersion(t.getFileName().toString()); + if (versionRange.containsVersion(artifactVersion)) { + getLog().debug("The artifact version directory '" + t.getFileName() + "' qualifies as included"); + paths.add(repoPath.relativize(t)); + } + } + } + }); + + return paths; + } + + private Path getRepositoryPath() { + return new File(this.session.getLocalRepository().getBasedir()).toPath(); + } + + + + private class DeleteDirectoryVisitor implements FileVisitor<Path> { + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + + } + +}