From ed36c9e2997f51279e6f277c1ea15c085fac0294 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 2 Apr 2026 11:50:16 +0200 Subject: [PATCH 1/6] Add dependency:add, dependency:remove and dependency:search goals Thin mojos using DomTrip for lossless POM editing (add/remove) and Sonatype Central Search API with jakarta.json for artifact search. - dependency:add supports -Dgav, -Dmanaged, -Dscope, -DuseProperty, -DpropertyName - dependency:remove supports -Dgav, -Dmanaged - dependency:search supports -Dquery, -Drows, works without a project Co-Authored-By: Claude Opus 4.6 --- pom.xml | 9 +- .../plugins/dependency/AddDependencyMojo.java | 204 ++++++++++++++++++ .../dependency/RemoveDependencyMojo.java | 96 +++++++++ .../dependency/SearchDependencyMojo.java | 150 +++++++++++++ 4 files changed, 458 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/apache/maven/plugins/dependency/AddDependencyMojo.java create mode 100644 src/main/java/org/apache/maven/plugins/dependency/RemoveDependencyMojo.java create mode 100644 src/main/java/org/apache/maven/plugins/dependency/SearchDependencyMojo.java diff --git a/pom.xml b/pom.xml index 327a618b1..f50489ee5 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,7 @@ under the License. 1.7.36 9.4.58.v20250814 4.11.0 + 0.6.0 4.11.0 3.5.1 @@ -198,6 +199,13 @@ under the License. 1.1.0 + + + eu.maveniverse.maven.domtrip + domtrip-maven + ${domtripVersion} + + org.apache.maven.shared @@ -319,7 +327,6 @@ under the License. org.glassfish jakarta.json 2.0.1 - test org.apache.maven.plugin-testing 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..9fc08baec --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/dependency/AddDependencyMojo.java @@ -0,0 +1,204 @@ +/* + * 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.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. + * + *

If the dependency already exists (matched by groupId, artifactId, type, and classifier), + * its version is updated. If the version is a property reference ({@code ${...}}), the property + * value is updated instead.

+ * + *

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 -Dmanaged
+ * mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre -DuseProperty
+ * 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 }. + */ + @Parameter(property = "managed", defaultValue = "false") + 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 (e.g., {@code ${artifactId.version}}) + * and reference it from the dependency's {@code } element. + * The property name is derived from the artifactId unless {@link #propertyName} is set. + */ + @Parameter(property = "useProperty", defaultValue = "false") + private boolean useProperty; + + /** + * Explicit property name to use for the version (implies {@code useProperty=true}). + * For example, {@code -DpropertyName=guava.version} creates {@code 33.0.0-jre} + * in {@code } and sets the dependency version to {@code ${guava.version}}. + */ + @Parameter(property = "propertyName") + private String propertyName; + + @Inject + public AddDependencyMojo(MavenSession session, BuildContext buildContext, MavenProject project) { + super(session, buildContext, project); + } + + @Override + protected void doExecute() throws MojoExecutionException, MojoFailureException { + Coordinates coords = parseCoordinates(gav); + File pomFile = getProject().getFile(); + + boolean wantProperty = useProperty || propertyName != null; + if (wantProperty && (coords.version() == null || coords.version().isEmpty())) { + throw new MojoFailureException("Version is required when using property-based versioning"); + } + + try { + Document doc = Document.of(pomFile.toPath()); + PomEditor editor = new PomEditor(doc); + + Coordinates effectiveCoords = coords; + if (wantProperty) { + String propName = propertyName != null ? propertyName : coords.artifactId() + ".version"; + // Use property reference as the version in the dependency element + effectiveCoords = Coordinates.of( + coords.groupId(), + coords.artifactId(), + "${" + propName + "}", + coords.classifier(), + coords.type()); + // Create or update the property with the actual version value + editor.properties().updateProperty(true, propName, coords.version()); + } + + boolean changed; + if (managed) { + 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 || wantProperty) { + try (OutputStream os = Files.newOutputStream(pomFile.toPath())) { + doc.toXml(os); + } + String section = managed ? "" : ""; + getLog().info("Added/updated " + coords.toGAV() + " in " + section); + } else { + getLog().info("No changes needed for " + coords.toGAV()); + } + } catch (IOException e) { + throw new MojoExecutionException("Failed to modify POM file: " + pomFile, e); + } + } + + private void setDependencyScope(PomEditor editor, Coordinates coords) { + Element root = editor.document().root(); + Element depsContainer; + if (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); + }); + } + } + + 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(); + } +} From 8ecc3b3dc60c0e76006b1ed72888a2452ca45ffa Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 2 Apr 2026 13:15:47 +0200 Subject: [PATCH 2/6] Add convention auto-detection (align=true by default) dependency:add now auto-detects the project's dependency management conventions by analyzing existing dependencies: - Detects if managed dependencies are used (version-less deps) - Detects if version properties are used and the naming pattern - Walks the parent chain to find the right POM for managed deps - Adds versioned entry to parent and version-less entry to current POM Explicit flags (-Dmanaged, -DuseProperty, -DpropertyName) override. Co-Authored-By: Claude Opus 4.6 --- .../plugins/dependency/AddDependencyMojo.java | 410 +++++++++++++++--- 1 file changed, 358 insertions(+), 52 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/dependency/AddDependencyMojo.java b/src/main/java/org/apache/maven/plugins/dependency/AddDependencyMojo.java index 9fc08baec..8ca2f008f 100644 --- a/src/main/java/org/apache/maven/plugins/dependency/AddDependencyMojo.java +++ b/src/main/java/org/apache/maven/plugins/dependency/AddDependencyMojo.java @@ -24,6 +24,8 @@ 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; @@ -41,16 +43,24 @@ * Adds or updates a dependency in the project's {@code pom.xml}. * Uses DomTrip for lossless XML editing that preserves formatting, comments, and whitespace. * - *

If the dependency already exists (matched by groupId, artifactId, type, and classifier), - * its version is updated. If the version is a property reference ({@code ${...}}), the property - * value is updated instead.

+ *

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 -Dmanaged
- * mvn dependency:add -Dgav=com.google.guava:guava:33.0.0-jre -DuseProperty
+ * 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
  * 
* @@ -67,9 +77,10 @@ public class AddDependencyMojo extends AbstractDependencyMojo { /** * When {@code true}, add to {@code } instead of {@code }. + * Overrides auto-detection from {@code align}. */ - @Parameter(property = "managed", defaultValue = "false") - private boolean managed; + @Parameter(property = "managed") + private Boolean managed; /** * The dependency scope (e.g., {@code compile}, {@code test}, {@code provided}, {@code runtime}, {@code system}). @@ -79,21 +90,32 @@ public class AddDependencyMojo extends AbstractDependencyMojo { private String scope; /** - * When {@code true}, store the version in a property (e.g., {@code ${artifactId.version}}) - * and reference it from the dependency's {@code } element. - * The property name is derived from the artifactId unless {@link #propertyName} is set. + * 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", defaultValue = "false") - private boolean useProperty; + @Parameter(property = "useProperty") + private Boolean useProperty; /** * Explicit property name to use for the version (implies {@code useProperty=true}). - * For example, {@code -DpropertyName=guava.version} creates {@code 33.0.0-jre} - * in {@code } and sets the dependency version to {@code ${guava.version}}. + * 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); @@ -102,61 +124,124 @@ public AddDependencyMojo(MavenSession session, BuildContext buildContext, MavenP @Override protected void doExecute() throws MojoExecutionException, MojoFailureException { Coordinates coords = parseCoordinates(gav); - File pomFile = getProject().getFile(); - boolean wantProperty = useProperty || propertyName != null; - if (wantProperty && (coords.version() == null || coords.version().isEmpty())) { + 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 { - Document doc = Document.of(pomFile.toPath()); - PomEditor editor = new PomEditor(doc); - - Coordinates effectiveCoords = coords; - if (wantProperty) { - String propName = propertyName != null ? propertyName : coords.artifactId() + ".version"; - // Use property reference as the version in the dependency element - effectiveCoords = Coordinates.of( - coords.groupId(), - coords.artifactId(), - "${" + propName + "}", - coords.classifier(), - coords.type()); - // Create or update the property with the actual version value - editor.properties().updateProperty(true, propName, coords.version()); - } - - boolean changed; - if (managed) { - changed = editor.dependencies().updateManagedDependency(true, effectiveCoords); + if (effectiveManaged && managedPomFile != null) { + addManagedDependency(coords, managedPomFile, effectiveUseProperty, effectivePropertyName); + addVersionlessDependency(coords); } else { - changed = editor.dependencies().updateDependency(true, effectiveCoords); + 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); - if (scope != null && !scope.isEmpty()) { - setDependencyScope(editor, effectiveCoords); - changed = true; + 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); + } + } - if (changed || wantProperty) { - try (OutputStream os = Files.newOutputStream(pomFile.toPath())) { - doc.toXml(os); - } - String section = managed ? "" : ""; - getLog().info("Added/updated " + coords.toGAV() + " in " + section); - } else { - getLog().info("No changes needed for " + coords.toGAV()); + 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); } - } catch (IOException e) { - throw new MojoExecutionException("Failed to modify POM file: " + pomFile, e); + 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) { + if (managed != null && managed) { Element dm = editor.findChildElement(root, "dependencyManagement"); depsContainer = dm != null ? editor.findChildElement(dm, "dependencies") : null; } else { @@ -173,6 +258,227 @@ private void setDependencyScope(PomEditor editor, Coordinates coords) { } } + // --- 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; + } + } + + // 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 and + scanVersionPatterns(root, "dependencies", patternCounts, new int[] {0, 0}); + Element dm = root.childElement("dependencyManagement").orElse(null); + if (dm != null) { + int[] counts = {0, 0}; // [propertyVersions, totalVersions] + scanVersionPatterns(dm, "dependencies", patternCounts, counts); + propertyVersions += counts[0]; + totalVersions += counts[1]; + } + + // Also count from directly + int[] directCounts = {0, 0}; + scanVersionPatterns(root, "dependencies", patternCounts, directCounts); + propertyVersions += directCounts[0]; + totalVersions += directCounts[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 suffix patterns: artifactId.version, artifactId-version, artifactIdVersion + 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("version." + simplified)) { + return "prefix:version."; + } + } + // Check if ends with .version or Version regardless + 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]"); From af588ae82c390ec323f1987a7411587e0579bdd8 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 2 Apr 2026 14:03:12 +0200 Subject: [PATCH 3/6] Add integration tests for add, remove and search goals 9 ITs covering: - add-dependency: basic add to existing POM - add-dependency-managed: add to dependencyManagement - add-dependency-scope: add with scope - add-dependency-property: add with version property - add-dependency-align: auto-detect managed deps + property convention - remove-dependency: remove from dependencies - remove-dependency-managed: remove from dependencyManagement - remove-dependency-not-found: expected failure when dep doesn't exist - search-dependency: search Maven Central (requires network) Co-Authored-By: Claude Opus 4.6 --- .../add-dependency-align/invoker.properties | 18 +++++ src/it/projects/add-dependency-align/pom.xml | 81 +++++++++++++++++++ .../add-dependency-align/test.properties | 18 +++++ .../add-dependency-align/verify.groovy | 52 ++++++++++++ .../add-dependency-managed/invoker.properties | 18 +++++ .../projects/add-dependency-managed/pom.xml | 32 ++++++++ .../add-dependency-managed/test.properties | 20 +++++ .../add-dependency-managed/verify.groovy | 33 ++++++++ .../invoker.properties | 18 +++++ .../projects/add-dependency-property/pom.xml | 32 ++++++++ .../add-dependency-property/test.properties | 20 +++++ .../add-dependency-property/verify.groovy | 36 +++++++++ .../add-dependency-scope/invoker.properties | 18 +++++ src/it/projects/add-dependency-scope/pom.xml | 32 ++++++++ .../add-dependency-scope/test.properties | 20 +++++ .../add-dependency-scope/verify.groovy | 28 +++++++ .../add-dependency/invoker.properties | 18 +++++ src/it/projects/add-dependency/pom.xml | 42 ++++++++++ .../projects/add-dependency/test.properties | 19 +++++ src/it/projects/add-dependency/verify.groovy | 38 +++++++++ .../invoker.properties | 18 +++++ .../remove-dependency-managed/pom.xml | 47 +++++++++++ .../remove-dependency-managed/test.properties | 19 +++++ .../remove-dependency-managed/verify.groovy | 32 ++++++++ .../invoker.properties | 19 +++++ .../remove-dependency-not-found/pom.xml | 40 +++++++++ .../test.properties | 18 +++++ .../remove-dependency-not-found/verify.groovy | 30 +++++++ .../remove-dependency/invoker.properties | 18 +++++ src/it/projects/remove-dependency/pom.xml | 46 +++++++++++ .../remove-dependency/test.properties | 18 +++++ .../projects/remove-dependency/verify.groovy | 36 +++++++++ .../search-dependency/invoker.properties | 20 +++++ src/it/projects/search-dependency/pom.xml | 32 ++++++++ .../search-dependency/test.properties | 19 +++++ .../projects/search-dependency/verify.groovy | 33 ++++++++ 36 files changed, 1038 insertions(+) create mode 100644 src/it/projects/add-dependency-align/invoker.properties create mode 100644 src/it/projects/add-dependency-align/pom.xml create mode 100644 src/it/projects/add-dependency-align/test.properties create mode 100644 src/it/projects/add-dependency-align/verify.groovy create mode 100644 src/it/projects/add-dependency-managed/invoker.properties create mode 100644 src/it/projects/add-dependency-managed/pom.xml create mode 100644 src/it/projects/add-dependency-managed/test.properties create mode 100644 src/it/projects/add-dependency-managed/verify.groovy create mode 100644 src/it/projects/add-dependency-property/invoker.properties create mode 100644 src/it/projects/add-dependency-property/pom.xml create mode 100644 src/it/projects/add-dependency-property/test.properties create mode 100644 src/it/projects/add-dependency-property/verify.groovy create mode 100644 src/it/projects/add-dependency-scope/invoker.properties create mode 100644 src/it/projects/add-dependency-scope/pom.xml create mode 100644 src/it/projects/add-dependency-scope/test.properties create mode 100644 src/it/projects/add-dependency-scope/verify.groovy create mode 100644 src/it/projects/add-dependency/invoker.properties create mode 100644 src/it/projects/add-dependency/pom.xml create mode 100644 src/it/projects/add-dependency/test.properties create mode 100644 src/it/projects/add-dependency/verify.groovy create mode 100644 src/it/projects/remove-dependency-managed/invoker.properties create mode 100644 src/it/projects/remove-dependency-managed/pom.xml create mode 100644 src/it/projects/remove-dependency-managed/test.properties create mode 100644 src/it/projects/remove-dependency-managed/verify.groovy create mode 100644 src/it/projects/remove-dependency-not-found/invoker.properties create mode 100644 src/it/projects/remove-dependency-not-found/pom.xml create mode 100644 src/it/projects/remove-dependency-not-found/test.properties create mode 100644 src/it/projects/remove-dependency-not-found/verify.groovy create mode 100644 src/it/projects/remove-dependency/invoker.properties create mode 100644 src/it/projects/remove-dependency/pom.xml create mode 100644 src/it/projects/remove-dependency/test.properties create mode 100644 src/it/projects/remove-dependency/verify.groovy create mode 100644 src/it/projects/search-dependency/invoker.properties create mode 100644 src/it/projects/search-dependency/pom.xml create mode 100644 src/it/projects/search-dependency/test.properties create mode 100644 src/it/projects/search-dependency/verify.groovy diff --git a/src/it/projects/add-dependency-align/invoker.properties b/src/it/projects/add-dependency-align/invoker.properties new file mode 100644 index 000000000..eb6424ecd --- /dev/null +++ b/src/it/projects/add-dependency-align/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-align/pom.xml b/src/it/projects/add-dependency-align/pom.xml new file mode 100644 index 000000000..0b0d07eac --- /dev/null +++ b/src/it/projects/add-dependency-align/pom.xml @@ -0,0 +1,81 @@ + + + + + + + + + 4.0.0 + + org.apache.maven.plugins.dependency.its + add-dependency-align + 1.0-SNAPSHOT + + + 4.13.2 + 2.0.9 + 2.15.0 + + + + + + junit + junit + ${junit.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + commons-io + commons-io + ${commons-io.version} + + + + + + + + junit + junit + test + + + org.slf4j + slf4j-api + + + commons-io + commons-io + + + + diff --git a/src/it/projects/add-dependency-align/test.properties b/src/it/projects/add-dependency-align/test.properties new file mode 100644 index 000000000..7e838cc09 --- /dev/null +++ b/src/it/projects/add-dependency-align/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=com.google.guava:guava:33.0.0-jre diff --git a/src/it/projects/add-dependency-align/verify.groovy b/src/it/projects/add-dependency-align/verify.groovy new file mode 100644 index 000000000..2f5bcec92 --- /dev/null +++ b/src/it/projects/add-dependency-align/verify.groovy @@ -0,0 +1,52 @@ +/* + * 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 pomText = new File( basedir, "pom.xml" ).text +def pom = new XmlSlurper().parse( new File( basedir, "pom.xml" ) ) + +// Verify auto-detection created a version property following the .version convention +assert pomText.contains( '33.0.0-jre' ) : + "Should have created guava.version property (matching existing .version suffix pattern)" + +// Verify managed dependency was added with property reference +def managedDeps = pom.dependencyManagement.dependencies.dependency +def managedGuava = managedDeps.find { it.artifactId == 'guava' } +assert managedGuava != null : "guava should be in dependencyManagement" +assert pomText.contains( '${guava.version}' ) : + "Managed dependency version should reference \${guava.version}" + +// Verify version-less dependency was added to regular dependencies +def deps = pom.dependencies.dependency +def guava = deps.find { it.artifactId == 'guava' } +assert guava != null : "guava should be in dependencies" + +// Verify the regular dependency is version-less (version comes from managed deps) +// In the raw XML, the dependency entry should NOT have a element +def depsSection = pomText.substring( pomText.lastIndexOf( '' ) ) +def guavaBlock = depsSection.substring( depsSection.indexOf( 'guava' ) ) +guavaBlock = guavaBlock.substring( 0, guavaBlock.indexOf( '
' ) ) +assert !guavaBlock.contains( '' ) : + "Regular dependency should be version-less (managed by dependencyManagement)" + +// Verify existing dependencies are preserved +assert deps.find { it.artifactId == 'junit' } != null +assert deps.find { it.artifactId == 'slf4j-api' } != null +assert deps.find { it.artifactId == 'commons-io' } != null + +return true diff --git a/src/it/projects/add-dependency-managed/invoker.properties b/src/it/projects/add-dependency-managed/invoker.properties new file mode 100644 index 000000000..eb6424ecd --- /dev/null +++ b/src/it/projects/add-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}:add diff --git a/src/it/projects/add-dependency-managed/pom.xml b/src/it/projects/add-dependency-managed/pom.xml new file mode 100644 index 000000000..5c4f4d072 --- /dev/null +++ b/src/it/projects/add-dependency-managed/pom.xml @@ -0,0 +1,32 @@ + + + + + + + 4.0.0 + + org.apache.maven.plugins.dependency.its + add-dependency-managed + 1.0-SNAPSHOT + + diff --git a/src/it/projects/add-dependency-managed/test.properties b/src/it/projects/add-dependency-managed/test.properties new file mode 100644 index 000000000..60d69c9af --- /dev/null +++ b/src/it/projects/add-dependency-managed/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=org.apache.commons:commons-lang3:3.12.0 +managed=true +align=false diff --git a/src/it/projects/add-dependency-managed/verify.groovy b/src/it/projects/add-dependency-managed/verify.groovy new file mode 100644 index 000000000..8e9287ee9 --- /dev/null +++ b/src/it/projects/add-dependency-managed/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 pom = new XmlSlurper().parse( new File( basedir, "pom.xml" ) ) + +// Verify dependency was added to dependencyManagement +def managedDeps = pom.dependencyManagement.dependencies.dependency +def lang3 = managedDeps.find { it.groupId == 'org.apache.commons' && it.artifactId == 'commons-lang3' } +assert lang3 != null : "commons-lang3 should be in dependencyManagement" +assert lang3.version == '3.12.0' + +// Verify it was NOT added to regular dependencies +def regularDeps = pom.dependencies.dependency +def regularLang3 = regularDeps.find { it.groupId == 'org.apache.commons' && it.artifactId == 'commons-lang3' } +assert regularLang3.isEmpty() : "commons-lang3 should NOT be in regular dependencies" + +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..e87f2413d --- /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 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..27dba4f1b --- /dev/null +++ b/src/it/projects/add-dependency-scope/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=org.junit.jupiter:junit-jupiter-api:5.10.0 +scope=test +align=false 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..d7f9229f3 --- /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 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..3fe9ada82 --- /dev/null +++ b/src/it/projects/add-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. + +gav=org.apache.commons:commons-lang3:3.12.0 +align=false diff --git a/src/it/projects/add-dependency/verify.groovy b/src/it/projects/add-dependency/verify.groovy new file mode 100644 index 000000000..7e7b11986 --- /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 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..9a6342154 --- /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 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..73c8f4a72 --- /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 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..bd6c6a3da --- /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 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 From 3185fbc1b51f49d86abadc101de9fe69e58fd7fe Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 2 Apr 2026 14:12:05 +0200 Subject: [PATCH 4/6] Fix ITs: use FQCN for Groovy XmlSlurper, declare domtrip-core dependency - Use groovy.xml.XmlSlurper (required in Groovy 4+) - Add domtrip-core as explicit dependency (used transitively via domtrip-maven) Co-Authored-By: Claude Opus 4.6 --- pom.xml | 5 +++++ src/it/projects/add-dependency-align/verify.groovy | 2 +- src/it/projects/add-dependency-managed/verify.groovy | 2 +- src/it/projects/add-dependency-property/verify.groovy | 2 +- src/it/projects/add-dependency-scope/verify.groovy | 2 +- src/it/projects/add-dependency/verify.groovy | 2 +- src/it/projects/remove-dependency-managed/verify.groovy | 2 +- src/it/projects/remove-dependency-not-found/verify.groovy | 2 +- src/it/projects/remove-dependency/verify.groovy | 2 +- 9 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index f50489ee5..c43938056 100644 --- a/pom.xml +++ b/pom.xml @@ -200,6 +200,11 @@ under the License. + + eu.maveniverse.maven.domtrip + domtrip-core + ${domtripVersion} + eu.maveniverse.maven.domtrip domtrip-maven diff --git a/src/it/projects/add-dependency-align/verify.groovy b/src/it/projects/add-dependency-align/verify.groovy index 2f5bcec92..831031b61 100644 --- a/src/it/projects/add-dependency-align/verify.groovy +++ b/src/it/projects/add-dependency-align/verify.groovy @@ -18,7 +18,7 @@ */ def pomText = new File( basedir, "pom.xml" ).text -def pom = new XmlSlurper().parse( new File( basedir, "pom.xml" ) ) +def pom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) ) // Verify auto-detection created a version property following the .version convention assert pomText.contains( '33.0.0-jre' ) : diff --git a/src/it/projects/add-dependency-managed/verify.groovy b/src/it/projects/add-dependency-managed/verify.groovy index 8e9287ee9..e56b238a9 100644 --- a/src/it/projects/add-dependency-managed/verify.groovy +++ b/src/it/projects/add-dependency-managed/verify.groovy @@ -17,7 +17,7 @@ * under the License. */ -def pom = new XmlSlurper().parse( new File( basedir, "pom.xml" ) ) +def pom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) ) // Verify dependency was added to dependencyManagement def managedDeps = pom.dependencyManagement.dependencies.dependency diff --git a/src/it/projects/add-dependency-property/verify.groovy b/src/it/projects/add-dependency-property/verify.groovy index e87f2413d..0dc4c17f6 100644 --- a/src/it/projects/add-dependency-property/verify.groovy +++ b/src/it/projects/add-dependency-property/verify.groovy @@ -29,7 +29,7 @@ assert pomText.contains( '${guava.version}' ) : "Dependency version should reference \${guava.version}" // Also verify via XML parsing that the dependency exists -def pom = new XmlSlurper().parse( new File( basedir, "pom.xml" ) ) +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" diff --git a/src/it/projects/add-dependency-scope/verify.groovy b/src/it/projects/add-dependency-scope/verify.groovy index d7f9229f3..957b260fd 100644 --- a/src/it/projects/add-dependency-scope/verify.groovy +++ b/src/it/projects/add-dependency-scope/verify.groovy @@ -17,7 +17,7 @@ * under the License. */ -def pom = new XmlSlurper().parse( new File( basedir, "pom.xml" ) ) +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' } diff --git a/src/it/projects/add-dependency/verify.groovy b/src/it/projects/add-dependency/verify.groovy index 7e7b11986..f78dd9977 100644 --- a/src/it/projects/add-dependency/verify.groovy +++ b/src/it/projects/add-dependency/verify.groovy @@ -17,7 +17,7 @@ * under the License. */ -def pom = new XmlSlurper().parse( new File( basedir, "pom.xml" ) ) +def pom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) ) def deps = pom.dependencies.dependency // Verify existing dependency is preserved diff --git a/src/it/projects/remove-dependency-managed/verify.groovy b/src/it/projects/remove-dependency-managed/verify.groovy index 9a6342154..98fd4c3ba 100644 --- a/src/it/projects/remove-dependency-managed/verify.groovy +++ b/src/it/projects/remove-dependency-managed/verify.groovy @@ -17,7 +17,7 @@ * under the License. */ -def pom = new XmlSlurper().parse( new File( basedir, "pom.xml" ) ) +def pom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) ) def managedDeps = pom.dependencyManagement.dependencies.dependency // Verify junit was removed from dependencyManagement diff --git a/src/it/projects/remove-dependency-not-found/verify.groovy b/src/it/projects/remove-dependency-not-found/verify.groovy index 73c8f4a72..503387554 100644 --- a/src/it/projects/remove-dependency-not-found/verify.groovy +++ b/src/it/projects/remove-dependency-not-found/verify.groovy @@ -22,7 +22,7 @@ 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 XmlSlurper().parse( new File( basedir, "pom.xml" ) ) +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' diff --git a/src/it/projects/remove-dependency/verify.groovy b/src/it/projects/remove-dependency/verify.groovy index bd6c6a3da..0becc727d 100644 --- a/src/it/projects/remove-dependency/verify.groovy +++ b/src/it/projects/remove-dependency/verify.groovy @@ -17,7 +17,7 @@ * under the License. */ -def pom = new XmlSlurper().parse( new File( basedir, "pom.xml" ) ) +def pom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) ) def deps = pom.dependencies.dependency // Verify junit was removed From f2645c598221035ec3b1e4008c40908f86c9f14b Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 2 Apr 2026 14:18:50 +0200 Subject: [PATCH 5/6] Improve IT coverage for convention auto-detection - Remove align=false from basic add-dependency and add-dependency-scope ITs so they exercise the default align=true code path - Add add-dependency-align-properties IT: tests auto-detection of property naming convention (.version suffix) without managed deps Co-Authored-By: Claude Opus 4.6 --- .../invoker.properties | 18 ++++++ .../add-dependency-align-properties/pom.xml | 62 +++++++++++++++++++ .../test.properties | 18 ++++++ .../verify.groovy | 45 ++++++++++++++ .../add-dependency-scope/test.properties | 1 - .../projects/add-dependency/test.properties | 1 - 6 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 src/it/projects/add-dependency-align-properties/invoker.properties create mode 100644 src/it/projects/add-dependency-align-properties/pom.xml create mode 100644 src/it/projects/add-dependency-align-properties/test.properties create mode 100644 src/it/projects/add-dependency-align-properties/verify.groovy diff --git a/src/it/projects/add-dependency-align-properties/invoker.properties b/src/it/projects/add-dependency-align-properties/invoker.properties new file mode 100644 index 000000000..eb6424ecd --- /dev/null +++ b/src/it/projects/add-dependency-align-properties/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-align-properties/pom.xml b/src/it/projects/add-dependency-align-properties/pom.xml new file mode 100644 index 000000000..d7051973c --- /dev/null +++ b/src/it/projects/add-dependency-align-properties/pom.xml @@ -0,0 +1,62 @@ + + + + + + + + + 4.0.0 + + org.apache.maven.plugins.dependency.its + add-dependency-align-properties + 1.0-SNAPSHOT + + + 4.13.2 + 2.0.9 + 2.15.0 + + + + + junit + junit + ${junit.version} + test + + + org.slf4j + slf4j-api + ${slf4j.version} + + + commons-io + commons-io + ${commons-io.version} + + + + diff --git a/src/it/projects/add-dependency-align-properties/test.properties b/src/it/projects/add-dependency-align-properties/test.properties new file mode 100644 index 000000000..7e838cc09 --- /dev/null +++ b/src/it/projects/add-dependency-align-properties/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=com.google.guava:guava:33.0.0-jre diff --git a/src/it/projects/add-dependency-align-properties/verify.groovy b/src/it/projects/add-dependency-align-properties/verify.groovy new file mode 100644 index 000000000..daa2606de --- /dev/null +++ b/src/it/projects/add-dependency-align-properties/verify.groovy @@ -0,0 +1,45 @@ +/* + * 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 pomText = new File( basedir, "pom.xml" ).text +def pom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) ) + +// Align should detect that all 3 existing deps use .version property convention +// and create a guava.version property for the new dependency +assert pomText.contains( '33.0.0-jre' ) : + "Should have created guava.version property (matching existing .version suffix convention)" +assert pomText.contains( '${guava.version}' ) : + "Dependency version should reference \${guava.version}" + +// Align should NOT use managed dependencies (all existing deps have explicit versions) +def managedDeps = pom.dependencyManagement.dependencies.dependency +assert managedDeps.isEmpty() : + "Should NOT have created dependencyManagement (existing deps all have versions)" + +// Verify the dependency was added to regular dependencies +def dep = pom.dependencies.dependency.find { it.artifactId == 'guava' } +assert dep != null : "guava should be in dependencies" + +// Verify existing deps are preserved +assert pom.dependencies.dependency.size() == 4 : "Should have 4 dependencies total" +assert pomText.contains( '${junit.version}' ) : "Existing junit property ref preserved" +assert pomText.contains( '${slf4j.version}' ) : "Existing slf4j property ref preserved" +assert pomText.contains( '${commons-io.version}' ) : "Existing commons-io property ref preserved" + +return true diff --git a/src/it/projects/add-dependency-scope/test.properties b/src/it/projects/add-dependency-scope/test.properties index 27dba4f1b..1d0099685 100644 --- a/src/it/projects/add-dependency-scope/test.properties +++ b/src/it/projects/add-dependency-scope/test.properties @@ -17,4 +17,3 @@ gav=org.junit.jupiter:junit-jupiter-api:5.10.0 scope=test -align=false diff --git a/src/it/projects/add-dependency/test.properties b/src/it/projects/add-dependency/test.properties index 3fe9ada82..3ed9ae987 100644 --- a/src/it/projects/add-dependency/test.properties +++ b/src/it/projects/add-dependency/test.properties @@ -16,4 +16,3 @@ # under the License. gav=org.apache.commons:commons-lang3:3.12.0 -align=false From 0315cac36fbf40cf4627c54311440d3e9241eb93 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 2 Apr 2026 23:36:35 +0200 Subject: [PATCH 6/6] Fix multi-module property detection, add -version pattern, add site docs - Scan parent POM for property patterns when child has version-less deps - Fix double-scan bug in analyzePropertyPatterns - Recognize -version suffix convention (e.g. Camel's ${jackson-version}) - Add multi-module ITs for both .version and -version conventions - Add site documentation for add, remove and search goals Co-Authored-By: Claude Opus 4.6 --- .../invoker.properties | 18 ++ .../parent/pom.xml | 62 +++++++ .../add-dependency-multimodule-dash/pom.xml | 57 ++++++ .../test.properties | 18 ++ .../verify.groovy | 61 +++++++ .../invoker.properties | 18 ++ .../add-dependency-multimodule/parent/pom.xml | 59 +++++++ .../add-dependency-multimodule/pom.xml | 60 +++++++ .../test.properties | 18 ++ .../add-dependency-multimodule/verify.groovy | 72 ++++++++ .../plugins/dependency/AddDependencyMojo.java | 43 +++-- .../adding-removing-dependencies.apt.vm | 164 ++++++++++++++++++ src/site/apt/index.apt.vm | 9 + src/site/apt/usage.apt.vm | 84 +++++++++ src/site/site.xml | 1 + 15 files changed, 733 insertions(+), 11 deletions(-) create mode 100644 src/it/projects/add-dependency-multimodule-dash/invoker.properties create mode 100644 src/it/projects/add-dependency-multimodule-dash/parent/pom.xml create mode 100644 src/it/projects/add-dependency-multimodule-dash/pom.xml create mode 100644 src/it/projects/add-dependency-multimodule-dash/test.properties create mode 100644 src/it/projects/add-dependency-multimodule-dash/verify.groovy create mode 100644 src/it/projects/add-dependency-multimodule/invoker.properties create mode 100644 src/it/projects/add-dependency-multimodule/parent/pom.xml create mode 100644 src/it/projects/add-dependency-multimodule/pom.xml create mode 100644 src/it/projects/add-dependency-multimodule/test.properties create mode 100644 src/it/projects/add-dependency-multimodule/verify.groovy create mode 100644 src/site/apt/examples/adding-removing-dependencies.apt.vm diff --git a/src/it/projects/add-dependency-multimodule-dash/invoker.properties b/src/it/projects/add-dependency-multimodule-dash/invoker.properties new file mode 100644 index 000000000..eb6424ecd --- /dev/null +++ b/src/it/projects/add-dependency-multimodule-dash/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-multimodule-dash/parent/pom.xml b/src/it/projects/add-dependency-multimodule-dash/parent/pom.xml new file mode 100644 index 000000000..a0faab275 --- /dev/null +++ b/src/it/projects/add-dependency-multimodule-dash/parent/pom.xml @@ -0,0 +1,62 @@ + + + + + + + + + 4.0.0 + + org.apache.maven.plugins.dependency.its + multimodule-dash-parent + 1.0-SNAPSHOT + pom + + + 4.13.2 + 2.0.9 + 2.15.0 + + + + + + junit + junit + ${junit-version} + + + org.slf4j + slf4j-api + ${slf4j-api-version} + + + commons-io + commons-io + ${commons-io-version} + + + + + diff --git a/src/it/projects/add-dependency-multimodule-dash/pom.xml b/src/it/projects/add-dependency-multimodule-dash/pom.xml new file mode 100644 index 000000000..03ccb38f2 --- /dev/null +++ b/src/it/projects/add-dependency-multimodule-dash/pom.xml @@ -0,0 +1,57 @@ + + + + + + + + + 4.0.0 + + + org.apache.maven.plugins.dependency.its + multimodule-dash-parent + 1.0-SNAPSHOT + parent/pom.xml + + + multimodule-dash-child + + + + junit + junit + test + + + org.slf4j + slf4j-api + + + commons-io + commons-io + + + + diff --git a/src/it/projects/add-dependency-multimodule-dash/test.properties b/src/it/projects/add-dependency-multimodule-dash/test.properties new file mode 100644 index 000000000..7e838cc09 --- /dev/null +++ b/src/it/projects/add-dependency-multimodule-dash/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=com.google.guava:guava:33.0.0-jre diff --git a/src/it/projects/add-dependency-multimodule-dash/verify.groovy b/src/it/projects/add-dependency-multimodule-dash/verify.groovy new file mode 100644 index 000000000..f60289818 --- /dev/null +++ b/src/it/projects/add-dependency-multimodule-dash/verify.groovy @@ -0,0 +1,61 @@ +/* + * 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. + */ + +// --- Verify parent POM was modified --- +def parentPomText = new File( basedir, "parent/pom.xml" ).text +def parentPom = new groovy.xml.XmlSlurper().parse( new File( basedir, "parent/pom.xml" ) ) + +// 1. Property should use dash-version convention (matching existing pattern) +assert parentPomText.contains( '33.0.0-jre' ) : + "Parent should have guava-version property (dash convention)" + +// 2. Managed dependency should reference property with dash convention +def managedDeps = parentPom.dependencyManagement.dependencies.dependency +def managedGuava = managedDeps.find { it.artifactId == 'guava' } +assert managedGuava != null : "guava should be in parent's dependencyManagement" +assert parentPomText.contains( '${guava-version}' ) : + "Parent managed dependency version should reference \${guava-version}" + +// 3. Existing managed dependencies should be preserved +assert managedDeps.find { it.artifactId == 'junit' } != null : "junit should still be managed" +assert managedDeps.find { it.artifactId == 'slf4j-api' } != null : "slf4j-api should still be managed" +assert managedDeps.find { it.artifactId == 'commons-io' } != null : "commons-io should still be managed" +assert parentPomText.contains( '${junit-version}' ) : "Existing junit-version property ref preserved" +assert parentPomText.contains( '${slf4j-api-version}' ) : "Existing slf4j-api-version property ref preserved" + +// --- Verify child POM was modified --- +def childPomText = new File( basedir, "pom.xml" ).text +def childPom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) ) + +// 4. Version-less dependency should have been added to child +def childDeps = childPom.dependencies.dependency +def childGuava = childDeps.find { it.artifactId == 'guava' } +assert childGuava != null : "guava should be in child dependencies" + +// 5. The child dependency should be version-less +def childDepsSection = childPomText.substring( childPomText.lastIndexOf( '' ) ) +def guavaBlock = childDepsSection.substring( childDepsSection.indexOf( 'guava' ) ) +guavaBlock = guavaBlock.substring( 0, guavaBlock.indexOf( '' ) ) +assert !guavaBlock.contains( '' ) : + "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" + +return true diff --git a/src/it/projects/add-dependency-multimodule/invoker.properties b/src/it/projects/add-dependency-multimodule/invoker.properties new file mode 100644 index 000000000..eb6424ecd --- /dev/null +++ b/src/it/projects/add-dependency-multimodule/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-multimodule/parent/pom.xml b/src/it/projects/add-dependency-multimodule/parent/pom.xml new file mode 100644 index 000000000..9f66bb3de --- /dev/null +++ b/src/it/projects/add-dependency-multimodule/parent/pom.xml @@ -0,0 +1,59 @@ + + + + + + + 4.0.0 + + org.apache.maven.plugins.dependency.its + multimodule-parent + 1.0-SNAPSHOT + pom + + + 4.13.2 + 2.0.9 + 2.15.0 + + + + + + junit + junit + ${junit.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + commons-io + commons-io + ${commons-io.version} + + + + + diff --git a/src/it/projects/add-dependency-multimodule/pom.xml b/src/it/projects/add-dependency-multimodule/pom.xml new file mode 100644 index 000000000..37739e950 --- /dev/null +++ b/src/it/projects/add-dependency-multimodule/pom.xml @@ -0,0 +1,60 @@ + + + + + + + + + 4.0.0 + + + org.apache.maven.plugins.dependency.its + multimodule-parent + 1.0-SNAPSHOT + parent/pom.xml + + + multimodule-child + + + + junit + junit + test + + + org.slf4j + slf4j-api + + + commons-io + commons-io + + + + diff --git a/src/it/projects/add-dependency-multimodule/test.properties b/src/it/projects/add-dependency-multimodule/test.properties new file mode 100644 index 000000000..7e838cc09 --- /dev/null +++ b/src/it/projects/add-dependency-multimodule/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=com.google.guava:guava:33.0.0-jre diff --git a/src/it/projects/add-dependency-multimodule/verify.groovy b/src/it/projects/add-dependency-multimodule/verify.groovy new file mode 100644 index 000000000..98157c35c --- /dev/null +++ b/src/it/projects/add-dependency-multimodule/verify.groovy @@ -0,0 +1,72 @@ +/* + * 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. + */ + +// --- Verify parent POM was modified --- +def parentPomText = new File( basedir, "parent/pom.xml" ).text +def parentPom = new groovy.xml.XmlSlurper().parse( new File( basedir, "parent/pom.xml" ) ) + +// 1. Property should have been added to parent +assert parentPomText.contains( '33.0.0-jre' ) : + "Parent should have guava.version property" + +// 2. Managed dependency should have been added to parent with property reference +def managedDeps = parentPom.dependencyManagement.dependencies.dependency +def managedGuava = managedDeps.find { it.artifactId == 'guava' } +assert managedGuava != null : "guava should be in parent's dependencyManagement" +assert parentPomText.contains( '${guava.version}' ) : + "Parent managed dependency version should reference \${guava.version}" + +// 3. Existing managed dependencies should be preserved +assert managedDeps.find { it.artifactId == 'junit' } != null : "junit should still be managed" +assert managedDeps.find { it.artifactId == 'slf4j-api' } != null : "slf4j-api should still be managed" +assert managedDeps.find { it.artifactId == 'commons-io' } != null : "commons-io should still be managed" +assert parentPomText.contains( '${junit.version}' ) : "Existing junit property ref preserved" +assert parentPomText.contains( '${slf4j.version}' ) : "Existing slf4j property ref preserved" + +// --- Verify child POM was modified --- +def childPomText = new File( basedir, "pom.xml" ).text +def childPom = new groovy.xml.XmlSlurper().parse( new File( basedir, "pom.xml" ) ) + +// 4. Version-less dependency should have been added to child +def childDeps = childPom.dependencies.dependency +def childGuava = childDeps.find { it.artifactId == 'guava' } +assert childGuava != null : "guava should be in child dependencies" + +// 5. The child dependency should be version-less +// Find the guava block in the child's section and verify no +def childDepsSection = childPomText.substring( childPomText.lastIndexOf( '' ) ) +def guavaBlock = childDepsSection.substring( childDepsSection.indexOf( 'guava' ) ) +guavaBlock = guavaBlock.substring( 0, guavaBlock.indexOf( '' ) ) +assert !guavaBlock.contains( '' ) : + "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/main/java/org/apache/maven/plugins/dependency/AddDependencyMojo.java b/src/main/java/org/apache/maven/plugins/dependency/AddDependencyMojo.java index 8ca2f008f..1915242f2 100644 --- a/src/main/java/org/apache/maven/plugins/dependency/AddDependencyMojo.java +++ b/src/main/java/org/apache/maven/plugins/dependency/AddDependencyMojo.java @@ -305,6 +305,19 @@ private Conventions detectConventions(Coordinates coords) { // 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 @@ -327,22 +340,21 @@ private void analyzePropertyPatterns(Document doc, Conventions conventions) { int propertyVersions = 0; int totalVersions = 0; - // Scan and - scanVersionPatterns(root, "dependencies", patternCounts, new int[] {0, 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}; // [propertyVersions, totalVersions] + int[] counts = {0, 0}; scanVersionPatterns(dm, "dependencies", patternCounts, counts); propertyVersions += counts[0]; totalVersions += counts[1]; } - // Also count from directly - int[] directCounts = {0, 0}; - scanVersionPatterns(root, "dependencies", patternCounts, directCounts); - propertyVersions += directCounts[0]; - totalVersions += directCounts[1]; - // If majority of versioned deps use properties, adopt that convention if (totalVersions > 0 && propertyVersions * 2 >= totalVersions) { conventions.useProperty = true; @@ -387,10 +399,13 @@ static String detectPattern(String propertyName, String artifactId) { if (artifactId == null) { return null; } - // Check suffix patterns: artifactId.version, artifactId-version, artifactIdVersion + // 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."; } @@ -400,14 +415,20 @@ static String detectPattern(String propertyName, String 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 or Version regardless + // 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"; } 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. +