},
+ * with version inference from managed dependencies.
+ *
+ * If the dependency already exists, the goal fails with a descriptive error
+ * directing the user to remove it first.
+ *
+ * The goal uses formatting-preserving DOM manipulation to maintain the POM's
+ * existing structure (comments, indentation, encoding). Duplicate detection uses
+ * type and classifier-aware matching, and cross-references Maven's resolved model
+ * to catch dependencies declared via property references.
+ *
+ * Scope values are validated against Maven's known scopes:
+ * {@code compile}, {@code provided}, {@code runtime}, {@code test}, {@code system}, {@code import}.
+ *
+ * @since 3.11.0
+ */
+@Mojo(name = "add", requiresProject = true, threadSafe = true)
+public class AddDependencyMojo extends AbstractDependencyMojo {
+
+ /**
+ * Dependency coordinates: {@code groupId:artifactId[:version]}
+ * or {@code groupId:artifactId[:extension[:classifier]]:version}.
+ * Scope, type, classifier, and optional can be overridden via separate parameters.
+ */
+ @Parameter(property = "gav")
+ private String gav;
+
+ /**
+ * Dependency scope. Validated against Maven's known scope values:
+ * {@code compile}, {@code provided}, {@code runtime}, {@code test}, {@code system}, {@code import}.
+ * Invalid values are rejected with a {@link org.apache.maven.plugin.MojoFailureException}.
+ */
+ @Parameter(property = "scope")
+ private String scope;
+
+ /**
+ * Dependency type/packaging (e.g., {@code jar}, {@code pom}, {@code war}).
+ */
+ @Parameter(property = "type")
+ private String type;
+
+ /**
+ * Dependency classifier (e.g., {@code sources}, {@code javadoc}, {@code tests}).
+ */
+ @Parameter(property = "classifier")
+ private String classifier;
+
+ /**
+ * Whether the dependency is optional.
+ */
+ @Parameter(property = "optional")
+ private Boolean optional;
+
+ /**
+ * When {@code true}, insert into {@code } instead of {@code }.
+ */
+ @Parameter(property = "managed", defaultValue = "false")
+ private boolean managed;
+
+ /**
+ * When {@code true} (the default), automatically detect and follow the project's existing
+ * dependency management conventions:
+ *
+ * - If most existing dependencies are version-less, add managed dependency to parent POM
+ * - If versions use {@code ${...}} property references, create a version property
+ * - Property naming follows the detected pattern (e.g., {@code .version}, {@code -version})
+ *
+ * Explicit parameters ({@code managed}, {@code useProperty}, {@code propertyName}) override
+ * detected conventions.
+ *
+ * @since 3.11.0
+ */
+ @Parameter(property = "align", defaultValue = "true")
+ private boolean align;
+
+ /**
+ * When set, controls whether a version property is created in {@code }.
+ * When {@code null} (the default), the behavior is auto-detected from existing conventions
+ * if {@code align=true}.
+ *
+ * @since 3.11.0
+ */
+ @Parameter(property = "useProperty")
+ private Boolean useProperty;
+
+ /**
+ * Explicit property name for the version (e.g., {@code guava.version}).
+ * When not set, the name is derived from the detected naming convention.
+ *
+ * @since 3.11.0
+ */
+ @Parameter(property = "propertyName")
+ private String propertyName;
+
+ /**
+ * Target a specific Maven profile by its {@code }. When set, the dependency is added
+ * to the profile's {@code } or {@code } section.
+ * The profile must already exist in the POM.
+ */
+ @Parameter(property = "profile")
+ private String profile;
+
+ @Inject
+ public AddDependencyMojo(MavenSession session, BuildContext buildContext, MavenProject project) {
+ super(session, buildContext, project);
+ }
+
+ @Override
+ protected void doExecute() throws MojoExecutionException, MojoFailureException {
+ DependencyEntry coords = resolveCoordinates();
+
+ MavenProject targetProject = getProject();
+ boolean effectiveManaged = managed;
+
+ // Convention auto-detection when align=true
+ Conventions conventions = null;
+ if (align && coords.getVersion() != null) {
+ conventions = detectConventions(targetProject);
+ if (!managed) {
+ effectiveManaged = conventions.useManaged;
+ }
+ }
+
+ // Validate version requirements
+ if (effectiveManaged && coords.getVersion() == null) {
+ throw new MojoFailureException("Version is required when adding to ."
+ + " Include version in -Dgav (e.g. groupId:artifactId:version)");
+ }
+
+ // Version inference for non-managed dependencies
+ if (!effectiveManaged && coords.getVersion() == null) {
+ String managedVersion = findManagedVersion(targetProject, coords.getGroupId(), coords.getArtifactId());
+ if (managedVersion != null) {
+ getLog().info("Version managed by parent: " + coords.getGroupId() + ":" + coords.getArtifactId() + ":"
+ + managedVersion);
+ } else {
+ throw new MojoFailureException("No version specified and no managed version found for "
+ + coords.getGroupId() + ":" + coords.getArtifactId()
+ + ". Include version in -Dgav (e.g. groupId:artifactId:version)");
+ }
+ }
+
+ // Determine effective property usage
+ boolean effectiveUseProperty = false;
+ String effectivePropertyName = null;
+ if (conventions != null && coords.getVersion() != null) {
+ effectiveUseProperty = useProperty != null ? useProperty : conventions.useProperty;
+ if (effectiveUseProperty) {
+ effectivePropertyName = propertyName != null ? propertyName : conventions.derivePropertyName(coords);
+ }
+ }
+
+ // Determine the target POM for managed deps
+ File managedPomFile = null;
+ if (effectiveManaged && conventions != null && conventions.managedPomFile != null) {
+ managedPomFile = conventions.managedPomFile;
+ }
+
+ // Cross-POM mode: add managed dep + property to parent, version-less dep to child
+ File pomFile = targetProject.getFile();
+ if (pomFile == null) {
+ throw new MojoExecutionException("Cannot add dependency: project has no POM file to modify.");
+ }
+
+ boolean crossPom = effectiveManaged
+ && managedPomFile != null
+ && !managedPomFile.getAbsolutePath().equals(pomFile.getAbsolutePath());
+
+ try {
+ if (crossPom) {
+ addCrossPom(coords, pomFile, managedPomFile, effectiveUseProperty, effectivePropertyName);
+ } else {
+ addSinglePom(
+ coords, targetProject, pomFile, effectiveManaged, effectiveUseProperty, effectivePropertyName);
+ }
+ } catch (IOException e) {
+ throw new MojoExecutionException("Failed to modify POM file: " + pomFile, e);
+ }
+ }
+
+ /**
+ * Cross-POM mode: adds a managed dependency (with optional property) to the parent POM,
+ * and a version-less dependency reference to the child POM.
+ */
+ private void addCrossPom(
+ DependencyEntry coords,
+ File childPomFile,
+ File parentPomFile,
+ boolean effectiveUseProperty,
+ String effectivePropertyName)
+ throws IOException, MojoFailureException {
+ // 1. Modify parent POM: add property + managed dependency
+ PomEditor parentEditor = loadPomEditor(parentPomFile);
+ Element existingManaged = findDependency(
+ parentEditor,
+ null,
+ coords.getGroupId(),
+ coords.getArtifactId(),
+ coords.getType(),
+ coords.getClassifier(),
+ true);
+ if (existingManaged != null) {
+ throw new MojoFailureException("Dependency " + coords.getGroupId() + ":" + coords.getArtifactId()
+ + " already exists in " + parentPomFile.getName() + " ."
+ + " Remove it first with dependency:remove, then re-add.");
+ }
+
+ String versionRef = coords.getVersion();
+ if (effectiveUseProperty && effectivePropertyName != null) {
+ parentEditor.properties().updateProperty(true, effectivePropertyName, coords.getVersion());
+ versionRef = "${" + effectivePropertyName + "}";
+ getLog().info("Added property " + effectivePropertyName + "=" + coords.getVersion() + " to "
+ + parentPomFile.getName());
+ }
+
+ DependencyEntry managedCoords = new DependencyEntry(coords.getGroupId(), coords.getArtifactId());
+ managedCoords.setVersion(versionRef);
+ if (coords.getType() != null && !coords.getType().isEmpty()) {
+ managedCoords.setType(coords.getType());
+ }
+ if (coords.getClassifier() != null && !coords.getClassifier().isEmpty()) {
+ managedCoords.setClassifier(coords.getClassifier());
+ }
+ addDependency(parentEditor, parentPomFile, null, managedCoords, true);
+ savePomEditor(parentEditor, parentPomFile);
+ getLog().info("Added managed dependency " + coords.getGroupId() + ":" + coords.getArtifactId() + ":"
+ + versionRef + " to " + parentPomFile.getName());
+
+ // 2. Modify child POM: add version-less dependency
+ PomEditor childEditor = loadPomEditor(childPomFile);
+ Element existingChild = findDependency(
+ childEditor,
+ profile,
+ coords.getGroupId(),
+ coords.getArtifactId(),
+ coords.getType(),
+ coords.getClassifier(),
+ false);
+ if (existingChild != null) {
+ throw new MojoFailureException("Dependency " + coords.getGroupId() + ":" + coords.getArtifactId()
+ + " already exists in " + childPomFile.getName()
+ + ". Remove it first with dependency:remove, then re-add.");
+ } else if (existsInResolvedModel(getProject(), coords, false)) {
+ throw new MojoFailureException("Dependency " + coords.getGroupId() + ":" + coords.getArtifactId()
+ + " already exists in the POM (using property references). "
+ + "Cannot safely add or update automatically. Please edit the POM manually.");
+ }
+
+ DependencyEntry childCoords = new DependencyEntry(coords.getGroupId(), coords.getArtifactId());
+ if (coords.getScope() != null && !coords.getScope().isEmpty()) {
+ childCoords.setScope(coords.getScope());
+ }
+ if (coords.getType() != null && !coords.getType().isEmpty()) {
+ childCoords.setType(coords.getType());
+ }
+ if (coords.getClassifier() != null && !coords.getClassifier().isEmpty()) {
+ childCoords.setClassifier(coords.getClassifier());
+ }
+ if (coords.getOptional() != null) {
+ childCoords.setOptional(coords.getOptional());
+ }
+ addDependency(childEditor, childPomFile, profile, childCoords, false);
+ savePomEditor(childEditor, childPomFile);
+ getLog().info("Added version-less dependency " + coords.getGroupId() + ":" + coords.getArtifactId() + " to "
+ + childPomFile.getName());
+
+ // Sync in-memory model
+ syncInMemoryModel(getProject(), coords, false, true);
+ }
+
+ /**
+ * Single-POM mode: adds the dependency (optionally with a version property) to the current POM.
+ */
+ private void addSinglePom(
+ DependencyEntry coords,
+ MavenProject targetProject,
+ File pomFile,
+ boolean targetManaged,
+ boolean effectiveUseProperty,
+ String effectivePropertyName)
+ throws IOException, MojoFailureException {
+
+ // Warn when adding to parent (not managed) in a multi-module project
+ if (!targetManaged
+ && targetProject.getModules() != null
+ && !targetProject.getModules().isEmpty()) {
+ getLog().warn("Adding dependency to parent POM — this will be inherited by all child modules. "
+ + "Use -Dmanaged to add to instead.");
+ }
+
+ PomEditor editor = loadPomEditor(pomFile);
+ Element existing = findDependency(
+ editor,
+ profile,
+ coords.getGroupId(),
+ coords.getArtifactId(),
+ coords.getType(),
+ coords.getClassifier(),
+ targetManaged);
+
+ if (existing != null) {
+ throw new MojoFailureException("Dependency " + coords.getGroupId() + ":" + coords.getArtifactId()
+ + " already exists in " + pomFile.getName()
+ + ". Remove it first with dependency:remove, then re-add.");
+ } else if (existsInResolvedModel(targetProject, coords, targetManaged)) {
+ throw new MojoFailureException("Dependency " + coords.getGroupId() + ":" + coords.getArtifactId()
+ + " already exists in the POM (using property references). "
+ + "Cannot safely add or update automatically. Please edit the POM manually.");
+ } else {
+ // Handle version property
+ if (effectiveUseProperty && effectivePropertyName != null && coords.getVersion() != null) {
+ editor.properties().updateProperty(true, effectivePropertyName, coords.getVersion());
+ getLog().info("Added property " + effectivePropertyName + "=" + coords.getVersion());
+ coords.setVersion("${" + effectivePropertyName + "}");
+ }
+ addDependency(editor, pomFile, profile, coords, targetManaged);
+ getLog().info("Added dependency " + coords + " to " + pomFile.getName());
+ }
+
+ savePomEditor(editor, pomFile);
+
+ syncInMemoryModel(targetProject, coords, targetManaged, false);
+ }
+
+ /**
+ * Syncs the in-memory Maven model after POM modifications.
+ */
+ private void syncInMemoryModel(
+ MavenProject targetProject, DependencyEntry coords, boolean targetManaged, boolean versionless) {
+ Model model = targetProject.getModel();
+ if (model != null) {
+ Dependency modelDep = new Dependency();
+ modelDep.setGroupId(coords.getGroupId());
+ modelDep.setArtifactId(coords.getArtifactId());
+ if (!versionless && coords.getVersion() != null) {
+ modelDep.setVersion(coords.getVersion());
+ }
+ if (coords.getScope() != null && !coords.getScope().isEmpty()) {
+ modelDep.setScope(coords.getScope());
+ }
+ if (coords.getType() != null && !coords.getType().isEmpty()) {
+ modelDep.setType(coords.getType());
+ }
+ if (coords.getClassifier() != null && !coords.getClassifier().isEmpty()) {
+ modelDep.setClassifier(coords.getClassifier());
+ }
+ if (coords.getOptional() != null) {
+ modelDep.setOptional(String.valueOf(coords.getOptional()));
+ }
+ if (targetManaged) {
+ DependencyManagement dm = model.getDependencyManagement();
+ if (dm == null) {
+ dm = new DependencyManagement();
+ model.setDependencyManagement(dm);
+ }
+ dm.addDependency(modelDep);
+ } else {
+ model.addDependency(modelDep);
+ }
+ }
+ }
+
+ private DependencyEntry resolveCoordinates() throws MojoFailureException {
+ if (gav == null || gav.isEmpty()) {
+ throw new MojoFailureException("You must specify -Dgav=groupId:artifactId[:version]");
+ }
+
+ DependencyEntry coords;
+ try {
+ coords = DependencyEntry.parse(gav);
+ } catch (IllegalArgumentException e) {
+ throw new MojoFailureException(e.getMessage());
+ }
+
+ // Explicit parameters override GAV shorthand values
+ if (scope != null) {
+ coords.setScope(scope);
+ }
+ if (type != null) {
+ coords.setType(type);
+ }
+ if (classifier != null) {
+ coords.setClassifier(classifier);
+ }
+
+ if (optional != null) {
+ coords.setOptional(optional);
+ }
+
+ try {
+ coords.validate();
+ } catch (IllegalArgumentException e) {
+ throw new MojoFailureException(e.getMessage());
+ }
+
+ return coords;
+ }
+
+ private String findManagedVersion(MavenProject project, String groupId, String artifactId) {
+ MavenProject current = project;
+ while (current != null) {
+ DependencyManagement depMgmt = current.getDependencyManagement();
+ if (depMgmt != null && depMgmt.getDependencies() != null) {
+ for (Dependency dep : depMgmt.getDependencies()) {
+ if (groupId.equals(dep.getGroupId()) && artifactId.equals(dep.getArtifactId())) {
+ return dep.getVersion();
+ }
+ }
+ }
+ current = current.getParent();
+ }
+ return null;
+ }
+
+ private void addDependency(
+ PomEditor editor, File pomFile, String profileId, DependencyEntry coords, boolean managed)
+ throws MojoFailureException {
+ PomEditor.Dependencies dependencies = dependenciesFor(editor, pomFile, profileId);
+ Coordinates coordinates = Coordinates.of(
+ coords.getGroupId(),
+ coords.getArtifactId(),
+ coords.getVersion(),
+ coords.getClassifier(),
+ coords.getType());
+
+ if (managed) {
+ dependencies.updateManagedDependency(true, coordinates);
+ } else {
+ dependencies.updateDependency(true, coordinates);
+ }
+
+ Element dependency = findDependency(
+ editor,
+ profileId,
+ coords.getGroupId(),
+ coords.getArtifactId(),
+ coords.getType(),
+ coords.getClassifier(),
+ managed);
+ if (dependency != null) {
+ if (coords.getScope() != null && !coords.getScope().isEmpty()) {
+ editor.updateOrCreateChildElement(dependency, "scope", coords.getScope());
+ }
+ if (coords.getOptional() != null && coords.getOptional()) {
+ editor.updateOrCreateChildElement(dependency, "optional", "true");
+ }
+ }
+ }
+
+ private PomEditor.Dependencies dependenciesFor(PomEditor editor, File pomFile, String profileId)
+ throws MojoFailureException {
+ PomEditor.Dependencies dependencies = editor.dependencies();
+ if (profileId == null || profileId.isEmpty()) {
+ return dependencies;
+ }
+ Element profileElement = editor.profiles().findProfile(profileId);
+ if (profileElement == null) {
+ throw new MojoFailureException("Profile '" + profileId + "' not found in " + pomFile.getName() + ".");
+ }
+ return dependencies.forProfile(profileElement);
+ }
+
+ private static PomEditor loadPomEditor(File pomFile) throws IOException {
+ try {
+ String content = new String(Files.readAllBytes(pomFile.toPath()), StandardCharsets.UTF_8);
+ String upper = content.toUpperCase(Locale.ROOT);
+ if (upper.contains(" root element but found <" + rootName + ">");
+ }
+ return editor;
+ } catch (RuntimeException e) {
+ throw new IOException("Failed to parse POM file: " + pomFile, e);
+ }
+ }
+
+ private static void savePomEditor(PomEditor editor, File pomFile) throws IOException {
+ Path target = pomFile.toPath();
+ File tempFile = File.createTempFile("pom", ".xml.tmp", pomFile.getParentFile());
+ boolean success = false;
+ try {
+ try (OutputStream os = Files.newOutputStream(tempFile.toPath())) {
+ editor.document().toXml(os);
+ }
+ Files.move(tempFile.toPath(), target, StandardCopyOption.REPLACE_EXISTING);
+ success = true;
+ } finally {
+ if (!success) {
+ Files.deleteIfExists(tempFile.toPath());
+ }
+ }
+ }
+
+ private static Element findDependency(
+ PomEditor editor,
+ String profileId,
+ String groupId,
+ String artifactId,
+ String type,
+ String classifier,
+ boolean managed) {
+ if (groupId == null || artifactId == null) {
+ throw new IllegalArgumentException("groupId and artifactId must not be null");
+ }
+ Element depsElement = getDependenciesElement(editor, profileId, managed);
+ if (depsElement == null) {
+ return null;
+ }
+
+ Coordinates coordinates = Coordinates.of(groupId, artifactId, null, classifier, type);
+ return depsElement
+ .childElements("dependency")
+ .filter(coordinates.predicateGATC())
+ .findFirst()
+ .orElse(null);
+ }
+
+ private static Element getDependenciesElement(PomEditor editor, String profileId, boolean managed) {
+ Element context = editor.root();
+ if (profileId != null && !profileId.isEmpty()) {
+ context = editor.profiles().findProfile(profileId);
+ if (context == null) {
+ return null;
+ }
+ }
+
+ if (managed) {
+ Element depMgmt = editor.findChildElement(context, "dependencyManagement");
+ return depMgmt != null ? editor.findChildElement(depMgmt, "dependencies") : null;
+ }
+ return editor.findChildElement(context, "dependencies");
+ }
+
+ private static List getDependencyVersions(PomEditor editor, boolean managed) {
+ List versions = new ArrayList<>();
+ Element depsElement = getDependenciesElement(editor, null, managed);
+ if (depsElement == null) {
+ return versions;
+ }
+ depsElement.childElements("dependency").forEach(dep -> {
+ String version = dep.childTextOr("version", null);
+ if (version != null && !version.isEmpty()) {
+ versions.add(version);
+ }
+ });
+ return versions;
+ }
+
+ private static long getDependencyCount(PomEditor editor, boolean managed) {
+ Element depsElement = getDependenciesElement(editor, null, managed);
+ return depsElement != null ? depsElement.childElements("dependency").count() : 0;
+ }
+
+ // --- Convention detection ---
+
+ /**
+ * Detects the project's dependency management conventions by analyzing existing dependencies.
+ */
+ private Conventions detectConventions(MavenProject project) throws MojoExecutionException {
+ Conventions conv = new Conventions();
+
+ File pomFile = project.getFile();
+ if (pomFile == null) {
+ return conv;
+ }
+
+ try {
+ PomEditor editor = loadPomEditor(pomFile);
+
+ // Analyze managed dependency usage: count deps with/without version
+ long totalDeps = getDependencyCount(editor, false);
+ List depVersions = getDependencyVersions(editor, false);
+ long depsWithVersion = depVersions.size();
+
+ if (totalDeps > 0 && depsWithVersion < totalDeps / 2.0) {
+ // Majority of deps are version-less → use managed deps
+ conv.useManaged = true;
+ getLog().debug("Convention detected: majority of dependencies are version-less (useManaged=true)");
+ }
+
+ // Find the POM that has
+ conv.managedPomFile = findManagedDepsPom(project);
+
+ // Analyze property patterns from child POM versions
+ List allVersions = new java.util.ArrayList<>(depVersions);
+ allVersions.addAll(getDependencyVersions(editor, true));
+
+ // If child has mostly version-less deps, also scan the parent POM for property patterns
+ if (conv.managedPomFile != null
+ && !conv.managedPomFile.getAbsolutePath().equals(pomFile.getAbsolutePath())) {
+ PomEditor parentEditor = loadPomEditor(conv.managedPomFile);
+ allVersions.addAll(getDependencyVersions(parentEditor, true));
+ allVersions.addAll(getDependencyVersions(parentEditor, false));
+ }
+
+ // Count property references vs literal versions
+ long propertyRefs = allVersions.stream()
+ .filter(v -> v.startsWith("${") && v.endsWith("}"))
+ .count();
+ if (!allVersions.isEmpty() && propertyRefs > allVersions.size() / 2.0) {
+ conv.useProperty = true;
+ getLog().debug("Convention detected: majority of versions use property references (useProperty=true)");
+
+ // Detect property naming pattern
+ conv.pattern = detectPropertyPattern(allVersions);
+ if (conv.pattern != null) {
+ getLog().debug("Convention detected: property naming pattern=" + conv.pattern);
+ }
+ }
+ } catch (IOException e) {
+ getLog().debug("Could not analyze POM conventions: " + e.getMessage());
+ }
+
+ return conv;
+ }
+
+ /**
+ * Walks the parent chain to find the nearest POM that declares {@code }.
+ */
+ private File findManagedDepsPom(MavenProject project) {
+ MavenProject current = project.getParent();
+ while (current != null) {
+ File pf = current.getFile();
+ if (pf != null) {
+ DependencyManagement dm = current.getOriginalModel().getDependencyManagement();
+ if (dm != null
+ && dm.getDependencies() != null
+ && !dm.getDependencies().isEmpty()) {
+ return pf;
+ }
+ }
+ current = current.getParent();
+ }
+ return null;
+ }
+
+ private static final Pattern PROPERTY_REF = Pattern.compile("^\\$\\{(.+)}$");
+
+ /**
+ * Property naming conventions detected in existing POM files.
+ */
+ enum PropertyPattern {
+ /** {@code artifactId.version} (e.g., {@code guava.version}) */
+ DOT_VERSION {
+ @Override
+ String toPropertyName(String artifactId) {
+ return artifactId + ".version";
+ }
+
+ @Override
+ boolean matches(String propName) {
+ return propName.endsWith(".version");
+ }
+ },
+ /** {@code artifactId-version} (e.g., {@code guava-version}) */
+ DASH_VERSION {
+ @Override
+ String toPropertyName(String artifactId) {
+ return artifactId + "-version";
+ }
+
+ @Override
+ boolean matches(String propName) {
+ return propName.endsWith("-version");
+ }
+ },
+ /** {@code artifactIdVersion} (camelCase, e.g., {@code guavaVersion}) */
+ CAMEL_VERSION {
+ @Override
+ String toPropertyName(String artifactId) {
+ return artifactId + "Version";
+ }
+
+ @Override
+ boolean matches(String propName) {
+ return propName.endsWith("Version");
+ }
+ },
+ /** {@code version.artifactId} (prefix, e.g., {@code version.guava}) */
+ VERSION_PREFIX {
+ @Override
+ String toPropertyName(String artifactId) {
+ return "version." + artifactId;
+ }
+
+ @Override
+ boolean matches(String propName) {
+ return propName.startsWith("version.");
+ }
+ };
+
+ abstract String toPropertyName(String artifactId);
+
+ abstract boolean matches(String propName);
+ }
+
+ /**
+ * Analyzes property reference names to detect the naming convention.
+ *
+ * @return the most common {@link PropertyPattern}, or {@code null} if no clear pattern is found
+ */
+ static PropertyPattern detectPropertyPattern(List versions) {
+ Map patternCounts = new HashMap<>();
+ for (String version : versions) {
+ Matcher m = PROPERTY_REF.matcher(version);
+ if (m.matches()) {
+ String propName = m.group(1);
+ for (PropertyPattern pp : PropertyPattern.values()) {
+ if (pp.matches(propName)) {
+ patternCounts.merge(pp, 1, Integer::sum);
+ break;
+ }
+ }
+ }
+ }
+ if (patternCounts.isEmpty()) {
+ return null;
+ }
+ return patternCounts.entrySet().stream()
+ .max(Map.Entry.comparingByValue())
+ .map(Map.Entry::getKey)
+ .orElse(null);
+ }
+
+ /**
+ * Holds the detected conventions for the project.
+ */
+ static class Conventions {
+ boolean useManaged;
+ boolean useProperty;
+ PropertyPattern pattern;
+ File managedPomFile;
+
+ /**
+ * Derives a property name for the given dependency based on the detected pattern.
+ */
+ String derivePropertyName(DependencyEntry coords) {
+ PropertyPattern effective = pattern != null ? pattern : PropertyPattern.DOT_VERSION;
+ return effective.toPropertyName(coords.getArtifactId());
+ }
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/dependency/RemoveDependencyMojo.java b/src/main/java/org/apache/maven/plugins/dependency/RemoveDependencyMojo.java
new file mode 100644
index 000000000..1bfd4a3e4
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/dependency/RemoveDependencyMojo.java
@@ -0,0 +1,304 @@
+/*
+ * 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 javax.inject.Inject;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Locale;
+
+import eu.maveniverse.domtrip.Document;
+import eu.maveniverse.domtrip.Element;
+import eu.maveniverse.domtrip.maven.Coordinates;
+import eu.maveniverse.domtrip.maven.PomEditor;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.model.Dependency;
+import org.apache.maven.model.DependencyManagement;
+import org.apache.maven.model.Model;
+import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
+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.plugins.dependency.pom.DependencyEntry;
+import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.util.xml.XmlStreamReader;
+import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
+import org.sonatype.plexus.build.incremental.BuildContext;
+
+/**
+ * Removes a dependency from the project's {@code pom.xml}.
+ * Supports removing from {@code } or {@code }.
+ *
+ * Matching uses groupId, artifactId, type, and classifier for precision.
+ * If the dependency exists in Maven's resolved model but uses property references
+ * in the raw POM, a clear error directs the user to edit manually.
+ * When removing a managed dependency from a parent POM, warns if child modules
+ * reference it without an explicit version.
+ *
+ * @since 3.11.0
+ */
+@Mojo(name = "remove", requiresProject = true, threadSafe = true)
+public class RemoveDependencyMojo extends AbstractDependencyMojo {
+
+ /**
+ * Dependency coordinates: {@code groupId:artifactId[:version]}
+ * or {@code groupId:artifactId[:extension[:classifier]]:version}.
+ * Only groupId and artifactId are required. Type and classifier, if provided,
+ * are used for precise matching when multiple dependency variants exist
+ * (e.g., jar vs test-jar).
+ */
+ @Parameter(property = "gav")
+ private String gav;
+
+ /**
+ * When {@code true}, remove from {@code } instead of {@code }.
+ */
+ @Parameter(property = "managed", defaultValue = "false")
+ private boolean managed;
+
+ /**
+ * Dependency type for precise matching (e.g., {@code pom}, {@code war}, {@code test-jar}).
+ * When not specified, defaults to {@code "jar"}.
+ */
+ @Parameter(property = "type")
+ private String type;
+
+ /**
+ * Dependency classifier for precise matching (e.g., {@code sources}, {@code javadoc}, {@code tests}).
+ */
+ @Parameter(property = "classifier")
+ private String classifier;
+
+ /**
+ * Target a specific Maven profile by its {@code }. When set, the dependency is removed
+ * from the profile's {@code } or {@code } section.
+ * The profile must already exist in the POM.
+ */
+ @Parameter(property = "profile")
+ private String profile;
+
+ @Inject
+ public RemoveDependencyMojo(MavenSession session, BuildContext buildContext, MavenProject project) {
+ super(session, buildContext, project);
+ }
+
+ @Override
+ protected void doExecute() throws MojoExecutionException, MojoFailureException {
+ DependencyEntry coords = resolveCoordinates();
+
+ boolean targetManaged = managed;
+ MavenProject targetProject = getProject();
+ File pomFile = targetProject.getFile();
+ if (pomFile == null) {
+ throw new MojoExecutionException("Cannot remove dependency: project has no POM file to modify.");
+ }
+
+ // Safety check for managed dependency removal in parent POM
+ if (targetManaged
+ && targetProject.getModules() != null
+ && !targetProject.getModules().isEmpty()) {
+ checkChildModuleDependencies(targetProject, coords.getGroupId(), coords.getArtifactId());
+ }
+
+ try {
+ PomEditor editor = loadPomEditor(pomFile);
+ PomEditor.Dependencies dependencies = dependenciesFor(editor, pomFile);
+ Coordinates coordinates = Coordinates.of(
+ coords.getGroupId(), coords.getArtifactId(), null, coords.getClassifier(), coords.getType());
+ boolean removed = targetManaged
+ ? dependencies.deleteManagedDependency(coordinates)
+ : dependencies.deleteDependency(coordinates);
+
+ if (!removed) {
+ // Cross-reference with resolved model to detect property-interpolated coords
+ if (existsInResolvedModel(targetProject, coords, targetManaged)) {
+ String section = targetManaged ? "" : "";
+ throw new MojoFailureException("Dependency " + coords.getGroupId() + ":"
+ + coords.getArtifactId()
+ + " exists in " + section + " but uses property references in the POM. "
+ + "Please remove it manually.");
+ }
+ String section = targetManaged ? "" : "";
+ throw new MojoFailureException("Dependency " + coords.getGroupId() + ":" + coords.getArtifactId()
+ + " not found in " + section + ".");
+ }
+
+ savePomEditor(editor, pomFile);
+
+ // Sync in-memory model so chained goals see the change
+ Model model = targetProject.getModel();
+ if (model != null) {
+ String removeType =
+ (coords.getType() != null && !coords.getType().isEmpty()) ? coords.getType() : "jar";
+ String removeClassifier = (coords.getClassifier() != null
+ && !coords.getClassifier().isEmpty())
+ ? coords.getClassifier()
+ : "";
+ if (targetManaged) {
+ DependencyManagement dm = model.getDependencyManagement();
+ if (dm != null && dm.getDependencies() != null) {
+ dm.getDependencies()
+ .removeIf(d -> coords.getGroupId().equals(d.getGroupId())
+ && coords.getArtifactId().equals(d.getArtifactId())
+ && removeType.equals(d.getType() != null ? d.getType() : "jar")
+ && removeClassifier.equals(d.getClassifier() != null ? d.getClassifier() : ""));
+ }
+ } else if (model.getDependencies() != null) {
+ model.getDependencies()
+ .removeIf(d -> coords.getGroupId().equals(d.getGroupId())
+ && coords.getArtifactId().equals(d.getArtifactId())
+ && removeType.equals(d.getType() != null ? d.getType() : "jar")
+ && removeClassifier.equals(d.getClassifier() != null ? d.getClassifier() : ""));
+ }
+ }
+
+ getLog().info("Removed dependency " + coords.getGroupId() + ":" + coords.getArtifactId() + " from "
+ + pomFile.getName());
+ } catch (IOException e) {
+ throw new MojoExecutionException("Failed to modify POM file: " + pomFile, e);
+ }
+ }
+
+ private PomEditor.Dependencies dependenciesFor(PomEditor editor, File pomFile) throws MojoFailureException {
+ PomEditor.Dependencies dependencies = editor.dependencies();
+ if (profile == null || profile.isEmpty()) {
+ return dependencies;
+ }
+ Element profileElement = editor.profiles().findProfile(profile);
+ if (profileElement == null) {
+ throw new MojoFailureException("Profile '" + profile + "' not found in " + pomFile.getName() + ".");
+ }
+ return dependencies.forProfile(profileElement);
+ }
+
+ private static PomEditor loadPomEditor(File pomFile) throws IOException {
+ try {
+ String content = new String(Files.readAllBytes(pomFile.toPath()), StandardCharsets.UTF_8);
+ String upper = content.toUpperCase(Locale.ROOT);
+ if (upper.contains(" root element but found <" + rootName + ">");
+ }
+ return editor;
+ } catch (RuntimeException e) {
+ throw new IOException("Failed to parse POM file: " + pomFile, e);
+ }
+ }
+
+ private static void savePomEditor(PomEditor editor, File pomFile) throws IOException {
+ Path target = pomFile.toPath();
+ File tempFile = File.createTempFile("pom", ".xml.tmp", pomFile.getParentFile());
+ boolean success = false;
+ try {
+ try (OutputStream os = Files.newOutputStream(tempFile.toPath())) {
+ editor.document().toXml(os);
+ }
+ Files.move(tempFile.toPath(), target, StandardCopyOption.REPLACE_EXISTING);
+ success = true;
+ } finally {
+ if (!success) {
+ Files.deleteIfExists(tempFile.toPath());
+ }
+ }
+ }
+
+ private DependencyEntry resolveCoordinates() throws MojoFailureException {
+ if (gav == null || gav.isEmpty()) {
+ throw new MojoFailureException("You must specify -Dgav=groupId:artifactId");
+ }
+
+ DependencyEntry coords;
+ try {
+ coords = DependencyEntry.parse(gav);
+ } catch (IllegalArgumentException e) {
+ throw new MojoFailureException(e.getMessage());
+ }
+
+ // Explicit parameters override GAV shorthand values
+ if (type != null) {
+ coords.setType(type);
+ }
+ if (classifier != null) {
+ coords.setClassifier(classifier);
+ }
+
+ try {
+ coords.validate();
+ } catch (IllegalArgumentException e) {
+ throw new MojoFailureException(e.getMessage());
+ }
+
+ return coords;
+ }
+
+ private void checkChildModuleDependencies(MavenProject parentProject, String depGroupId, String depArtifactId)
+ throws MojoExecutionException {
+ if (parentProject.getBasedir() == null) {
+ getLog().debug("Parent project basedir is null, skipping child module dependency check");
+ return;
+ }
+ StringBuilder affected = new StringBuilder();
+ MavenXpp3Reader pomReader = new MavenXpp3Reader();
+
+ for (String moduleName : parentProject.getModules()) {
+ File moduleDir = new File(parentProject.getBasedir(), moduleName);
+ File modulePom = new File(moduleDir, "pom.xml");
+ if (!modulePom.exists()) {
+ continue;
+ }
+ try (Reader reader = new XmlStreamReader(modulePom)) {
+ Model model = pomReader.read(reader);
+ if (model.getDependencies() != null) {
+ for (Dependency dep : model.getDependencies()) {
+ if (depGroupId.equals(dep.getGroupId())
+ && depArtifactId.equals(dep.getArtifactId())
+ && (dep.getVersion() == null || dep.getVersion().isEmpty())) {
+ if (affected.length() > 0) {
+ affected.append(", ");
+ }
+ affected.append(moduleName);
+ break;
+ }
+ }
+ }
+ } catch (IOException | XmlPullParserException e) {
+ getLog().debug("Could not read module POM: " + modulePom + " - " + e.getMessage());
+ }
+ }
+
+ if (affected.length() > 0) {
+ getLog().warn("The following child modules depend on " + depGroupId + ":" + depArtifactId
+ + " without an explicit version and will break: [" + affected + "]. Proceeding with removal.");
+ }
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/dependency/pom/DependencyEntry.java b/src/main/java/org/apache/maven/plugins/dependency/pom/DependencyEntry.java
new file mode 100644
index 000000000..e69e5e1d5
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/dependency/pom/DependencyEntry.java
@@ -0,0 +1,235 @@
+/*
+ * 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.pom;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Represents parsed Maven dependency coordinates (GAV + scope/type/classifier).
+ * Supports parsing from a colon-separated string in the format:
+ * {@code groupId:artifactId[:version]} or
+ * {@code groupId:artifactId[:extension[:classifier]]:version}.
+ *
+ * This follows the standard Maven coordinate convention used by
+ * {@code org.eclipse.aether.artifact.DefaultArtifact}. Scope and optional
+ * are not part of coordinates and must be specified as separate parameters.
+ *
+ * Design note: This class exists because no single class on the
+ * classpath covers all requirements. {@code DefaultArtifact} (Aether) provides GAV
+ * parsing but requires a version (throws on {@code g:a}) and lacks scope/optional.
+ * {@code Dependency} (maven-model) has all fields but no coordinate string parser.
+ * This class bridges both: it parses {@code g:a} (needed by {@code dependency:remove}),
+ * carries scope/optional, and validates scope against Maven's known values.
+ *
+ * @since 3.11.0
+ */
+public class DependencyEntry {
+
+ private static final Set VALID_SCOPES =
+ new HashSet<>(Arrays.asList("compile", "provided", "runtime", "test", "system", "import"));
+
+ private String groupId;
+ private String artifactId;
+ private String version;
+ private String scope;
+ private String type;
+ private String classifier;
+ private Boolean optional;
+
+ public DependencyEntry() {}
+
+ public DependencyEntry(String groupId, String artifactId) {
+ this.groupId = groupId;
+ this.artifactId = artifactId;
+ }
+
+ /**
+ * Parses a colon-separated coordinate string following Maven conventions.
+ *
+ * Supported formats:
+ *
+ * - {@code groupId:artifactId} — minimum
+ * - {@code groupId:artifactId:version}
+ * - {@code groupId:artifactId:extension:version}
+ * - {@code groupId:artifactId:extension:classifier:version}
+ *
+ *
+ * @param gav the coordinate string
+ * @return the parsed coordinates
+ * @throws IllegalArgumentException if the string has fewer than 2 or more than 5 segments
+ */
+ public static DependencyEntry parse(String gav) {
+ if (gav == null || gav.trim().isEmpty()) {
+ throw new IllegalArgumentException("GAV string must not be null or empty");
+ }
+ String[] tokens = gav.split(":", -1);
+ if (tokens.length < 2 || tokens.length > 5) {
+ throw new IllegalArgumentException(
+ "Invalid GAV format: '" + gav
+ + "'. Expected groupId:artifactId[:version] or groupId:artifactId[:extension[:classifier]]:version");
+ }
+ DependencyEntry coords = new DependencyEntry();
+ coords.groupId = tokens[0].trim();
+ coords.artifactId = tokens[1].trim();
+ if (coords.groupId.isEmpty()) {
+ throw new IllegalArgumentException("Invalid GAV format: '" + gav + "'. groupId must not be empty.");
+ }
+ if (coords.artifactId.isEmpty()) {
+ throw new IllegalArgumentException("Invalid GAV format: '" + gav + "'. artifactId must not be empty.");
+ }
+ switch (tokens.length) {
+ case 3:
+ // g:a:v
+ if (!tokens[2].trim().isEmpty()) {
+ coords.version = tokens[2].trim();
+ }
+ break;
+ case 4:
+ // g:a:ext:v
+ if (!tokens[2].trim().isEmpty()) {
+ coords.type = tokens[2].trim();
+ }
+ if (!tokens[3].trim().isEmpty()) {
+ coords.version = tokens[3].trim();
+ }
+ break;
+ case 5:
+ // g:a:ext:cls:v
+ if (!tokens[2].trim().isEmpty()) {
+ coords.type = tokens[2].trim();
+ }
+ if (!tokens[3].trim().isEmpty()) {
+ coords.classifier = tokens[3].trim();
+ }
+ if (!tokens[4].trim().isEmpty()) {
+ coords.version = tokens[4].trim();
+ }
+ break;
+ default:
+ break;
+ }
+ return coords;
+ }
+
+ /**
+ * Validates that required fields are present.
+ *
+ * @throws IllegalArgumentException if groupId or artifactId is missing
+ */
+ public void validate() {
+ if (groupId == null || groupId.isEmpty()) {
+ throw new IllegalArgumentException("groupId must not be null or empty");
+ }
+ if (artifactId == null || artifactId.isEmpty()) {
+ throw new IllegalArgumentException("artifactId must not be null or empty");
+ }
+ if (scope != null && !scope.isEmpty() && !VALID_SCOPES.contains(scope)) {
+ throw new IllegalArgumentException("Invalid scope: '" + scope + "'. Valid scopes are: " + VALID_SCOPES);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(groupId).append(':').append(artifactId);
+ if (version != null) {
+ sb.append(':').append(version);
+ }
+ StringBuilder details = new StringBuilder();
+ if (type != null && !"jar".equals(type)) {
+ details.append("type=").append(type);
+ }
+ if (classifier != null && !classifier.isEmpty()) {
+ if (details.length() > 0) {
+ details.append(", ");
+ }
+ details.append("classifier=").append(classifier);
+ }
+ if (scope != null) {
+ if (details.length() > 0) {
+ details.append(", ");
+ }
+ details.append("scope=").append(scope);
+ }
+ if (details.length() > 0) {
+ sb.append(" [").append(details).append(']');
+ }
+ return sb.toString();
+ }
+
+ // Getters and setters
+
+ public String getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(String groupId) {
+ this.groupId = groupId;
+ }
+
+ public String getArtifactId() {
+ return artifactId;
+ }
+
+ public void setArtifactId(String artifactId) {
+ this.artifactId = artifactId;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public String getScope() {
+ return scope;
+ }
+
+ public void setScope(String scope) {
+ this.scope = scope;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getClassifier() {
+ return classifier;
+ }
+
+ public void setClassifier(String classifier) {
+ this.classifier = classifier;
+ }
+
+ public Boolean getOptional() {
+ return optional;
+ }
+
+ public void setOptional(Boolean optional) {
+ this.optional = optional;
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/dependency/resolvers/ResolvePluginsMojo.java b/src/main/java/org/apache/maven/plugins/dependency/resolvers/ResolvePluginsMojo.java
index 1503bc0fb..b48998215 100644
--- a/src/main/java/org/apache/maven/plugins/dependency/resolvers/ResolvePluginsMojo.java
+++ b/src/main/java/org/apache/maven/plugins/dependency/resolvers/ResolvePluginsMojo.java
@@ -153,7 +153,6 @@ public ResolvePluginsMojo(
@Override
protected void doExecute() throws MojoExecutionException {
try {
- // ideally this should either be DependencyCoordinates or DependencyNode
final Set plugins = getProjectPlugins();
StringBuilder sb = new StringBuilder();
diff --git a/src/site/apt/examples/managing-dependencies.apt.vm b/src/site/apt/examples/managing-dependencies.apt.vm
new file mode 100644
index 000000000..8f384e9c9
--- /dev/null
+++ b/src/site/apt/examples/managing-dependencies.apt.vm
@@ -0,0 +1,231 @@
+~~ 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.
+
+ ------
+ Managing Dependencies
+ ------
+ Bruno Borges
+ ------
+ 2025-06-01
+ ------
+
+Managing Dependencies
+
+ Starting with version 3.11.0, the Maven Dependency Plugin provides two goals
+ for managing dependencies directly from the command line: <<>>
+ and <<>>.
+
+%{toc|fromDepth=2}
+
+* Adding Dependencies
+
+ The <<>> goal adds a dependency to your project's <<>>.
+
+** Basic Usage
+
+ You can add a dependency by specifying its coordinates with the <<>> parameter:
+
++---+
+mvn dependency:add -Dgav=org.apache.commons:commons-lang3:3.17.0
++---+
+
+ The <<>> parameter accepts the format <<>>.
+ Scope must be specified separately with <<<-Dscope>>>:
+
++---+
+mvn dependency:add -Dgav=org.apache.commons:commons-lang3:3.17.0 -Dscope=test
++---+
+
+** Adding to dependencyManagement
+
+ Use the <<<-Dmanaged>>> flag to add a dependency to the <<<\>>> section
+ instead of <<<\>>>:
+
++---+
+mvn dependency:add -Dgav=org.apache.commons:commons-lang3:3.17.0 -Dmanaged
++---+
+
+** Adding a BOM Import
+
+ To import a BOM (Bill of Materials) into <<<\>>>, specify the type and scope explicitly:
+
++---+
+mvn dependency:add -Dgav=org.springframework.boot:spring-boot-dependencies:pom:3.2.0 -Dscope=import -Dmanaged
++---+
+
+ This inserts a dependency with <<>> and <<>> into <<<\>>>.
+
+** Version Inference
+
+ When adding to <<<\>>> and the version is already managed by a parent POM's
+ <<<\>>>, you can omit the version:
+
++---+
+mvn dependency:add -Dgav=org.apache.commons:commons-lang3
++---+
+
+ The goal will detect the managed version and omit the <<<\>>> element from the inserted
+ dependency, following Maven convention. If no managed version is found, the goal fails with
+ a descriptive error.
+
+** Additional Parameters
+
+ You can also specify <<>> and <<>> explicitly. These override any values
+ provided in the <<>> shorthand:
+
++---+
+mvn dependency:add -Dgav=com.example:lib:1.0 -Dtype=test-jar
+mvn dependency:add -Dgav=com.example:lib:1.0 -Dclassifier=sources
++---+
+
+** Duplicate Detection
+
+ If the dependency already exists in the POM (matching on <<>>, <<>>,
+ <<>>, and <<>>), the goal fails with a descriptive error directing you to
+ remove the existing dependency first:
+
++---+
+mvn dependency:remove -Dgav=org.apache.commons:commons-lang3
+mvn dependency:add -Dgav=org.apache.commons:commons-lang3:3.17.0 -Dscope=test
++---+
+
+** Optional Dependencies
+
+ Mark a dependency as optional using the <<>> parameter:
+
++---+
+mvn dependency:add -Dgav=org.apache.commons:commons-lang3:3.17.0 -Doptional=true
++---+
+
+** Scope Validation
+
+ The <<>> parameter is validated against Maven's known scope values:
+ <<>>, <<>>, <<>>, <<>>, <<>>, and <<>>.
+ Invalid scope values are rejected with an error.
+
+** Dependencies Using Property References
+
+ Some POMs declare dependencies using Maven property references for the <<>> or
+ <<>>, for example:
+
++---+
+
+ ${my.group}
+ lib
+
++---+
+
+ When <<>> detects that a dependency with the same coordinates already exists
+ in the resolved (effective) model but cannot be found in the raw XML, it means the dependency
+ is declared using property interpolation. In this case, the goal <> with an error message
+ explaining that the dependency cannot be safely added or updated automatically:
+
++---+
+[ERROR] Dependency com.example:lib already exists in the POM (using property references).
+Cannot safely add or update automatically. Please edit the POM manually.
++---+
+
+ This safety check prevents <<>> from inserting a duplicate entry that would
+ conflict with the property-based declaration. The same check applies to <<>>.
+
+** Multi-Module Projects
+
+ To add a dependency to a specific child module from the root of a multi-module project,
+ use Maven's built-in <<<-pl>>> (project list) flag:
+
++---+
+mvn dependency:add -Dgav=org.apache.commons:commons-lang3:3.17.0 -pl my-service
++---+
+
+** Adding to a Profile
+
+ To add a dependency to a specific Maven profile, use the <<<-Dprofile>>> parameter.
+ The profile must already exist in the POM:
+
++---+
+mvn dependency:add -Dgav=org.apache.commons:commons-lang3:3.17.0 -Dprofile=dev
++---+
+
+* Removing Dependencies
+
+ The <<>> goal removes a dependency from your project's <<>>.
+
+** Basic Usage
+
+ Remove a dependency using the <<>> parameter:
+
++---+
+mvn dependency:remove -Dgav=org.apache.commons:commons-lang3
++---+
+
+ Only <<>> and <<>> are required to identify the dependency to remove.
+
+** Type and Classifier Matching
+
+ Maven allows the same <<>> to appear multiple times in a POM
+ with different types or classifiers (e.g., both the main jar and a test-jar). These
+ are distinct dependencies.
+
+ When no <<<-Dtype>>> or <<<-Dclassifier>>> is specified, <<>> targets
+ the default variant (<<>>, no classifier). To remove a specific variant, use
+ <<<-Dtype>>> and/or <<<-Dclassifier>>>:
+
++---+
+mvn dependency:remove -Dgav=com.example:lib -Dtype=test-jar
+mvn dependency:remove -Dgav=com.example:lib -Dclassifier=sources
++---+
+
+ Explicit <<<-Dtype>>> and <<<-Dclassifier>>> override any values from the <<<-Dgav>>> parameter.
+
+ Similarly, <<>> uses <<>> matching for
+ duplicate detection. Adding <<>> will succeed even if
+ <<>> already exists, since they are distinct dependencies.
+
+** Removing a BOM Import
+
+ To remove a BOM import from <<<\>>>, specify the type and use <<<-Dmanaged>>>:
+
++---+
+mvn dependency:remove -Dgav=org.springframework.boot:spring-boot-dependencies -Dtype=pom -Dmanaged
++---+
+
+** Removing from dependencyManagement
+
+ To remove a dependency from <<<\>>> instead of <<<\>>>:
+
++---+
+mvn dependency:remove -Dgav=org.apache.commons:commons-lang3 -Dmanaged
++---+
+
+** Removing from a Specific Module
+
+ In a multi-module project, use Maven's <<<-pl>>> flag to target a specific child module:
+
++---+
+mvn dependency:remove -Dgav=org.apache.commons:commons-lang3 -pl my-service
++---+
+
+ If the dependency is not found, the goal fails with a descriptive error message.
+
+** Removing from a Profile
+
+ To remove a dependency from a specific Maven profile:
+
++---+
+mvn dependency:remove -Dgav=org.apache.commons:commons-lang3 -Dprofile=dev
++---+
+
diff --git a/src/site/apt/index.apt.vm b/src/site/apt/index.apt.vm
index b44c07f56..9a5db684f 100644
--- a/src/site/apt/index.apt.vm
+++ b/src/site/apt/index.apt.vm
@@ -34,6 +34,10 @@ ${project.name}
The Dependency plugin has several goals:
+ *{{{./add-mojo.html}dependency:add}} adds a dependency to the project's <<>> from the command line.
+ Supports GAV shorthand, version inference from <<<\>>>, BOM imports, profile targeting, and more.
+ See {{{./examples/managing-dependencies.html}Managing Dependencies}}.
+
*{{{./analyze-mojo.html}dependency:analyze}} analyzes the dependencies of this project and determines which are: used
and declared; used and undeclared; unused and declared.
@@ -89,6 +93,10 @@ ${project.name}
*{{{./purge-local-repository-mojo.html}dependency:purge-local-repository}} tells Maven to clear dependency artifact
files out of the local repository, and optionally re-resolve them.
+ *{{{./remove-mojo.html}dependency:remove}} removes a dependency from the project's <<>> from the command line.
+ Supports <<<\>>>, BOM imports, profile targeting, and child module safety checks.
+ See {{{./examples/managing-dependencies.html}Managing Dependencies}}.
+
*{{{./resolve-mojo.html}dependency:resolve}} tells Maven to resolve all dependencies and displays the version. <>
@@ -151,6 +159,8 @@ ${project.name}
* {{{./examples/filtering-the-dependency-tree.html}Filtering the Dependency Tree}}
+ * {{{./examples/managing-dependencies.html}Managing Dependencies}}
+
* {{{./examples/purging-local-repository.html}Purging the local repository}}
* {{{./examples/tree-mojo.html}Tree Mojo}}
diff --git a/src/site/apt/usage.apt.vm b/src/site/apt/usage.apt.vm
index 6519ea04d..7c1ef0aaf 100644
--- a/src/site/apt/usage.apt.vm
+++ b/src/site/apt/usage.apt.vm
@@ -714,3 +714,46 @@ mvn dependency:analyze-exclusions
[WARNING] - javax.annotation:javax.annotation-api
[WARNING] - javax.activation:javax.activation-api
+---+
+
+* <<>>
+
+ This goal adds a dependency to the project's <<>> directly from the command line.
+ If the dependency already exists, the goal fails with a descriptive error directing you to
+ remove it first. For detailed examples, see
+ {{{./examples/managing-dependencies.html}Managing Dependencies}}.
+
+---
+mvn dependency:add -Dgav=org.apache.commons:commons-lang3:3.17.0
+mvn dependency:add -Dgav=org.apache.commons:commons-lang3:3.17.0 -Dscope=test
+---
+
+ Add to <<<\>>>:
+
+---
+mvn dependency:add -Dgav=org.apache.commons:commons-lang3:3.17.0 -Dmanaged
+---
+
+ Add a BOM import:
+
+---
+mvn dependency:add -Dgav=org.springframework.boot:spring-boot-dependencies:pom:3.2.0 -Dscope=import -Dmanaged
+---
+
+ In multi-module projects, use Maven's <<<-pl>>> flag to target a specific module:
+
+---
+mvn dependency:add -Dgav=org.apache.commons:commons-lang3:3.17.0 -pl my-service
+---
+
+* <<>>
+
+ This goal removes a dependency from the project's <<>> directly from the command line.
+ For detailed examples, see {{{./examples/managing-dependencies.html}Managing Dependencies}}.
+
+---
+mvn dependency:remove -Dgav=org.apache.commons:commons-lang3
+mvn dependency:remove -Dgav=org.apache.commons:commons-lang3 -Dmanaged
+---
+
+ When removing a managed dependency from a parent POM, the goal warns if child modules still
+ reference that dependency without an explicit version.
diff --git a/src/site/site.xml b/src/site/site.xml
index 969c6875a..66ab52122 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -44,6 +44,7 @@ under the License.
+