' ) :
+ "Child dependency should be version-less (managed by parent)"
+
+// 6. Existing child dependencies should be preserved
+assert childDeps.size() == 4 : "Child should now have 4 dependencies"
+assert childDeps.find { it.artifactId == 'junit' } != null
+assert childDeps.find { it.artifactId == 'slf4j-api' } != null
+assert childDeps.find { it.artifactId == 'commons-io' } != null
+
+// --- Verify build log ---
+def buildLog = new File( basedir, "build.log" ).text
+assert buildLog.contains( 'Added/updated com.google.guava:guava:33.0.0-jre' ) :
+ "Log should confirm managed dep added"
+assert buildLog.contains( 'Added com.google.guava:guava' ) :
+ "Log should confirm version-less dep added"
+
+return true
diff --git a/src/it/projects/add-dependency-property/invoker.properties b/src/it/projects/add-dependency-property/invoker.properties
new file mode 100644
index 000000000..eb6424ecd
--- /dev/null
+++ b/src/it/projects/add-dependency-property/invoker.properties
@@ -0,0 +1,18 @@
+# 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.
+
+invoker.goals = ${project.groupId}:${project.artifactId}:${project.version}:add
diff --git a/src/it/projects/add-dependency-property/pom.xml b/src/it/projects/add-dependency-property/pom.xml
new file mode 100644
index 000000000..74e3cf1a2
--- /dev/null
+++ b/src/it/projects/add-dependency-property/pom.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.plugins.dependency.its
+ add-dependency-property
+ 1.0-SNAPSHOT
+
+
diff --git a/src/it/projects/add-dependency-property/test.properties b/src/it/projects/add-dependency-property/test.properties
new file mode 100644
index 000000000..49db570f6
--- /dev/null
+++ b/src/it/projects/add-dependency-property/test.properties
@@ -0,0 +1,20 @@
+# 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.
+
+gav=com.google.guava:guava:33.0.0-jre
+propertyName=guava.version
+align=false
diff --git a/src/it/projects/add-dependency-property/verify.groovy b/src/it/projects/add-dependency-property/verify.groovy
new file mode 100644
index 000000000..0dc4c17f6
--- /dev/null
+++ b/src/it/projects/add-dependency-property/verify.groovy
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+// Read the raw POM text to verify the property reference (XmlSlurper resolves properties)
+def pomText = new File( basedir, "pom.xml" ).text
+
+// Verify property was created
+assert pomText.contains( '33.0.0-jre' ) :
+ "Property guava.version should be defined"
+
+// Verify dependency version references the property
+assert pomText.contains( '${guava.version}' ) :
+ "Dependency version should reference \${guava.version}"
+
+// Also verify via XML parsing that the dependency exists
+def pom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) )
+def dep = pom.dependencies.dependency.find { it.artifactId == 'guava' }
+assert dep != null : "guava dependency should have been added"
+
+return true
diff --git a/src/it/projects/add-dependency-scope/invoker.properties b/src/it/projects/add-dependency-scope/invoker.properties
new file mode 100644
index 000000000..eb6424ecd
--- /dev/null
+++ b/src/it/projects/add-dependency-scope/invoker.properties
@@ -0,0 +1,18 @@
+# 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.
+
+invoker.goals = ${project.groupId}:${project.artifactId}:${project.version}:add
diff --git a/src/it/projects/add-dependency-scope/pom.xml b/src/it/projects/add-dependency-scope/pom.xml
new file mode 100644
index 000000000..427bf9056
--- /dev/null
+++ b/src/it/projects/add-dependency-scope/pom.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.plugins.dependency.its
+ add-dependency-scope
+ 1.0-SNAPSHOT
+
+
diff --git a/src/it/projects/add-dependency-scope/test.properties b/src/it/projects/add-dependency-scope/test.properties
new file mode 100644
index 000000000..1d0099685
--- /dev/null
+++ b/src/it/projects/add-dependency-scope/test.properties
@@ -0,0 +1,19 @@
+# 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.
+
+gav=org.junit.jupiter:junit-jupiter-api:5.10.0
+scope=test
diff --git a/src/it/projects/add-dependency-scope/verify.groovy b/src/it/projects/add-dependency-scope/verify.groovy
new file mode 100644
index 000000000..957b260fd
--- /dev/null
+++ b/src/it/projects/add-dependency-scope/verify.groovy
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+def pom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) )
+def deps = pom.dependencies.dependency
+
+def dep = deps.find { it.groupId == 'org.junit.jupiter' && it.artifactId == 'junit-jupiter-api' }
+assert dep != null : "junit-jupiter-api should have been added"
+assert dep.version == '5.10.0'
+assert dep.scope == 'test' : "scope should be 'test'"
+
+return true
diff --git a/src/it/projects/add-dependency/invoker.properties b/src/it/projects/add-dependency/invoker.properties
new file mode 100644
index 000000000..eb6424ecd
--- /dev/null
+++ b/src/it/projects/add-dependency/invoker.properties
@@ -0,0 +1,18 @@
+# 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.
+
+invoker.goals = ${project.groupId}:${project.artifactId}:${project.version}:add
diff --git a/src/it/projects/add-dependency/pom.xml b/src/it/projects/add-dependency/pom.xml
new file mode 100644
index 000000000..89063ec9f
--- /dev/null
+++ b/src/it/projects/add-dependency/pom.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.plugins.dependency.its
+ add-dependency
+ 1.0-SNAPSHOT
+
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+
+
diff --git a/src/it/projects/add-dependency/test.properties b/src/it/projects/add-dependency/test.properties
new file mode 100644
index 000000000..3ed9ae987
--- /dev/null
+++ b/src/it/projects/add-dependency/test.properties
@@ -0,0 +1,18 @@
+# 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.
+
+gav=org.apache.commons:commons-lang3:3.12.0
diff --git a/src/it/projects/add-dependency/verify.groovy b/src/it/projects/add-dependency/verify.groovy
new file mode 100644
index 000000000..f78dd9977
--- /dev/null
+++ b/src/it/projects/add-dependency/verify.groovy
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+def pom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) )
+def deps = pom.dependencies.dependency
+
+// Verify existing dependency is preserved
+def junit = deps.find { it.groupId == 'junit' && it.artifactId == 'junit' }
+assert junit != null : "Existing junit dependency should be preserved"
+assert junit.version == '4.13.2'
+assert junit.scope == 'test'
+
+// Verify new dependency was added
+def lang3 = deps.find { it.groupId == 'org.apache.commons' && it.artifactId == 'commons-lang3' }
+assert lang3 != null : "commons-lang3 dependency should have been added"
+assert lang3.version == '3.12.0'
+
+// Verify build log
+def buildLog = new File( basedir, "build.log" ).text
+assert buildLog.contains( 'Added/updated org.apache.commons:commons-lang3:3.12.0' )
+
+return true
diff --git a/src/it/projects/remove-dependency-managed/invoker.properties b/src/it/projects/remove-dependency-managed/invoker.properties
new file mode 100644
index 000000000..a7864b138
--- /dev/null
+++ b/src/it/projects/remove-dependency-managed/invoker.properties
@@ -0,0 +1,18 @@
+# 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.
+
+invoker.goals = ${project.groupId}:${project.artifactId}:${project.version}:remove
diff --git a/src/it/projects/remove-dependency-managed/pom.xml b/src/it/projects/remove-dependency-managed/pom.xml
new file mode 100644
index 000000000..63dfa91d9
--- /dev/null
+++ b/src/it/projects/remove-dependency-managed/pom.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.plugins.dependency.its
+ remove-dependency-managed
+ 1.0-SNAPSHOT
+
+
+
+
+ junit
+ junit
+ 4.13.2
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.9
+
+
+
+
+
diff --git a/src/it/projects/remove-dependency-managed/test.properties b/src/it/projects/remove-dependency-managed/test.properties
new file mode 100644
index 000000000..8302a3d02
--- /dev/null
+++ b/src/it/projects/remove-dependency-managed/test.properties
@@ -0,0 +1,19 @@
+# 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.
+
+gav=junit:junit
+managed=true
diff --git a/src/it/projects/remove-dependency-managed/verify.groovy b/src/it/projects/remove-dependency-managed/verify.groovy
new file mode 100644
index 000000000..98fd4c3ba
--- /dev/null
+++ b/src/it/projects/remove-dependency-managed/verify.groovy
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+def pom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) )
+def managedDeps = pom.dependencyManagement.dependencies.dependency
+
+// Verify junit was removed from dependencyManagement
+def junit = managedDeps.find { it.groupId == 'junit' && it.artifactId == 'junit' }
+assert junit.isEmpty() : "junit should have been removed from dependencyManagement"
+
+// Verify slf4j-api is still there
+def slf4j = managedDeps.find { it.groupId == 'org.slf4j' && it.artifactId == 'slf4j-api' }
+assert slf4j != null : "slf4j-api should be preserved in dependencyManagement"
+assert slf4j.version == '2.0.9'
+
+return true
diff --git a/src/it/projects/remove-dependency-not-found/invoker.properties b/src/it/projects/remove-dependency-not-found/invoker.properties
new file mode 100644
index 000000000..afd1eb822
--- /dev/null
+++ b/src/it/projects/remove-dependency-not-found/invoker.properties
@@ -0,0 +1,19 @@
+# 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.
+
+invoker.goals = ${project.groupId}:${project.artifactId}:${project.version}:remove
+invoker.buildResult = failure
diff --git a/src/it/projects/remove-dependency-not-found/pom.xml b/src/it/projects/remove-dependency-not-found/pom.xml
new file mode 100644
index 000000000..ca82a1445
--- /dev/null
+++ b/src/it/projects/remove-dependency-not-found/pom.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.plugins.dependency.its
+ remove-dependency-not-found
+ 1.0-SNAPSHOT
+
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.9
+
+
+
+
diff --git a/src/it/projects/remove-dependency-not-found/test.properties b/src/it/projects/remove-dependency-not-found/test.properties
new file mode 100644
index 000000000..ae5241a96
--- /dev/null
+++ b/src/it/projects/remove-dependency-not-found/test.properties
@@ -0,0 +1,18 @@
+# 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.
+
+gav=junit:junit
diff --git a/src/it/projects/remove-dependency-not-found/verify.groovy b/src/it/projects/remove-dependency-not-found/verify.groovy
new file mode 100644
index 000000000..503387554
--- /dev/null
+++ b/src/it/projects/remove-dependency-not-found/verify.groovy
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+// Build is expected to fail
+def buildLog = new File( basedir, "build.log" ).text
+assert buildLog.contains( 'not found in' ) : "Should report dependency not found"
+
+// POM should be unchanged
+def pom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) )
+def deps = pom.dependencies.dependency
+assert deps.size() == 1 : "POM should still have exactly 1 dependency"
+assert deps[0].artifactId == 'slf4j-api'
+
+return true
diff --git a/src/it/projects/remove-dependency/invoker.properties b/src/it/projects/remove-dependency/invoker.properties
new file mode 100644
index 000000000..a7864b138
--- /dev/null
+++ b/src/it/projects/remove-dependency/invoker.properties
@@ -0,0 +1,18 @@
+# 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.
+
+invoker.goals = ${project.groupId}:${project.artifactId}:${project.version}:remove
diff --git a/src/it/projects/remove-dependency/pom.xml b/src/it/projects/remove-dependency/pom.xml
new file mode 100644
index 000000000..b613241d2
--- /dev/null
+++ b/src/it/projects/remove-dependency/pom.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.plugins.dependency.its
+ remove-dependency
+ 1.0-SNAPSHOT
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.9
+
+
+
+
diff --git a/src/it/projects/remove-dependency/test.properties b/src/it/projects/remove-dependency/test.properties
new file mode 100644
index 000000000..ae5241a96
--- /dev/null
+++ b/src/it/projects/remove-dependency/test.properties
@@ -0,0 +1,18 @@
+# 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.
+
+gav=junit:junit
diff --git a/src/it/projects/remove-dependency/verify.groovy b/src/it/projects/remove-dependency/verify.groovy
new file mode 100644
index 000000000..0becc727d
--- /dev/null
+++ b/src/it/projects/remove-dependency/verify.groovy
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+def pom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) )
+def deps = pom.dependencies.dependency
+
+// Verify junit was removed
+def junit = deps.find { it.groupId == 'junit' && it.artifactId == 'junit' }
+assert junit.isEmpty() : "junit dependency should have been removed"
+
+// Verify slf4j-api is still there
+def slf4j = deps.find { it.groupId == 'org.slf4j' && it.artifactId == 'slf4j-api' }
+assert slf4j != null : "slf4j-api dependency should be preserved"
+assert slf4j.version == '2.0.9'
+
+// Verify build log
+def buildLog = new File( basedir, "build.log" ).text
+assert buildLog.contains( 'Removed junit:junit' )
+
+return true
diff --git a/src/it/projects/search-dependency/invoker.properties b/src/it/projects/search-dependency/invoker.properties
new file mode 100644
index 000000000..81f1af56e
--- /dev/null
+++ b/src/it/projects/search-dependency/invoker.properties
@@ -0,0 +1,20 @@
+# 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.
+
+invoker.goals = ${project.groupId}:${project.artifactId}:${project.version}:search
+# This IT requires network access to Maven Central search API
+invoker.os.family = !offline
diff --git a/src/it/projects/search-dependency/pom.xml b/src/it/projects/search-dependency/pom.xml
new file mode 100644
index 000000000..ec15cdb98
--- /dev/null
+++ b/src/it/projects/search-dependency/pom.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.plugins.dependency.its
+ search-dependency
+ 1.0-SNAPSHOT
+
+
diff --git a/src/it/projects/search-dependency/test.properties b/src/it/projects/search-dependency/test.properties
new file mode 100644
index 000000000..990f99fa6
--- /dev/null
+++ b/src/it/projects/search-dependency/test.properties
@@ -0,0 +1,19 @@
+# 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.
+
+query=g:org.apache.commons AND a:commons-lang3
+rows=5
diff --git a/src/it/projects/search-dependency/verify.groovy b/src/it/projects/search-dependency/verify.groovy
new file mode 100644
index 000000000..d2ff214e7
--- /dev/null
+++ b/src/it/projects/search-dependency/verify.groovy
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+def buildLog = new File( basedir, "build.log" ).text
+
+// Verify search was executed
+assert buildLog.contains( 'Searching Maven Central' ) : "Should show search message"
+
+// Verify results contain commons-lang3
+assert buildLog.contains( 'org.apache.commons' ) : "Results should contain org.apache.commons"
+assert buildLog.contains( 'commons-lang3' ) : "Results should contain commons-lang3"
+
+// Verify table header was printed
+assert buildLog.contains( 'GroupId' ) : "Should print table header"
+assert buildLog.contains( 'ArtifactId' ) : "Should print table header"
+
+return true
diff --git a/src/main/java/org/apache/maven/plugins/dependency/AddDependencyMojo.java b/src/main/java/org/apache/maven/plugins/dependency/AddDependencyMojo.java
new file mode 100644
index 000000000..1915242f2
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/dependency/AddDependencyMojo.java
@@ -0,0 +1,531 @@
+/*
+ * 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.nio.file.Files;
+import java.util.HashMap;
+import java.util.Map;
+
+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.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.sonatype.plexus.build.incremental.BuildContext;
+
+/**
+ * Adds or updates a dependency in the project's {@code pom.xml}.
+ * Uses DomTrip for lossless XML editing that preserves formatting, comments, and whitespace.
+ *
+ * By default ({@code align=true}), this mojo auto-detects the project's dependency management
+ * conventions by analyzing existing dependencies:
+ *
+ * - If the project uses {@code } (most deps are version-less),
+ * the version is added to the managed section and a version-less entry to {@code }.
+ * - If existing versions use property references ({@code ${...}}), a property is created
+ * following the detected naming convention.
+ * - The managed dependency POM is discovered by walking the parent chain.
+ *
+ *
+ * Explicit flags ({@code -Dmanaged}, {@code -DuseProperty}, {@code -DpropertyName}) override
+ * auto-detected conventions.
+ *
+ * Examples:
+ *
+ * mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre
+ * mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre -Dscope=compile
+ * mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre -Dalign=false -Dmanaged
+ * mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre -DpropertyName=guava.version
+ *
+ *
+ * @since 3.11.0
+ */
+@Mojo(name = "add", requiresProject = true, threadSafe = true)
+public class AddDependencyMojo extends AbstractDependencyMojo {
+
+ /**
+ * The dependency coordinates: {@code groupId:artifactId[:version[:classifier[:type]]]}.
+ */
+ @Parameter(property = "gav", required = true)
+ private String gav;
+
+ /**
+ * When {@code true}, add to {@code } instead of {@code }.
+ * Overrides auto-detection from {@code align}.
+ */
+ @Parameter(property = "managed")
+ private Boolean managed;
+
+ /**
+ * The dependency scope (e.g., {@code compile}, {@code test}, {@code provided}, {@code runtime}, {@code system}).
+ * If not specified, no {@code } element is added (Maven defaults to {@code compile}).
+ */
+ @Parameter(property = "scope")
+ private String scope;
+
+ /**
+ * When {@code true}, store the version in a property and reference it from the dependency's
+ * {@code } element. Overrides auto-detection from {@code align}.
+ */
+ @Parameter(property = "useProperty")
+ private Boolean useProperty;
+
+ /**
+ * Explicit property name to use for the version (implies {@code useProperty=true}).
+ * Overrides auto-detection from {@code align}.
+ */
+ @Parameter(property = "propertyName")
+ private String propertyName;
+
+ /**
+ * When {@code true} (the default), auto-detect the project's dependency management conventions
+ * by analyzing existing dependencies. Detected conventions are:
+ *
+ * - Whether to use managed dependencies (based on whether existing deps are version-less)
+ * - Whether to use version properties (based on existing {@code ${...}} references)
+ * - Property naming convention (e.g., {@code artifactId.version} vs {@code version.artifactId})
+ * - Which POM to add managed dependencies to (walks parent chain)
+ *
+ */
+ @Parameter(property = "align", defaultValue = "true")
+ private boolean align;
+
+ @Inject
+ public AddDependencyMojo(MavenSession session, BuildContext buildContext, MavenProject project) {
+ super(session, buildContext, project);
+ }
+
+ @Override
+ protected void doExecute() throws MojoExecutionException, MojoFailureException {
+ Coordinates coords = parseCoordinates(gav);
+
+ Conventions conventions;
+ if (align) {
+ conventions = detectConventions(coords);
+ } else {
+ conventions = new Conventions();
+ }
+
+ // Explicit flags override detected conventions
+ boolean effectiveManaged = managed != null ? managed : conventions.useManaged;
+ boolean effectiveUseProperty =
+ propertyName != null || (useProperty != null ? useProperty : conventions.useProperty);
+ String effectivePropertyName = propertyName != null ? propertyName : conventions.propertyName;
+ File managedPomFile = conventions.managedPomFile;
+
+ if (effectiveUseProperty
+ && (coords.version() == null || coords.version().isEmpty())) {
+ throw new MojoFailureException("Version is required when using property-based versioning");
+ }
+
+ try {
+ if (effectiveManaged && managedPomFile != null) {
+ addManagedDependency(coords, managedPomFile, effectiveUseProperty, effectivePropertyName);
+ addVersionlessDependency(coords);
+ } else {
+ addDependencyToCurrentPom(coords, effectiveManaged, effectiveUseProperty, effectivePropertyName);
+ }
+ } catch (IOException e) {
+ throw new MojoExecutionException("Failed to modify POM file", e);
+ }
+ }
+
+ private void addManagedDependency(
+ Coordinates coords, File managedPomFile, boolean effectiveUseProperty, String effectivePropertyName)
+ throws IOException {
+ Document managedDoc = Document.of(managedPomFile.toPath());
+ PomEditor managedEditor = new PomEditor(managedDoc);
+
+ Coordinates managedCoords = coords;
+ if (effectiveUseProperty && coords.version() != null) {
+ String propName = effectivePropertyName != null ? effectivePropertyName : coords.artifactId() + ".version";
+ managedCoords = coords.withVersion("${" + propName + "}");
+ managedEditor.properties().updateProperty(true, propName, coords.version());
+ }
+
+ boolean changed = managedEditor.dependencies().updateManagedDependency(true, managedCoords);
+ if (changed) {
+ try (OutputStream os = Files.newOutputStream(managedPomFile.toPath())) {
+ managedDoc.toXml(os);
+ }
+ getLog().info("Added/updated " + coords.toGAV() + " in of " + managedPomFile);
+ }
+ }
+
+ private void addVersionlessDependency(Coordinates coords) throws IOException {
+ File pomFile = getProject().getFile();
+ Document doc = Document.of(pomFile.toPath());
+ PomEditor editor = new PomEditor(doc);
+
+ // Add dependency without version (version comes from managed deps)
+ Coordinates versionless =
+ Coordinates.of(coords.groupId(), coords.artifactId(), null, coords.classifier(), coords.type());
+ boolean changed = editor.dependencies().updateDependency(true, versionless);
+
+ if (scope != null && !scope.isEmpty()) {
+ setDependencyScope(editor, versionless);
+ changed = true;
+ }
+
+ if (changed) {
+ try (OutputStream os = Files.newOutputStream(pomFile.toPath())) {
+ doc.toXml(os);
+ }
+ getLog().info("Added " + coords.toGA() + " (version-less) in of " + pomFile);
+ }
+ }
+
+ private void addDependencyToCurrentPom(
+ Coordinates coords, boolean effectiveManaged, boolean effectiveUseProperty, String effectivePropertyName)
+ throws IOException {
+ File pomFile = getProject().getFile();
+ Document doc = Document.of(pomFile.toPath());
+ PomEditor editor = new PomEditor(doc);
+
+ Coordinates effectiveCoords = coords;
+ if (effectiveUseProperty && coords.version() != null) {
+ String propName = effectivePropertyName != null ? effectivePropertyName : coords.artifactId() + ".version";
+ effectiveCoords = coords.withVersion("${" + propName + "}");
+ editor.properties().updateProperty(true, propName, coords.version());
+ }
+
+ boolean changed;
+ if (effectiveManaged) {
+ changed = editor.dependencies().updateManagedDependency(true, effectiveCoords);
+ } else {
+ changed = editor.dependencies().updateDependency(true, effectiveCoords);
+ }
+
+ if (scope != null && !scope.isEmpty()) {
+ setDependencyScope(editor, effectiveCoords);
+ changed = true;
+ }
+
+ if (changed || effectiveUseProperty) {
+ try (OutputStream os = Files.newOutputStream(pomFile.toPath())) {
+ doc.toXml(os);
+ }
+ String section = effectiveManaged ? "" : "";
+ getLog().info("Added/updated " + coords.toGAV() + " in " + section);
+ } else {
+ getLog().info("No changes needed for " + coords.toGAV());
+ }
+ }
+
+ private void setDependencyScope(PomEditor editor, Coordinates coords) {
+ Element root = editor.document().root();
+ Element depsContainer;
+ if (managed != null && managed) {
+ Element dm = editor.findChildElement(root, "dependencyManagement");
+ depsContainer = dm != null ? editor.findChildElement(dm, "dependencies") : null;
+ } else {
+ depsContainer = editor.findChildElement(root, "dependencies");
+ }
+ if (depsContainer != null) {
+ depsContainer
+ .childElements("dependency")
+ .filter(coords.predicateGATC())
+ .findFirst()
+ .ifPresent(dep -> {
+ editor.updateOrCreateChildElement(dep, "scope", scope);
+ });
+ }
+ }
+
+ // --- Convention detection ---
+
+ static class Conventions {
+ boolean useManaged;
+ boolean useProperty;
+ String propertyName; // null means use detected pattern to generate
+ String propertyPattern; // e.g., "suffix:.version" or "prefix:version."
+ File managedPomFile;
+
+ String derivePropertyName(String artifactId) {
+ if (propertyName != null) {
+ return propertyName;
+ }
+ if (propertyPattern != null) {
+ if (propertyPattern.startsWith("suffix:")) {
+ return artifactId + propertyPattern.substring("suffix:".length());
+ } else if (propertyPattern.startsWith("prefix:")) {
+ return propertyPattern.substring("prefix:".length()) + artifactId;
+ }
+ }
+ return artifactId + ".version";
+ }
+ }
+
+ private Conventions detectConventions(Coordinates coords) {
+ Conventions conventions = new Conventions();
+ MavenProject project = getProject();
+
+ // 1. Analyze the current POM's dependencies for property and managed dep patterns
+ File pomFile = project.getFile();
+ if (pomFile != null && pomFile.exists()) {
+ try {
+ Document doc = Document.of(pomFile.toPath());
+ analyzePropertyPatterns(doc, conventions);
+ analyzeManagedDependencyUsage(doc, conventions);
+ } catch (Exception e) {
+ getLog().debug("Could not analyze POM conventions: " + e.getMessage());
+ }
+ }
+
+ // 2. Find the POM that owns by walking the parent chain
+ if (conventions.useManaged) {
+ conventions.managedPomFile = findManagedDependenciesPom(project);
+ if (conventions.managedPomFile == null) {
+ // No parent with managed deps found on disk — fall back to current POM
+ conventions.managedPomFile = pomFile;
+ }
+
+ // 3. Also scan the managed deps POM for property patterns (the child POM
+ // may have only version-less deps, so patterns live in the parent)
+ if (!conventions.useProperty
+ && conventions.managedPomFile != null
+ && !conventions.managedPomFile.equals(pomFile)) {
+ try {
+ Document managedDoc = Document.of(conventions.managedPomFile.toPath());
+ analyzePropertyPatterns(managedDoc, conventions);
+ } catch (Exception e) {
+ getLog().debug("Could not analyze managed POM conventions: " + e.getMessage());
+ }
+ }
+ }
+
+ // Use detected pattern for property name
+ if (conventions.useProperty) {
+ conventions.propertyName = conventions.derivePropertyName(coords.artifactId());
+ }
+
+ if (getLog().isDebugEnabled()) {
+ getLog().debug("Detected conventions: useManaged=" + conventions.useManaged + ", useProperty="
+ + conventions.useProperty + ", propertyPattern=" + conventions.propertyPattern
+ + ", managedPomFile=" + conventions.managedPomFile);
+ }
+
+ return conventions;
+ }
+
+ private void analyzePropertyPatterns(Document doc, Conventions conventions) {
+ Element root = doc.root();
+ Map patternCounts = new HashMap<>();
+ int propertyVersions = 0;
+ int totalVersions = 0;
+
+ // Scan
+ int[] directCounts = {0, 0};
+ scanVersionPatterns(root, "dependencies", patternCounts, directCounts);
+ propertyVersions += directCounts[0];
+ totalVersions += directCounts[1];
+
+ // Scan
+ Element dm = root.childElement("dependencyManagement").orElse(null);
+ if (dm != null) {
+ int[] counts = {0, 0};
+ scanVersionPatterns(dm, "dependencies", patternCounts, counts);
+ propertyVersions += counts[0];
+ totalVersions += counts[1];
+ }
+
+ // If majority of versioned deps use properties, adopt that convention
+ if (totalVersions > 0 && propertyVersions * 2 >= totalVersions) {
+ conventions.useProperty = true;
+ // Find dominant pattern
+ String dominantPattern = null;
+ int maxCount = 0;
+ for (Map.Entry entry : patternCounts.entrySet()) {
+ if (entry.getValue() > maxCount) {
+ maxCount = entry.getValue();
+ dominantPattern = entry.getKey();
+ }
+ }
+ conventions.propertyPattern = dominantPattern;
+ }
+ }
+
+ private void scanVersionPatterns(
+ Element parent, String containerName, Map patternCounts, int[] counts) {
+ Element container = parent.childElement(containerName).orElse(null);
+ if (container == null) {
+ return;
+ }
+ container.childElements("dependency").forEach(dep -> {
+ String artifactId = dep.childTextTrimmed("artifactId");
+ Element versionEl = dep.childElement("version").orElse(null);
+ if (versionEl != null) {
+ String version = versionEl.textContentTrimmed();
+ counts[1]++; // totalVersions
+ if (version != null && version.startsWith("${") && version.endsWith("}")) {
+ counts[0]++; // propertyVersions
+ String propName = version.substring(2, version.length() - 1);
+ String pattern = detectPattern(propName, artifactId);
+ if (pattern != null) {
+ patternCounts.merge(pattern, 1, Integer::sum);
+ }
+ }
+ }
+ });
+ }
+
+ static String detectPattern(String propertyName, String artifactId) {
+ if (artifactId == null) {
+ return null;
+ }
+ // Check exact suffix patterns: artifactId.version, artifactId-version
+ if (propertyName.equals(artifactId + ".version")) {
+ return "suffix:.version";
+ }
+ if (propertyName.equals(artifactId + "-version")) {
+ return "suffix:-version";
+ }
+ if (propertyName.equals("version." + artifactId)) {
+ return "prefix:version.";
+ }
+ // Check with simplified artifactId (strip common prefixes/suffixes)
+ String simplified = simplifyArtifactId(artifactId);
+ if (!simplified.equals(artifactId)) {
+ if (propertyName.equals(simplified + ".version")) {
+ return "suffix:.version";
+ }
+ if (propertyName.equals(simplified + "-version")) {
+ return "suffix:-version";
+ }
+ if (propertyName.equals("version." + simplified)) {
+ return "prefix:version.";
+ }
+ }
+ // Check if ends with .version, -version, or Version regardless
+ if (propertyName.endsWith(".version")) {
+ return "suffix:.version";
+ }
+ if (propertyName.endsWith("-version")) {
+ return "suffix:-version";
+ }
+ if (propertyName.endsWith("Version")) {
+ return "suffix:Version";
+ }
+ if (propertyName.startsWith("version.")) {
+ return "prefix:version.";
+ }
+ return null;
+ }
+
+ private static String simplifyArtifactId(String artifactId) {
+ // Remove common prefixes/suffixes that are often dropped in property names
+ String result = artifactId;
+ for (String prefix : new String[] {"maven-", "jakarta.", "javax."}) {
+ if (result.startsWith(prefix)) {
+ result = result.substring(prefix.length());
+ }
+ }
+ for (String suffix : new String[] {"-api", "-core", "-impl"}) {
+ if (result.endsWith(suffix)) {
+ result = result.substring(0, result.length() - suffix.length());
+ }
+ }
+ return result;
+ }
+
+ private void analyzeManagedDependencyUsage(Document doc, Conventions conventions) {
+ Element root = doc.root();
+ Element deps = root.childElement("dependencies").orElse(null);
+ if (deps == null) {
+ return;
+ }
+ int withVersion = 0;
+ int withoutVersion = 0;
+ for (Element dep :
+ (Iterable) () -> deps.childElements("dependency").iterator()) {
+ Element versionEl = dep.childElement("version").orElse(null);
+ if (versionEl != null) {
+ withVersion++;
+ } else {
+ withoutVersion++;
+ }
+ }
+ // If there are dependencies and majority are version-less, project uses managed deps
+ int total = withVersion + withoutVersion;
+ if (total > 0 && withoutVersion * 2 >= total) {
+ conventions.useManaged = true;
+ }
+ }
+
+ private File findManagedDependenciesPom(MavenProject project) {
+ MavenProject parent = project.getParent();
+ while (parent != null) {
+ File parentPom = parent.getFile();
+ if (parentPom != null && parentPom.exists()) {
+ try {
+ Document parentDoc = Document.of(parentPom.toPath());
+ Element root = parentDoc.root();
+ Element dm = root.childElement("dependencyManagement").orElse(null);
+ if (dm != null) {
+ return parentPom;
+ }
+ } catch (Exception e) {
+ getLog().debug("Could not read parent POM: " + parentPom);
+ }
+ }
+ parent = parent.getParent();
+ }
+ // If no parent has dependencyManagement, use current POM
+ return project.getFile();
+ }
+
+ static Coordinates parseCoordinates(String gav) throws MojoFailureException {
+ if (gav == null || gav.trim().isEmpty()) {
+ throw new MojoFailureException("GAV must not be empty. Use -Dgav=groupId:artifactId[:version]");
+ }
+ String[] parts = gav.split(":");
+ if (parts.length < 2 || parts.length > 5) {
+ throw new MojoFailureException(
+ "Invalid GAV format: '" + gav + "'. Expected groupId:artifactId[:version[:classifier[:type]]]");
+ }
+ String groupId = parts[0].trim();
+ String artifactId = parts[1].trim();
+ String version = parts.length >= 3 ? parts[2].trim() : null;
+ String classifier = parts.length >= 4 ? parts[3].trim() : null;
+ String type = parts.length >= 5 ? parts[4].trim() : null;
+ if (groupId.isEmpty() || artifactId.isEmpty()) {
+ throw new MojoFailureException("groupId and artifactId must not be empty");
+ }
+ if (version != null && version.isEmpty()) {
+ version = null;
+ }
+ if (classifier != null && classifier.isEmpty()) {
+ classifier = null;
+ }
+ if (type != null && type.isEmpty()) {
+ type = null;
+ }
+ return Coordinates.of(groupId, artifactId, version, classifier, type != null ? type : "jar");
+ }
+}
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..5df5434fd
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/dependency/RemoveDependencyMojo.java
@@ -0,0 +1,96 @@
+/*
+ * 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.nio.file.Files;
+
+import eu.maveniverse.domtrip.Document;
+import eu.maveniverse.domtrip.maven.Coordinates;
+import eu.maveniverse.domtrip.maven.PomEditor;
+import org.apache.maven.execution.MavenSession;
+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.sonatype.plexus.build.incremental.BuildContext;
+
+/**
+ * Removes a dependency from the project's {@code pom.xml}.
+ * Uses DomTrip for lossless XML editing that preserves formatting, comments, and whitespace.
+ *
+ * @since 3.11.0
+ */
+@Mojo(name = "remove", requiresProject = true, threadSafe = true)
+public class RemoveDependencyMojo extends AbstractDependencyMojo {
+
+ /**
+ * The dependency GAV coordinates: {@code groupId:artifactId[:version[:classifier[:type]]]}.
+ * Only groupId and artifactId are used for matching.
+ */
+ @Parameter(property = "gav", required = true)
+ private String gav;
+
+ /**
+ * When {@code true}, remove from {@code } instead of {@code }.
+ */
+ @Parameter(property = "managed", defaultValue = "false")
+ private boolean managed;
+
+ @Inject
+ public RemoveDependencyMojo(MavenSession session, BuildContext buildContext, MavenProject project) {
+ super(session, buildContext, project);
+ }
+
+ @Override
+ protected void doExecute() throws MojoExecutionException, MojoFailureException {
+ Coordinates coords = AddDependencyMojo.parseCoordinates(gav);
+ File pomFile = getProject().getFile();
+
+ try {
+ Document doc = Document.of(pomFile.toPath());
+ PomEditor editor = new PomEditor(doc);
+
+ boolean removed;
+ if (managed) {
+ removed = editor.dependencies().deleteManagedDependency(coords);
+ } else {
+ removed = editor.dependencies().deleteDependency(coords);
+ }
+
+ if (removed) {
+ try (OutputStream os = Files.newOutputStream(pomFile.toPath())) {
+ doc.toXml(os);
+ }
+ String section = managed ? "" : "";
+ getLog().info("Removed " + coords.toGA() + " from " + section);
+ } else {
+ String section = managed ? "" : "";
+ throw new MojoFailureException("Dependency " + coords.toGA() + " not found in " + section + ".");
+ }
+ } catch (IOException e) {
+ throw new MojoExecutionException("Failed to modify POM file: " + pomFile, e);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/dependency/SearchDependencyMojo.java b/src/main/java/org/apache/maven/plugins/dependency/SearchDependencyMojo.java
new file mode 100644
index 000000000..b95acbe7e
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/dependency/SearchDependencyMojo.java
@@ -0,0 +1,150 @@
+/*
+ * 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.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+
+import jakarta.json.Json;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonReader;
+import org.apache.maven.execution.MavenSession;
+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.sonatype.plexus.build.incremental.BuildContext;
+
+/**
+ * Searches Maven Central for artifacts matching a query.
+ *
+ * Uses the Sonatype Central Search API (Solr). Supports free-text search
+ * and field-based queries using Solr syntax.
+ *
+ * Examples:
+ *
+ * mvn dependency:search -Dquery=guava
+ * mvn dependency:search -Dquery="g:com.google.guava AND a:guava"
+ * mvn dependency:search -Dquery=guava -Drows=5
+ *
+ *
+ * @since 3.11.0
+ */
+@Mojo(name = "search", requiresProject = false, threadSafe = true)
+public class SearchDependencyMojo extends AbstractDependencyMojo {
+
+ private static final String SEARCH_URL = "https://central.sonatype.com/solrsearch/select";
+
+ /**
+ * The search query. Supports free-text (e.g., {@code guava}) or Solr field syntax
+ * (e.g., {@code g:com.google.guava AND a:guava}).
+ */
+ @Parameter(property = "query", required = true)
+ private String query;
+
+ /**
+ * Maximum number of results to return.
+ */
+ @Parameter(property = "rows", defaultValue = "20")
+ private int rows;
+
+ @Inject
+ public SearchDependencyMojo(MavenSession session, BuildContext buildContext, MavenProject project) {
+ super(session, buildContext, project);
+ }
+
+ @Override
+ protected void doExecute() throws MojoExecutionException, MojoFailureException {
+ getLog().info("Searching Maven Central for: " + query);
+
+ try {
+ String url = buildSearchUrl();
+ JsonObject response = executeSearch(url);
+ JsonObject responseBody = response.getJsonObject("response");
+ int totalHits = responseBody.getInt("numFound");
+ JsonArray docs = responseBody.getJsonArray("docs");
+
+ if (docs.isEmpty()) {
+ getLog().info("No results found.");
+ return;
+ }
+
+ getLog().info("Found " + totalHits + " result(s), showing " + docs.size() + ":");
+ getLog().info("");
+
+ // Print header
+ String format = "%-40s %-30s %-15s";
+ getLog().info(String.format(format, "GroupId", "ArtifactId", "Version"));
+ getLog().info(String.format(format, dashes(40), dashes(30), dashes(15)));
+
+ for (int i = 0; i < docs.size(); i++) {
+ JsonObject doc = docs.getJsonObject(i);
+ String groupId = doc.getString("g", "");
+ String artifactId = doc.getString("a", "");
+ String version = doc.getString("latestVersion", doc.getString("v", ""));
+ getLog().info(String.format(format, groupId, artifactId, version));
+ }
+
+ if (totalHits > docs.size()) {
+ getLog().info("");
+ getLog().info("... and " + (totalHits - docs.size()) + " more. Use -Drows=N to see more results.");
+ }
+ } catch (IOException e) {
+ throw new MojoExecutionException("Search failed: " + e.getMessage(), e);
+ }
+ }
+
+ private String buildSearchUrl() throws UnsupportedEncodingException {
+ return SEARCH_URL + "?q=" + URLEncoder.encode(query, "UTF-8") + "&rows=" + rows + "&wt=json";
+ }
+
+ private JsonObject executeSearch(String url) throws IOException, MojoExecutionException {
+ HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+ connection.setRequestMethod("GET");
+ connection.setRequestProperty("Accept", "application/json");
+ connection.setConnectTimeout(10_000);
+ connection.setReadTimeout(10_000);
+
+ int status = connection.getResponseCode();
+ if (status != 200) {
+ throw new MojoExecutionException("Search API returned HTTP " + status);
+ }
+
+ try (InputStream is = connection.getInputStream();
+ JsonReader reader = Json.createReader(is)) {
+ return reader.readObject();
+ }
+ }
+
+ private static String dashes(int count) {
+ StringBuilder sb = new StringBuilder(count);
+ for (int i = 0; i < count; i++) {
+ sb.append('-');
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/site/apt/examples/adding-removing-dependencies.apt.vm b/src/site/apt/examples/adding-removing-dependencies.apt.vm
new file mode 100644
index 000000000..692989c53
--- /dev/null
+++ b/src/site/apt/examples/adding-removing-dependencies.apt.vm
@@ -0,0 +1,164 @@
+~~ 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.
+
+ ------
+ Adding and Removing Dependencies
+ ------
+ Apache Maven Team
+ ------
+ 2024-01-01
+ ------
+
+Adding and Removing Dependencies
+
+%{toc|fromDepth=2}
+
+* Adding a dependency
+
+ The <<>> goal adds a dependency to the project's <<>> using lossless XML editing
+ that preserves formatting, comments, and whitespace.
+
+ The coordinates are specified using the <<>> parameter in the format
+ <<>>.
+
+** Basic usage
+
+---
+mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre
+---
+
+ This adds the following to your <<>>:
+
++---+
+
+
+ com.google.guava
+ guava
+ 33.0.0-jre
+
+
++---+
+
+** Adding with a scope
+
+ Use the <<>> parameter to set the dependency scope:
+
+---
+mvn dependency:add -Dgav=junit:junit:4.13.2 -Dscope=test
+---
+
+** Convention auto-detection (align)
+
+ By default, <<>> causes the goal to analyze existing dependencies and follow the
+ project's conventions:
+
+ * <>: If most existing dependencies are version-less (delegated to
+ <<<\>>>), the new dependency is added the same way.
+
+ * <>: If existing versions use property references (e.g., <<<$\{guava.version\}>>>),
+ a property is created following the detected naming convention.
+
+ * <>: In multi-module projects, managed dependencies and properties are added to the
+ appropriate parent POM.
+
+ []
+
+ For example, in a project where all dependencies use <<<$\{artifactId.version\}>>> properties
+ and managed dependencies:
+
+---
+mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre
+---
+
+ This will automatically:
+
+ [[1]] Add <<<\33.0.0-jre\>>> to <<<\>>>
+
+ [[2]] Add a managed dependency with <<<\$\{guava.version\}\>>> to <<<\>>>
+
+ [[3]] Add a version-less <<<\>>> entry to <<<\>>>
+
+ []
+
+** Multi-module projects
+
+ In a multi-module project, when running from a child module that inherits managed dependencies
+ from a parent, the goal will:
+
+ * Detect that existing dependencies are version-less (managed by parent)
+
+ * Walk the parent POM chain to find the POM with <<<\>>>
+
+ * Scan the parent POM's managed dependencies for property naming patterns
+
+ * Add the version property and managed dependency to the parent POM
+
+ * Add a version-less dependency to the child POM
+
+ []
+
+---
+cd child-module
+mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre
+---
+
+** Explicit overrides
+
+ You can override auto-detected conventions with explicit flags:
+
+ * <<<-Dmanaged=true>>> / <<<-Dmanaged=false>>>: Force or prevent using <<<\>>>
+
+ * <<<-DuseProperty=true>>> / <<<-DuseProperty=false>>>: Force or prevent using a version property
+
+ * <<<-DpropertyName=my.version>>>: Use a specific property name (implies <<>>)
+
+ * <<<-Dalign=false>>>: Disable convention detection entirely
+
+ []
+
+---
+mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre -Dalign=false -DpropertyName=guava.version
+---
+
+
+* Removing a dependency
+
+ The <<>> goal removes a dependency from the project's <<>>.
+ Only <<>> and <<>> are required.
+
+---
+mvn dependency:remove -Dgav=com.google.guava:guava
+---
+
+ To remove from <<<\>>> instead of <<<\>>>:
+
+---
+mvn dependency:remove -Dgav=com.google.guava:guava -Dmanaged=true
+---
+
+ The goal fails with an error if the specified dependency is not found in the POM.
+
+
+* Searching for dependencies
+
+ The <<>> goal searches Maven Central for artifacts. It does not require a project.
+
+---
+mvn dependency:search -Dquery=guava
+mvn dependency:search -Dquery="g:com.google.guava"
+mvn dependency:search -Dquery=guava -Drows=5
+---
diff --git a/src/site/apt/index.apt.vm b/src/site/apt/index.apt.vm
index b44c07f56..d1efd3391 100644
--- a/src/site/apt/index.apt.vm
+++ b/src/site/apt/index.apt.vm
@@ -34,6 +34,9 @@ ${project.name}
The Dependency plugin has several goals:
+ *{{{./add-mojo.html}dependency:add}} adds or updates a dependency in the project's <<>>, with optional
+ convention auto-detection for managed dependencies and version properties.
+
*{{{./analyze-mojo.html}dependency:analyze}} analyzes the dependencies of this project and determines which are: used
and declared; used and undeclared; unused and declared.
@@ -106,9 +109,13 @@ ${project.name}
*{{{./unpack-dependencies-mojo.html}dependency:unpack-dependencies}} like
copy-dependencies but unpacks.
+ *{{{./remove-mojo.html}dependency:remove}} removes a dependency from the project's <<>>.
+
*{{{./render-dependencies-mojo.html}dependency:render-dependencies}} like
build-classpath but with a custom Velocity template.
+ *{{{./search-mojo.html}dependency:search}} searches Maven Central for artifacts matching a query string.
+
[]
* Usage
@@ -157,6 +164,8 @@ ${project.name}
* {{{./examples/render-dependencies.html}Render Dependencies}}
+ * {{{./examples/adding-removing-dependencies.html}Adding and Removing Dependencies}}
+
[]
* Resources
diff --git a/src/site/apt/usage.apt.vm b/src/site/apt/usage.apt.vm
index 6519ea04d..5bccb325e 100644
--- a/src/site/apt/usage.apt.vm
+++ b/src/site/apt/usage.apt.vm
@@ -714,3 +714,87 @@ mvn dependency:analyze-exclusions
[WARNING] - javax.annotation:javax.annotation-api
[WARNING] - javax.activation:javax.activation-api
+---+
+
+
+* <<>>
+
+ This goal adds or updates a dependency in the project's <<>>. It uses lossless XML editing
+ (DomTrip) to preserve formatting, comments, and whitespace.
+
+ By default (<<>>), the goal auto-detects the project's dependency management conventions
+ by analyzing existing dependencies. If the project uses <<<\>>>, the version is
+ added to the managed section (in the appropriate parent POM) and a version-less entry to
+ <<<\>>>. If existing versions use property references (<<<$\{...\}>>>), a property is
+ created following the detected naming convention.
+
+ In its simplest form:
+
+---
+mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre
+---
+
+ With an explicit scope:
+
+---
+mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre -Dscope=test
+---
+
+ Disabling convention alignment and explicitly requesting a managed dependency:
+
+---
+mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre -Dalign=false -Dmanaged=true
+---
+
+ Using an explicit version property name:
+
+---
+mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre -DpropertyName=guava.version
+---
+
+ See {{{./examples/adding-removing-dependencies.html}Adding and Removing Dependencies}} for more
+ detailed examples including multi-module projects.
+
+
+* <<>>
+
+ This goal removes a dependency from the project's <<>>. Like <<>>, it uses
+ lossless XML editing to preserve formatting.
+
+---
+mvn dependency:remove -Dgav=com.google.guava:guava
+---
+
+ To remove from <<<\>>> instead of <<<\>>>:
+
+---
+mvn dependency:remove -Dgav=com.google.guava:guava -Dmanaged=true
+---
+
+ The goal will fail with a <<>> if the specified dependency is not found.
+
+
+* <<>>
+
+ This goal searches Maven Central for artifacts matching a query string. It does not require a project
+ and can be run from any directory.
+
+---
+mvn dependency:search -Dquery=guava
+---
+
+ By default, up to 20 results are displayed. Use the <<>> parameter to change the limit:
+
+---
+mvn dependency:search -Dquery=guava -Drows=5
+---
+
+ Sample output:
+
++---+
+[INFO] Search results for: guava
+[INFO] ----------
+[INFO] GroupId ArtifactId Version
+[INFO] com.google.guava guava 33.0.0-jre
+[INFO] com.google.guava guava-testlib 33.0.0-jre
+...
++---+
diff --git a/src/site/site.xml b/src/site/site.xml
index 969c6875a..c71104d53 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -44,6 +44,7 @@ under the License.
+