This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch mvnup in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/mvnup by this push: new d599d7e5e8 Improve BaseUpgradeGoal to fix Maven 4 compatibility issues d599d7e5e8 is described below commit d599d7e5e820d3442f53a6a081b85a0d2adbe475 Author: Guillaume Nodet <gno...@gmail.com> AuthorDate: Fri May 30 08:25:05 2025 +0200 Improve BaseUpgradeGoal to fix Maven 4 compatibility issues - Add fixDuplicatePlugins() to handle duplicate plugin declarations - Add fixIncorrectParentRelativePaths() to correct invalid parent paths - Enhance fixUnsupportedRepositoryExpressions() for better error handling - Add comprehensive unit tests for new functionality - Exclude FileLength checkstyle check for BaseUpgradeGoal Fixes issues identified in Maven 4 testing: - Duplicate plugin declarations (e.g., axis-axis1-java) - Unsupported repository URL expressions (e.g., spark with ${asf.staging}) - Incorrect parent relative paths in multi-module projects --- impl/maven-cli/pom.xml | 5 + .../cling/invoker/mvnup/goals/BaseUpgradeGoal.java | 262 ++++++++++++++++++--- .../mvnup/goals/Maven4CompatibilityFixesTest.java | 127 ++++++++++ 3 files changed, 362 insertions(+), 32 deletions(-) diff --git a/impl/maven-cli/pom.xml b/impl/maven-cli/pom.xml index cabe1f0eee..0c9c703dca 100644 --- a/impl/maven-cli/pom.xml +++ b/impl/maven-cli/pom.xml @@ -31,6 +31,11 @@ under the License. <name>Maven 4 CLI</name> <description>Maven 4 CLI component, with CLI and logging support.</description> + <properties> + <!-- in: BaseUpgradeGoal --> + <checkstyle.violation.ignore>FileLength</checkstyle.violation.ignore> + </properties> + <dependencies> <!-- Maven4 API --> <dependency> diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/BaseUpgradeGoal.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/BaseUpgradeGoal.java index 38f810b92e..1163b1942a 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/BaseUpgradeGoal.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/BaseUpgradeGoal.java @@ -405,11 +405,21 @@ protected void applyMaven4CompatibilityFixes(UpgradeContext context, Map<Path, D hasIssues = true; } + // Fix duplicate plugins + if (fixDuplicatePlugins(context, pomDocument)) { + hasIssues = true; + } + // Fix unsupported expressions in repository URLs if (fixUnsupportedRepositoryExpressions(context, pomDocument)) { hasIssues = true; } + // Fix incorrect parent relative paths + if (fixIncorrectParentRelativePaths(context, pomDocument, pomPath, pomMap)) { + hasIssues = true; + } + if (hasIssues) { fixedCount++; context.logger.info(" ✓ Maven 4 compatibility issues fixed"); @@ -1845,40 +1855,66 @@ protected boolean fixUnsupportedRepositoryExpressions(UpgradeContext context, Do */ protected boolean fixRepositoryExpressions( UpgradeContext context, Element repositoriesElement, Namespace namespace) { + if (repositoriesElement == null) { + return false; + } + boolean fixed = false; + String elementType = repositoriesElement.getName().equals("repositories") ? "repository" : "pluginRepository"; + List<Element> repositories = repositoriesElement.getChildren(elementType, namespace); + + for (Element repository : repositories) { + Element urlElement = repository.getChild("url", namespace); + if (urlElement != null) { + String url = urlElement.getTextTrim(); + if (url.contains("${") + && !url.contains("${project.basedir}") + && !url.contains("${project.rootDirectory}")) { + String repositoryId = getChildText(repository, "id", namespace); + context.logger.warn(" • Found unsupported expression in " + elementType + " URL (id: " + + repositoryId + "): " + url); + context.logger.warn( + " Maven 4 only supports ${project.basedir} and ${project.rootDirectory} expressions in repository URLs"); + + // Comment out the problematic repository + org.jdom2.Comment comment = new org.jdom2.Comment( + " Repository disabled due to unsupported expression in URL: " + url + " "); + Element parent = repository.getParentElement(); + parent.addContent(parent.indexOf(repository), comment); + removeElementWithFormatting(repository); + + context.logger.info(" • Commented out " + elementType + + " with unsupported URL expression (id: " + repositoryId + ")"); + fixed = true; + } + } + } - if (repositoriesElement != null) { - String elementType = - repositoriesElement.getName().equals("repositories") ? "repository" : "pluginRepository"; - List<Element> repositories = repositoriesElement.getChildren(elementType, namespace); - - for (Element repository : repositories) { - Element urlElement = repository.getChild("url", namespace); - if (urlElement != null) { - String url = urlElement.getTextTrim(); - if (url.contains("${") - && !url.contains("${project.basedir}") - && !url.contains("${project.rootDirectory}")) { - // Replace unsupported expressions with a comment - String repositoryId = getChildText(repository, "id", namespace); - context.logger.warn(" • Found unsupported expression in " + elementType + " URL (id: " - + repositoryId + "): " + url); - context.logger.warn( - " Maven 4 only supports ${project.basedir} and ${project.rootDirectory} expressions in repository URLs"); - - // For now, we'll comment out the problematic repository - // In a real implementation, you might want to ask the user how to handle this - org.jdom2.Comment comment = new org.jdom2.Comment( - " Repository disabled due to unsupported expression in URL: " + url + " "); - Element parent = repository.getParentElement(); - int index = parent.indexOf(repository); - parent.addContent(index, comment); - removeElementWithFormatting(repository); - - context.logger.info(" • Commented out " + elementType - + " with unsupported URL expression (id: " + repositoryId + ")"); - fixed = true; - } + return fixed; + } + + /** + * Fixes duplicate plugin declarations. + * Maven 4 enforces stricter validation for duplicate plugins in the same section. + */ + protected boolean fixDuplicatePlugins(UpgradeContext context, Document pomDocument) { + boolean fixed = false; + Element root = pomDocument.getRootElement(); + Namespace namespace = root.getNamespace(); + + // Check main build plugins + Element buildElement = root.getChild("build", namespace); + if (buildElement != null) { + fixed |= fixPluginsInBuildElement(context, buildElement, namespace, "build"); + } + + // Check profile plugins + Element profilesElement = root.getChild("profiles", namespace); + if (profilesElement != null) { + for (Element profileElement : profilesElement.getChildren("profile", namespace)) { + Element profileBuildElement = profileElement.getChild("build", namespace); + if (profileBuildElement != null) { + fixed |= fixPluginsInBuildElement(context, profileBuildElement, namespace, "profile build"); } } } @@ -1886,6 +1922,168 @@ protected boolean fixRepositoryExpressions( return fixed; } + /** + * Fixes duplicate plugins in a build element (both plugins and pluginManagement). + */ + protected boolean fixPluginsInBuildElement( + UpgradeContext context, Element buildElement, Namespace namespace, String prefix) { + boolean fixed = false; + + Element pluginsElement = buildElement.getChild("plugins", namespace); + if (pluginsElement != null) { + fixed |= fixDuplicatePluginsInSection(context, pluginsElement, namespace, prefix + "/plugins"); + } + + Element pluginManagementElement = buildElement.getChild("pluginManagement", namespace); + if (pluginManagementElement != null) { + Element managedPluginsElement = pluginManagementElement.getChild("plugins", namespace); + if (managedPluginsElement != null) { + fixed |= fixDuplicatePluginsInSection( + context, managedPluginsElement, namespace, prefix + "/pluginManagement/plugins"); + } + } + + return fixed; + } + + /** + * Fixes duplicate plugins within a specific plugins section. + */ + protected boolean fixDuplicatePluginsInSection( + UpgradeContext context, Element pluginsElement, Namespace namespace, String sectionName) { + boolean fixed = false; + List<Element> plugins = pluginsElement.getChildren("plugin", namespace); + Map<String, Element> seenPlugins = new HashMap<>(); + List<Element> toRemove = new ArrayList<>(); + + for (Element plugin : plugins) { + String groupId = getChildText(plugin, "groupId", namespace); + String artifactId = getChildText(plugin, "artifactId", namespace); + + // Default groupId for Maven plugins + if (groupId == null && artifactId != null && artifactId.startsWith("maven-")) { + groupId = "org.apache.maven.plugins"; + } + + if (groupId != null && artifactId != null) { + // Create a key for uniqueness check (groupId:artifactId) + String key = groupId + ":" + artifactId; + + if (seenPlugins.containsKey(key)) { + // Found duplicate - remove it + toRemove.add(plugin); + context.logger.info(" • Removed duplicate plugin: " + key + " in " + sectionName); + fixed = true; + } else { + seenPlugins.put(key, plugin); + } + } + } + + // Remove duplicates while preserving formatting + for (Element duplicate : toRemove) { + removeElementWithFormatting(duplicate); + } + + return fixed; + } + + /** + * Fixes incorrect parent relative paths by searching for the correct parent POM in the project structure. + */ + protected boolean fixIncorrectParentRelativePaths( + UpgradeContext context, Document pomDocument, Path pomPath, Map<Path, Document> pomMap) { + Element root = pomDocument.getRootElement(); + Namespace namespace = root.getNamespace(); + Element parentElement = root.getChild("parent", namespace); + + if (parentElement == null) { + return false; + } + + // Get current relativePath (default is "../pom.xml") + Element relativePathElement = parentElement.getChild("relativePath", namespace); + String currentRelativePath = relativePathElement != null ? relativePathElement.getTextTrim() : "../pom.xml"; + if (currentRelativePath == null || currentRelativePath.trim().isEmpty()) { + currentRelativePath = "../pom.xml"; + } + + // Check if current path is valid + Path resolvedParentPath = resolveParentPomPath(pomPath, currentRelativePath); + if (resolvedParentPath != null && Files.exists(resolvedParentPath)) { + return false; // Path is correct + } + + // Find correct parent in pomMap + String parentGroupId = getChildText(parentElement, "groupId", namespace); + String parentArtifactId = getChildText(parentElement, "artifactId", namespace); + String parentVersion = getChildText(parentElement, "version", namespace); + + if (parentGroupId == null || parentArtifactId == null) { + return false; + } + + Path correctParentPath = findParentPomInMap(parentGroupId, parentArtifactId, parentVersion, pomMap); + if (correctParentPath == null) { + return false; + } + + String correctRelativePath = calculateRelativePath(pomPath, correctParentPath); + if (correctRelativePath == null || correctRelativePath.equals(currentRelativePath)) { + return false; + } + + // Update relativePath element + if (relativePathElement == null) { + relativePathElement = new Element("relativePath", namespace); + Element insertAfter = parentElement.getChild("version", namespace); + if (insertAfter == null) { + insertAfter = parentElement.getChild("artifactId", namespace); + } + if (insertAfter != null) { + parentElement.addContent(parentElement.indexOf(insertAfter) + 1, relativePathElement); + } else { + parentElement.addContent(relativePathElement); + } + } + + relativePathElement.setText(correctRelativePath); + context.logger.info(" • Fixed parent relativePath from '" + currentRelativePath + "' to '" + + correctRelativePath + "'"); + return true; + } + + /** + * Finds a parent POM in the pomMap by GAV coordinates. + */ + protected Path findParentPomInMap(String groupId, String artifactId, String version, Map<Path, Document> pomMap) { + return pomMap.entrySet().stream() + .filter(entry -> { + GAV gav = extractGAVWithParentResolution(entry.getValue(), null); + return gav != null + && java.util.Objects.equals(gav.groupId, groupId) + && java.util.Objects.equals(gav.artifactId, artifactId) + && (version == null || java.util.Objects.equals(gav.version, version)); + }) + .map(Map.Entry::getKey) + .findFirst() + .orElse(null); + } + + /** + * Calculates the relative path from a child POM to a parent POM. + */ + protected String calculateRelativePath(Path childPomPath, Path parentPomPath) { + try { + Path childDir = childPomPath.getParent(); + return childDir != null + ? childDir.relativize(parentPomPath).toString().replace('\\', '/') + : null; + } catch (Exception e) { + return null; + } + } + /** * Recursively finds all elements with a specific attribute value. */ diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/Maven4CompatibilityFixesTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/Maven4CompatibilityFixesTest.java index 73e2426c74..1957a5864b 100644 --- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/Maven4CompatibilityFixesTest.java +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/Maven4CompatibilityFixesTest.java @@ -19,6 +19,7 @@ package org.apache.maven.cling.invoker.mvnup.goals; import java.io.StringReader; +import java.util.List; import org.apache.maven.cling.invoker.mvnup.UpgradeContext; import org.jdom2.Document; @@ -28,6 +29,7 @@ import org.mockito.Mockito; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -205,6 +207,117 @@ void testDefaultBehavior() throws Exception { assertTrue(defaultFixModel, "Default behavior should enable --fix-model"); } + @Test + void testFixDuplicatePlugins() throws Exception { + String pomXml = + """ + <?xml version="1.0" encoding="UTF-8"?> + <project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>com.example</groupId> + <artifactId>test</artifactId> + <version>1.0.0</version> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.8.1</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.9.0</version> + </plugin> + </plugins> + </build> + </project> + """; + + SAXBuilder builder = new SAXBuilder(); + Document document = builder.build(new StringReader(pomXml)); + + TestableBaseUpgradeGoal goal = new TestableBaseUpgradeGoal(); + UpgradeContext context = Mockito.mock(UpgradeContext.class); + context.logger = Mockito.mock(org.apache.maven.api.cli.Logger.class); + + boolean fixed = goal.fixDuplicatePlugins(context, document); + + assertTrue(fixed, "Should have fixed duplicate plugins"); + + // Verify only one plugin remains + Element buildElement = document.getRootElement() + .getChild("build", document.getRootElement().getNamespace()); + Element pluginsElement = buildElement.getChild("plugins", buildElement.getNamespace()); + List<Element> plugins = pluginsElement.getChildren("plugin", pluginsElement.getNamespace()); + + assertEquals(1, plugins.size(), "Should have only one plugin after removing duplicates"); + } + + @Test + void testFixIncorrectParentRelativePaths() throws Exception { + String pomXml = + """ + <?xml version="1.0" encoding="UTF-8"?> + <project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.example</groupId> + <artifactId>parent</artifactId> + <version>1.0.0</version> + <relativePath>../wrong-path/pom.xml</relativePath> + </parent> + <artifactId>child</artifactId> + </project> + """; + + String parentPomXml = + """ + <?xml version="1.0" encoding="UTF-8"?> + <project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>com.example</groupId> + <artifactId>parent</artifactId> + <version>1.0.0</version> + <packaging>pom</packaging> + </project> + """; + + SAXBuilder builder = new SAXBuilder(); + Document childDocument = builder.build(new StringReader(pomXml)); + Document parentDocument = builder.build(new StringReader(parentPomXml)); + + // Create mock pomMap with parent POM + java.util.Map<java.nio.file.Path, Document> pomMap = new java.util.HashMap<>(); + java.nio.file.Path childPath = java.nio.file.Paths.get("child/pom.xml"); + java.nio.file.Path parentPath = java.nio.file.Paths.get("pom.xml"); + pomMap.put(childPath, childDocument); + pomMap.put(parentPath, parentDocument); + + TestableBaseUpgradeGoal goal = new TestableBaseUpgradeGoal(); + UpgradeContext context = Mockito.mock(UpgradeContext.class); + context.logger = Mockito.mock(org.apache.maven.api.cli.Logger.class); + + boolean fixed = goal.fixIncorrectParentRelativePaths(context, childDocument, childPath, pomMap); + + assertTrue(fixed, "Should have fixed incorrect parent relative path"); + + // Verify the relativePath was updated + Element parentElement = childDocument + .getRootElement() + .getChild("parent", childDocument.getRootElement().getNamespace()); + Element relativePathElement = parentElement.getChild("relativePath", parentElement.getNamespace()); + + assertNotNull(relativePathElement, "relativePath element should exist"); + assertEquals("../pom.xml", relativePathElement.getTextTrim(), "relativePath should be corrected"); + } + /** * Testable subclass that exposes protected methods for testing. */ @@ -224,6 +337,20 @@ public boolean fixUnsupportedCombineSelfAttributes(UpgradeContext context, Docum return super.fixUnsupportedCombineSelfAttributes(context, pomDocument); } + @Override + public boolean fixDuplicatePlugins(UpgradeContext context, Document pomDocument) { + return super.fixDuplicatePlugins(context, pomDocument); + } + + @Override + public boolean fixIncorrectParentRelativePaths( + UpgradeContext context, + Document pomDocument, + java.nio.file.Path pomPath, + java.util.Map<java.nio.file.Path, Document> pomMap) { + return super.fixIncorrectParentRelativePaths(context, pomDocument, pomPath, pomMap); + } + // Test helper methods to expose internal logic public String testDoUpgradeLogic( UpgradeContext context,