org.apache.maven.plugins.dependency.PurgeLocalRepositoryMojo Maven / Gradle / Ivy
/*
* 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.
*/
package org.apache.maven.plugins.dependency;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecution.Source;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.artifact.filter.resolve.AbstractFilter;
import org.apache.maven.shared.artifact.filter.resolve.AndFilter;
import org.apache.maven.shared.artifact.filter.resolve.Node;
import org.apache.maven.shared.artifact.filter.resolve.PatternExclusionsFilter;
import org.apache.maven.shared.artifact.filter.resolve.PatternInclusionsFilter;
import org.apache.maven.shared.artifact.filter.resolve.ScopeFilter;
import org.apache.maven.shared.artifact.filter.resolve.TransformableFilter;
import org.apache.maven.shared.artifact.filter.resolve.transform.ArtifactIncludeFilterTransformer;
import org.apache.maven.shared.transfer.artifact.DefaultArtifactCoordinate;
import org.apache.maven.shared.transfer.artifact.TransferUtils;
import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolver;
import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolverException;
import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResult;
import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolver;
import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolverException;
import org.apache.maven.shared.utils.logging.MessageBuilder;
import org.apache.maven.shared.utils.logging.MessageUtils;
import org.codehaus.plexus.util.FileUtils;
/**
* When run on a project, remove the project dependencies from the local repository, and optionally re-resolve them.
* Outside of a project, remove the manually given dependencies.
*
* @author jdcasey
* @since 2.0
*/
@Mojo(name = "purge-local-repository", threadSafe = true, requiresProject = false)
public class PurgeLocalRepositoryMojo extends AbstractMojo {
private static final String VERSION_FUZZINESS = "version";
private static final String ARTIFACT_ID_FUZZINESS = "artifactId";
private static final String GROUP_ID_FUZZINESS = "groupId";
/**
* The Maven projects in the reactor.
*/
@Parameter(defaultValue = "${reactorProjects}", readonly = true, required = true)
private List reactorProjects;
/**
* The current Maven project.
*/
@Component
private MavenProject project;
@Component
private MavenSession session;
/**
* This mojo execution, used to determine if it was launched from the lifecycle or the command-line.
*/
@Parameter(defaultValue = "${mojo}", required = true, readonly = true)
private MojoExecution mojoExecution;
/**
* Artifact handler manager.
*/
@Component
private ArtifactHandlerManager artifactHandlerManager;
/**
* The list of dependencies in the form of groupId:artifactId which should BE deleted/purged from the local
* repository. Note that using this parameter will deactivate the normal process for purging the current project
* dependency tree. If this parameter is used, only the included artifacts will be purged. The manualIncludes
* parameter should not be used in combination with the includes/excludes parameters.
*
* @since 2.6
*/
@Parameter
private List manualIncludes;
/**
* Comma-separated list of groupId:artifactId entries, which should be used to manually include artifacts for
* deletion. This is a command-line alternative to the manualIncludes
parameter, since List parameters
* are not currently compatible with CLI specification.
*
* @since 2.6
*/
@Parameter(property = "manualInclude")
private String manualInclude;
/**
* The list of dependencies in the form of groupId:artifactId which should BE deleted/refreshed.
*
* @since 2.6
*/
@Parameter
private List includes;
/**
* Comma-separated list of groupId:artifactId entries, which should be used to include artifacts for
* deletion/refresh. This is a command-line alternative to the includes
parameter, since List
* parameters are not currently compatible with CLI specification.
*
* @since 2.6
*/
@Parameter(property = "include")
private String include;
/**
* The list of dependencies in the form of groupId:artifactId which should NOT be deleted/refreshed.
*/
@Parameter
private List excludes;
/**
* Comma-separated list of groupId:artifactId entries, which should be used to exclude artifacts from
* deletion/refresh. This is a command-line alternative to the excludes
parameter, since List
* parameters are not currently compatible with CLI specification.
*/
@Parameter(property = "exclude")
private String exclude;
/**
* Whether to re-resolve the artifacts once they have been deleted from the local repository. If you are running
* this mojo from the command-line, you may want to disable this. By default, artifacts will be re-resolved.
*/
@Parameter(property = "reResolve", defaultValue = "true")
private boolean reResolve;
/**
* The local repository, from which to delete artifacts.
*/
@Parameter(defaultValue = "${localRepository}", readonly = true, required = true)
private ArtifactRepository localRepository;
/**
* The dependency resolver
*/
@Component
private DependencyResolver dependencyResolver;
/**
* The artifact resolver used to re-resolve dependencies, if that option is enabled.
*/
@Component
private ArtifactResolver artifactResolver;
/**
* Determines how liberally the plugin will delete an artifact from the local repository. Values are:
*
* - file - Eliminate only the artifact's file.
* - version (default) - Eliminate all files associated with the version of the artifact.
* - artifactId - Eliminate all files associated with the artifact's artifactId.
* - groupId - Eliminate all files associated with the artifact's groupId.
*
*/
@Parameter(property = "resolutionFuzziness", defaultValue = "version")
private String resolutionFuzziness;
/**
* Whether this mojo should act on all transitive dependencies. Default value is true.
*/
@Parameter(property = "actTransitively", defaultValue = "true")
private boolean actTransitively;
/**
* Whether this plugin should output verbose messages. Default is false.
*/
@Parameter(property = "verbose", defaultValue = "false")
private boolean verbose;
/**
* Whether to purge only snapshot artifacts.
*
* @since 2.4
*/
@Parameter(property = "snapshotsOnly", defaultValue = "false")
private boolean snapshotsOnly;
/**
* Skip plugin execution completely.
*
* @since 2.7
*/
@Parameter(property = "skip", defaultValue = "false")
private boolean skip;
/**
* Includes only direct project dependencies.
*/
private class DirectDependencyFilter extends AbstractFilter {
private final Artifact projectArtifact;
private final List directDependencies;
/**
* Default constructor
*
* @param directDependencies Set of dependencies objects which represent the direct dependencies of the project
*/
DirectDependencyFilter(Artifact projectArtifact, List directDependencies) {
this.projectArtifact = projectArtifact;
this.directDependencies = directDependencies;
}
@Override
public boolean accept(Node node, List parents) {
if (artifactsGAMatch(node, projectArtifact.getGroupId(), projectArtifact.getArtifactId())) {
return true;
}
for (Dependency dep : directDependencies) {
if (this.artifactsGAMatch(node, dep.getGroupId(), dep.getArtifactId())) {
return true;
}
}
return false;
}
/*
* Compare the groupId:artifactId of two artifacts.
*/
private boolean artifactsGAMatch(Node node, String groupId, String artifactId) {
if (node.getDependency() == null) {
return false;
}
if (!node.getDependency().getGroupId().equals(groupId)) {
getLog().debug("Different groupId: " + node.getDependency() + " " + groupId);
return false;
}
if (!node.getDependency().getArtifactId().equals(artifactId)) {
getLog().debug("Different artifactId: " + node.getDependency() + " " + artifactId);
return false;
}
return true;
}
}
/**
* Includes only snapshot artifacts
*/
private static class SnapshotsFilter extends AbstractFilter {
@Override
public boolean accept(Node node, List parents) {
if (node.getDependency() == null) {
return false;
} else {
return ArtifactUtils.isSnapshot(node.getDependency().getVersion());
}
}
}
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (isSkip()) {
getLog().info("Skipping plugin execution");
return;
}
if (!(manualInclude == null || manualInclude.isEmpty())) {
manualIncludes = this.parseIncludes(manualInclude);
}
// If it's a manual purge, the only step is to delete from the local repo
if (manualIncludes != null && !manualIncludes.isEmpty()) {
manualPurge(manualIncludes);
return;
}
Set purgedArtifacts = new HashSet<>();
if (shouldPurgeAllProjectsInReactor()) {
for (MavenProject reactorProject : reactorProjects) {
purgeLocalRepository(reactorProject, purgedArtifacts);
}
} else {
purgeLocalRepository(project, purgedArtifacts);
}
}
/**
* Determines if all projects in the reactor should be purged from their dependencies. When this goal is started on
* the command-line, it is always the case. When it is bound to a phase in the lifecycle, it is never the case.
*
* @return true
if all projects in the reactor should be purged, false
otherwise.
*/
private boolean shouldPurgeAllProjectsInReactor() {
Source source = mojoExecution.getSource();
return reactorProjects.size() > 1 && source == Source.CLI;
}
/**
* Purges the local repository for the dependencies in the given Maven project.
*
* @param theProject Maven project.
* @param purgedArtifacts The artifacts that were already purged.
* @throws MojoFailureException in case of errors during the purge.
*/
private void purgeLocalRepository(MavenProject theProject, Set purgedArtifacts)
throws MojoFailureException {
List dependencies = theProject.getDependencies();
TransformableFilter dependencyFilter = createPurgeArtifactsFilter(theProject, dependencies, purgedArtifacts);
Set resolvedArtifactsToPurge =
getFilteredResolvedArtifacts(theProject, dependencies, dependencyFilter);
if (resolvedArtifactsToPurge.isEmpty()) {
getLog().info("No artifacts included for purge for project: " + getProjectKey(theProject));
return;
}
purgeArtifacts(theProject, resolvedArtifactsToPurge);
purgedArtifacts.addAll(resolvedArtifactsToPurge);
if (reResolve) {
getLog().info("Re-resolving dependencies");
try {
reResolveArtifacts(theProject, resolvedArtifactsToPurge);
} catch (ArtifactResolutionException e) {
String failureMessage = "Failed to refresh project dependencies for: " + theProject.getId();
throw new MojoFailureException(failureMessage, e);
}
}
}
/**
* Purge/Delete artifacts from the local repository according to the given patterns.
*
* @param theIncludes The includes.
* @throws MojoExecutionException in case of an error.
*/
private void manualPurge(List theIncludes) throws MojoExecutionException {
MessageBuilder messageBuilder = MessageUtils.buffer();
getLog().info(messageBuilder
.a("Deleting ")
.strong(theIncludes.size())
.a(" manual ")
.a(theIncludes.size() != 1 ? "dependencies" : "dependency")
.a(" from ")
.strong(localRepository.getBasedir())
.build());
for (String gavPattern : theIncludes) {
if (gavPattern == null || gavPattern.isEmpty()) {
getLog().debug("Skipping empty gav pattern");
continue;
}
String relativePath = gavToPath(gavPattern);
if (relativePath == null || relativePath.isEmpty()) {
getLog().debug("Skipping empty relative path for gav pattern: " + gavPattern);
continue;
}
File purgeDir = new File(localRepository.getBasedir(), relativePath);
if (purgeDir.exists()) {
getLog().debug("Deleting directory: " + purgeDir);
try {
FileUtils.deleteDirectory(purgeDir);
} catch (IOException e) {
throw new MojoExecutionException("Unable to purge directory: " + purgeDir);
}
} else {
getLog().debug("Directory: " + purgeDir + " doesn't exist");
}
}
}
/**
* Convert a groupId:artifactId:version to a file system path
*
* @param gav the groupId:artifactId:version string
* @return the corresponding path
*/
private String gavToPath(String gav) {
if (gav == null || gav.isEmpty()) {
return null;
}
String[] pathComponents = gav.split(":");
StringBuilder path = new StringBuilder(pathComponents[0].replace('.', '/'));
for (int i = 1; i < pathComponents.length; ++i) {
path.append("/").append(pathComponents[i]);
}
return path.toString();
}
/**
* Create the includes exclude filter to use when resolving and purging dependencies Also excludes any "system"
* scope dependencies
*
* @param theProject The Maven project.
* @param dependencies The dependencies to use as a reference if we're excluding transitive dependencies
* @param purgedArtifacts The artifacts already purged.
* @return the created filter
*/
private TransformableFilter createPurgeArtifactsFilter(
MavenProject theProject, List dependencies, Set purgedArtifacts) {
List subFilters = new ArrayList<>();
// System dependencies should never be purged
subFilters.add(ScopeFilter.excluding(Artifact.SCOPE_SYSTEM));
if (this.snapshotsOnly) {
subFilters.add(new SnapshotsFilter());
}
// The CLI includes/excludes overrides configuration in the pom
if (!(this.include == null || this.include.isEmpty())) {
this.includes = parseIncludes(this.include);
}
if (this.includes != null) {
subFilters.add(new PatternInclusionsFilter(includes));
}
if (!(this.exclude == null || this.exclude.isEmpty())) {
this.excludes = parseIncludes(this.exclude);
}
if (this.excludes != null) {
subFilters.add(new PatternExclusionsFilter(excludes));
}
if (!actTransitively) {
subFilters.add(new DirectDependencyFilter(theProject.getArtifact(), dependencies));
}
List exclusions = new ArrayList<>(reactorProjects.size());
// It doesn't make sense to include projects from the reactor here since they're likely not able to be resolved
for (MavenProject reactorProject : reactorProjects) {
exclusions.add(toPatternExcludes(reactorProject.getArtifact()));
}
// There is no need to consider a second time artifacts that were already purged (re-resolved or not)
for (Artifact purgedArtifact : purgedArtifacts) {
exclusions.add(toPatternExcludes(purgedArtifact));
}
subFilters.add(new PatternExclusionsFilter(exclusions));
return new AndFilter(subFilters);
}
/**
* Returns a string that represents a pattern for an exclude filter for the given artifact.
*
* @param artifact Artifact.
* @return String representation of a pattern for an exclude filter for the given artifact.
*/
private String toPatternExcludes(Artifact artifact) {
return artifact.getGroupId() + ":" + artifact.getArtifactId() + ":"
+ artifact.getArtifactHandler().getExtension() + ":" + artifact.getVersion();
}
/**
* Convert comma separated list of includes to List object
*
* @param theInclude The list of includes
* @return the includes list
*/
private List parseIncludes(String theInclude) {
List theIncludes = new ArrayList<>();
if (theInclude != null) {
String[] elements = theInclude.split(",");
theIncludes.addAll(Arrays.asList(elements));
}
return theIncludes;
}
private Set getFilteredResolvedArtifacts(
MavenProject theProject, List dependencies, TransformableFilter filter) {
try {
Iterable results = dependencyResolver.resolveDependencies(
session.getProjectBuildingRequest(), theProject.getModel(), filter);
Set resolvedArtifacts = new LinkedHashSet<>();
for (ArtifactResult artResult : results) {
resolvedArtifacts.add(artResult.getArtifact());
}
return resolvedArtifacts;
} catch (DependencyResolverException e) {
getLog().info("Unable to resolve all dependencies for: " + getProjectKey(theProject)
+ ". Falling back to non-transitive mode for initial artifact resolution.");
}
Set resolvedArtifacts = new LinkedHashSet<>();
ArtifactFilter artifactFilter = filter.transform(new ArtifactIncludeFilterTransformer());
for (Dependency dependency : dependencies) {
DefaultArtifactCoordinate coordinate = new DefaultArtifactCoordinate();
coordinate.setGroupId(dependency.getGroupId());
coordinate.setArtifactId(dependency.getArtifactId());
coordinate.setVersion(dependency.getVersion());
coordinate.setExtension(artifactHandlerManager
.getArtifactHandler(dependency.getType())
.getExtension());
try {
Artifact artifact = artifactResolver
.resolveArtifact(session.getProjectBuildingRequest(), coordinate)
.getArtifact();
if (artifactFilter.include(artifact)) {
resolvedArtifacts.add(artifact);
}
} catch (ArtifactResolverException e) {
getLog().debug("Unable to resolve artifact: " + coordinate);
}
}
return resolvedArtifacts;
}
private void purgeArtifacts(MavenProject theProject, Set artifacts) {
MessageBuilder messageBuilder = MessageUtils.buffer();
getLog().info(messageBuilder
.a("Deleting ")
.strong(artifacts.size())
.a(" ")
.strong(actTransitively ? "transitive" : "direct")
.a(artifacts.size() != 1 ? " dependencies" : " dependency")
.a(" for project ")
.strong(getProjectKey(theProject))
.a(" from ")
.strong(localRepository.getBasedir())
.a(" with artifact ")
.strong(resolutionFuzziness)
.a(" resolution fuzziness")
.build());
for (Artifact artifact : artifacts) {
verbose("Purging artifact: " + artifact.getId());
File deleteTarget = findDeleteTarget(artifact);
verbose("Deleting: " + deleteTarget);
if (deleteTarget.isDirectory()) {
try {
FileUtils.deleteDirectory(deleteTarget);
} catch (IOException e) {
getLog().warn("Unable to purge local repository location: " + deleteTarget, e);
}
} else {
if (!deleteTarget.delete()) {
deleteTarget.deleteOnExit();
getLog().warn("Unable to purge local repository location immediately: " + deleteTarget);
}
}
artifact.setResolved(false);
}
}
private void reResolveArtifacts(MavenProject theProject, Set artifacts)
throws ArtifactResolutionException {
// Always need to re-resolve the poms in case they were purged along with the artifact
// because Maven 2 will not automatically re-resolve them when resolving the artifact
for (Artifact artifact : artifacts) {
verbose("Resolving artifact: " + artifact.getId());
try {
// CHECKSTYLE_OFF: LineLength
artifactResolver.resolveArtifact(
session.getProjectBuildingRequest(), TransferUtils.toArtifactCoordinate(artifact));
// CHECKSTYLE_ON: LineLength
} catch (ArtifactResolverException e) {
verbose(e.getMessage());
}
}
List missingArtifacts = new ArrayList<>();
for (Artifact artifact : artifacts) {
try {
artifactResolver.resolveArtifact(session.getProjectBuildingRequest(), artifact);
} catch (ArtifactResolverException e) {
verbose(e.getMessage());
missingArtifacts.add(artifact);
}
}
if (!missingArtifacts.isEmpty()) {
StringBuilder message = new StringBuilder("required artifacts missing:");
message.append(System.lineSeparator());
for (Artifact missingArtifact : missingArtifacts) {
message.append(" ").append(missingArtifact.getId()).append(System.lineSeparator());
}
message.append(System.lineSeparator());
message.append("for the artifact:");
throw new ArtifactResolutionException(
message.toString(), theProject.getArtifact(), theProject.getRemoteArtifactRepositories());
}
}
private File findDeleteTarget(Artifact artifact) {
// Use localRepository.pathOf() in case artifact.getFile() is not set
File deleteTarget = new File(localRepository.getBasedir(), localRepository.pathOf(artifact));
if (GROUP_ID_FUZZINESS.equals(resolutionFuzziness)) {
// get the groupId dir.
deleteTarget = deleteTarget.getParentFile().getParentFile().getParentFile();
} else if (ARTIFACT_ID_FUZZINESS.equals(resolutionFuzziness)) {
// get the artifactId dir.
deleteTarget = deleteTarget.getParentFile().getParentFile();
} else if (VERSION_FUZZINESS.equals(resolutionFuzziness)) {
// get the version dir.
deleteTarget = deleteTarget.getParentFile();
}
// else it's file fuzziness.
return deleteTarget;
}
private void verbose(String message) {
if (verbose || getLog().isDebugEnabled()) {
getLog().info(message);
}
}
private String getProjectKey(MavenProject project) {
return project.getArtifactId();
}
/**
* @return {@link #skip}
*/
public boolean isSkip() {
return skip;
}
/**
* @param skip {@link #skip}
*/
public void setSkip(boolean skip) {
this.skip = skip;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy