diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategy.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategy.java index aeff300581bf..23c582660a8c 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategy.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategy.java @@ -19,11 +19,13 @@ package org.apache.maven.cling.invoker.mvnup.goals; import java.nio.file.Path; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import org.apache.maven.api.Lifecycle; import org.apache.maven.api.cli.mvnup.UpgradeOptions; import org.apache.maven.api.di.Named; import org.apache.maven.api.di.Priority; @@ -42,8 +44,15 @@ import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.SCHEMA_LOCATION; import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.XSI_NAMESPACE_PREFIX; import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.XSI_NAMESPACE_URI; +import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.BUILD; +import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.EXECUTION; +import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.EXECUTIONS; import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODULE; import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODULES; +import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PHASE; +import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN; +import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGINS; +import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN_MANAGEMENT; import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILE; import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILES; import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SUBPROJECT; @@ -141,6 +150,7 @@ private void performModelUpgrade( // Convert modules to subprojects (for 4.1.0 and higher) if (ModelVersionUtils.isVersionGreaterOrEqual(targetModelVersion, MODEL_VERSION_4_1_0)) { convertModulesToSubprojects(pomDocument, context); + upgradeDeprecatedPhases(pomDocument, context); } // Update modelVersion to target version (perhaps removed later during inference step) @@ -252,4 +262,116 @@ private String getNamespaceForModelVersion(String modelVersion) { return MAVEN_4_0_0_NAMESPACE; } } + + /** + * Upgrades deprecated Maven 3 phase names to Maven 4 equivalents. + * This replaces pre-/post- phases with before:/after: phases. + */ + private void upgradeDeprecatedPhases(Document pomDocument, UpgradeContext context) { + // Create mapping of deprecated phases to their Maven 4 equivalents + Map phaseUpgrades = createPhaseUpgradeMap(); + + Element root = pomDocument.getRootElement(); + Namespace namespace = root.getNamespace(); + + int totalUpgrades = 0; + + // Upgrade phases in main build section + totalUpgrades += upgradePhaseElements(root.getChild(BUILD, namespace), namespace, phaseUpgrades, context); + + // Upgrade phases in profiles + Element profilesElement = root.getChild(PROFILES, namespace); + if (profilesElement != null) { + List profileElements = profilesElement.getChildren(PROFILE, namespace); + for (Element profileElement : profileElements) { + Element profileBuildElement = profileElement.getChild(BUILD, namespace); + totalUpgrades += upgradePhaseElements(profileBuildElement, namespace, phaseUpgrades, context); + } + } + + if (totalUpgrades > 0) { + context.detail("Upgraded " + totalUpgrades + " deprecated phase name(s) to Maven 4 equivalents"); + } + } + + /** + * Creates the mapping of deprecated phase names to their Maven 4 equivalents. + * Uses Maven API constants to ensure consistency with the lifecycle definitions. + */ + private Map createPhaseUpgradeMap() { + Map phaseUpgrades = new HashMap<>(); + + // Clean lifecycle aliases + phaseUpgrades.put("pre-clean", Lifecycle.BEFORE + Lifecycle.Phase.CLEAN); + phaseUpgrades.put("post-clean", Lifecycle.AFTER + Lifecycle.Phase.CLEAN); + + // Default lifecycle aliases + phaseUpgrades.put("pre-integration-test", Lifecycle.BEFORE + Lifecycle.Phase.INTEGRATION_TEST); + phaseUpgrades.put("post-integration-test", Lifecycle.AFTER + Lifecycle.Phase.INTEGRATION_TEST); + + // Site lifecycle aliases + phaseUpgrades.put("pre-site", Lifecycle.BEFORE + Lifecycle.SITE); + phaseUpgrades.put("post-site", Lifecycle.AFTER + Lifecycle.SITE); + + return phaseUpgrades; + } + + /** + * Upgrades phase elements within a build section. + */ + private int upgradePhaseElements( + Element buildElement, Namespace namespace, Map phaseUpgrades, UpgradeContext context) { + if (buildElement == null) { + return 0; + } + + int upgrades = 0; + + // Check plugins section + Element pluginsElement = buildElement.getChild(PLUGINS, namespace); + if (pluginsElement != null) { + upgrades += upgradePhaseElementsInPlugins(pluginsElement, namespace, phaseUpgrades, context); + } + + // Check pluginManagement section + Element pluginManagementElement = buildElement.getChild(PLUGIN_MANAGEMENT, namespace); + if (pluginManagementElement != null) { + Element managedPluginsElement = pluginManagementElement.getChild(PLUGINS, namespace); + if (managedPluginsElement != null) { + upgrades += upgradePhaseElementsInPlugins(managedPluginsElement, namespace, phaseUpgrades, context); + } + } + + return upgrades; + } + + /** + * Upgrades phase elements within a plugins section. + */ + private int upgradePhaseElementsInPlugins( + Element pluginsElement, Namespace namespace, Map phaseUpgrades, UpgradeContext context) { + int upgrades = 0; + + List pluginElements = pluginsElement.getChildren(PLUGIN, namespace); + for (Element pluginElement : pluginElements) { + Element executionsElement = pluginElement.getChild(EXECUTIONS, namespace); + if (executionsElement != null) { + List executionElements = executionsElement.getChildren(EXECUTION, namespace); + for (Element executionElement : executionElements) { + Element phaseElement = executionElement.getChild(PHASE, namespace); + if (phaseElement != null) { + String currentPhase = phaseElement.getTextTrim(); + String newPhase = phaseUpgrades.get(currentPhase); + if (newPhase != null) { + phaseElement.setText(newPhase); + context.detail("Upgraded phase: " + currentPhase + " → " + newPhase); + upgrades++; + } + } + } + } + } + + return upgrades; + } } diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/UpgradeConstants.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/UpgradeConstants.java index 8d49fcc76b7a..004ad070b0a8 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/UpgradeConstants.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/UpgradeConstants.java @@ -74,9 +74,11 @@ public static final class XmlElements { public static final String TEST_OUTPUT_DIRECTORY = "testOutputDirectory"; public static final String EXTENSIONS = "extensions"; public static final String EXECUTIONS = "executions"; + public static final String EXECUTION = "execution"; public static final String GOALS = "goals"; public static final String INHERITED = "inherited"; public static final String CONFIGURATION = "configuration"; + public static final String PHASE = "phase"; // Module elements public static final String MODULES = "modules"; diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategyTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategyTest.java index 2a0c3c171980..ab20e4c13298 100644 --- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategyTest.java +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategyTest.java @@ -324,6 +324,467 @@ void shouldProvideMeaningfulDescription() { } } + @Nested + @DisplayName("Phase Upgrades") + class PhaseUpgradeTests { + + @Test + @DisplayName("should upgrade deprecated phases to Maven 4 equivalents in 4.1.0") + void shouldUpgradeDeprecatedPhasesIn410() throws Exception { + Document document = createDocumentWithDeprecatedPhases(); + Map pomMap = Map.of(Paths.get("pom.xml"), document); + + // Create context with --model-version=4.1.0 option to trigger phase upgrade + UpgradeOptions options = mock(UpgradeOptions.class); + when(options.modelVersion()).thenReturn(Optional.of("4.1.0")); + when(options.all()).thenReturn(Optional.empty()); + UpgradeContext context = createMockContext(options); + + UpgradeResult result = strategy.apply(context, pomMap); + + assertTrue(result.success(), "Model upgrade should succeed"); + assertTrue(result.modifiedCount() > 0, "Should have upgraded phases"); + + // Verify phases were upgraded + verifyCleanPluginPhases(document); + verifyFailsafePluginPhases(document); + verifySitePluginPhases(document); + verifyPluginManagementPhases(document); + verifyProfilePhases(document); + } + + private Document createDocumentWithDeprecatedPhases() throws Exception { + String pomXml = + """ + + + 4.0.0 + com.example + test-project + 1.0.0 + + + + org.apache.maven.plugins + maven-clean-plugin + 3.2.0 + + + pre-clean-test + pre-clean + + clean + + + + post-clean-test + post-clean + + clean + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.0.0-M7 + + + pre-integration-test-setup + pre-integration-test + + integration-test + + + + post-integration-test-cleanup + post-integration-test + + verify + + + + + + org.apache.maven.plugins + maven-site-plugin + 3.12.1 + + + pre-site-setup + pre-site + + site + + + + post-site-cleanup + post-site + + deploy + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + pre-clean-compile + pre-clean + + compile + + + + + + + + + + test-profile + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + profile-pre-integration-test + pre-integration-test + + run + + + + + + + + + + """; + + return saxBuilder.build(new StringReader(pomXml)); + } + + private void verifyCleanPluginPhases(Document document) { + Element root = document.getRootElement(); + Namespace namespace = root.getNamespace(); + Element build = root.getChild("build", namespace); + Element plugins = build.getChild("plugins", namespace); + + Element cleanPlugin = plugins.getChildren("plugin", namespace).stream() + .filter(p -> "maven-clean-plugin" + .equals(p.getChild("artifactId", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(cleanPlugin); + + Element cleanExecutions = cleanPlugin.getChild("executions", namespace); + Element preCleanExecution = cleanExecutions.getChildren("execution", namespace).stream() + .filter(e -> + "pre-clean-test".equals(e.getChild("id", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(preCleanExecution); + assertEquals( + "before:clean", + preCleanExecution.getChild("phase", namespace).getText()); + + Element postCleanExecution = cleanExecutions.getChildren("execution", namespace).stream() + .filter(e -> + "post-clean-test".equals(e.getChild("id", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(postCleanExecution); + assertEquals( + "after:clean", + postCleanExecution.getChild("phase", namespace).getText()); + } + + private void verifyFailsafePluginPhases(Document document) { + Element root = document.getRootElement(); + Namespace namespace = root.getNamespace(); + Element build = root.getChild("build", namespace); + Element plugins = build.getChild("plugins", namespace); + + Element failsafePlugin = plugins.getChildren("plugin", namespace).stream() + .filter(p -> "maven-failsafe-plugin" + .equals(p.getChild("artifactId", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(failsafePlugin); + + Element failsafeExecutions = failsafePlugin.getChild("executions", namespace); + Element preIntegrationExecution = failsafeExecutions.getChildren("execution", namespace).stream() + .filter(e -> "pre-integration-test-setup" + .equals(e.getChild("id", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(preIntegrationExecution); + assertEquals( + "before:integration-test", + preIntegrationExecution.getChild("phase", namespace).getText()); + + Element postIntegrationExecution = failsafeExecutions.getChildren("execution", namespace).stream() + .filter(e -> "post-integration-test-cleanup" + .equals(e.getChild("id", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(postIntegrationExecution); + assertEquals( + "after:integration-test", + postIntegrationExecution.getChild("phase", namespace).getText()); + } + + private void verifySitePluginPhases(Document document) { + Element root = document.getRootElement(); + Namespace namespace = root.getNamespace(); + Element build = root.getChild("build", namespace); + Element plugins = build.getChild("plugins", namespace); + + Element sitePlugin = plugins.getChildren("plugin", namespace).stream() + .filter(p -> "maven-site-plugin" + .equals(p.getChild("artifactId", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(sitePlugin); + + Element siteExecutions = sitePlugin.getChild("executions", namespace); + Element preSiteExecution = siteExecutions.getChildren("execution", namespace).stream() + .filter(e -> + "pre-site-setup".equals(e.getChild("id", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(preSiteExecution); + assertEquals( + "before:site", preSiteExecution.getChild("phase", namespace).getText()); + + Element postSiteExecution = siteExecutions.getChildren("execution", namespace).stream() + .filter(e -> "post-site-cleanup" + .equals(e.getChild("id", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(postSiteExecution); + assertEquals( + "after:site", postSiteExecution.getChild("phase", namespace).getText()); + } + + private void verifyPluginManagementPhases(Document document) { + Element root = document.getRootElement(); + Namespace namespace = root.getNamespace(); + Element build = root.getChild("build", namespace); + Element pluginManagement = build.getChild("pluginManagement", namespace); + Element managedPlugins = pluginManagement.getChild("plugins", namespace); + Element compilerPlugin = managedPlugins.getChildren("plugin", namespace).stream() + .filter(p -> "maven-compiler-plugin" + .equals(p.getChild("artifactId", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(compilerPlugin); + + Element compilerExecutions = compilerPlugin.getChild("executions", namespace); + Element preCleanCompileExecution = compilerExecutions.getChildren("execution", namespace).stream() + .filter(e -> "pre-clean-compile" + .equals(e.getChild("id", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(preCleanCompileExecution); + assertEquals( + "before:clean", + preCleanCompileExecution.getChild("phase", namespace).getText()); + } + + private void verifyProfilePhases(Document document) { + Element root = document.getRootElement(); + Namespace namespace = root.getNamespace(); + Element profiles = root.getChild("profiles", namespace); + Element profile = profiles.getChild("profile", namespace); + Element profileBuild = profile.getChild("build", namespace); + Element profilePlugins = profileBuild.getChild("plugins", namespace); + Element antrunPlugin = profilePlugins.getChildren("plugin", namespace).stream() + .filter(p -> "maven-antrun-plugin" + .equals(p.getChild("artifactId", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(antrunPlugin); + + Element antrunExecutions = antrunPlugin.getChild("executions", namespace); + Element profilePreIntegrationExecution = antrunExecutions.getChildren("execution", namespace).stream() + .filter(e -> "profile-pre-integration-test" + .equals(e.getChild("id", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(profilePreIntegrationExecution); + assertEquals( + "before:integration-test", + profilePreIntegrationExecution.getChild("phase", namespace).getText()); + } + + @Test + @DisplayName("should not upgrade phases when upgrading to 4.0.0") + void shouldNotUpgradePhasesWhenUpgradingTo400() throws Exception { + String pomXml = + """ + + + 4.0.0 + com.example + test-project + 1.0.0 + + + + org.apache.maven.plugins + maven-clean-plugin + 3.2.0 + + + pre-clean-test + pre-clean + + clean + + + + + + + + """; + + Document document = saxBuilder.build(new StringReader(pomXml)); + Map pomMap = Map.of(Paths.get("pom.xml"), document); + + // Create context with --model-version=4.0.0 option (no phase upgrade) + UpgradeOptions options = mock(UpgradeOptions.class); + when(options.modelVersion()).thenReturn(Optional.of("4.0.0")); + when(options.all()).thenReturn(Optional.empty()); + UpgradeContext context = createMockContext(options); + + UpgradeResult result = strategy.apply(context, pomMap); + + assertTrue(result.success(), "Model upgrade should succeed"); + + // Verify phases were NOT upgraded (should remain as pre-clean) + Element root = document.getRootElement(); + Namespace namespace = root.getNamespace(); + Element build = root.getChild("build", namespace); + Element plugins = build.getChild("plugins", namespace); + Element cleanPlugin = plugins.getChild("plugin", namespace); + Element executions = cleanPlugin.getChild("executions", namespace); + Element execution = executions.getChild("execution", namespace); + Element phase = execution.getChild("phase", namespace); + + assertEquals("pre-clean", phase.getText(), "Phase should remain as pre-clean for 4.0.0"); + } + + @Test + @DisplayName("should preserve non-deprecated phases") + void shouldPreserveNonDeprecatedPhases() throws Exception { + String pomXml = + """ + + + 4.0.0 + com.example + test-project + 1.0.0 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + compile-test + compile + + compile + + + + test-compile-test + test-compile + + testCompile + + + + package-test + package + + compile + + + + + + + + """; + + Document document = saxBuilder.build(new StringReader(pomXml)); + Map pomMap = Map.of(Paths.get("pom.xml"), document); + + // Create context with --model-version=4.1.0 option + UpgradeOptions options = mock(UpgradeOptions.class); + when(options.modelVersion()).thenReturn(Optional.of("4.1.0")); + when(options.all()).thenReturn(Optional.empty()); + UpgradeContext context = createMockContext(options); + + UpgradeResult result = strategy.apply(context, pomMap); + + assertTrue(result.success(), "Model upgrade should succeed"); + + // Verify non-deprecated phases were preserved + Element root = document.getRootElement(); + Namespace namespace = root.getNamespace(); + Element build = root.getChild("build", namespace); + Element plugins = build.getChild("plugins", namespace); + Element compilerPlugin = plugins.getChild("plugin", namespace); + Element executions = compilerPlugin.getChild("executions", namespace); + + Element compileExecution = executions.getChildren("execution", namespace).stream() + .filter(e -> + "compile-test".equals(e.getChild("id", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(compileExecution); + assertEquals( + "compile", compileExecution.getChild("phase", namespace).getText()); + + Element testCompileExecution = executions.getChildren("execution", namespace).stream() + .filter(e -> "test-compile-test" + .equals(e.getChild("id", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(testCompileExecution); + assertEquals( + "test-compile", + testCompileExecution.getChild("phase", namespace).getText()); + + Element packageExecution = executions.getChildren("execution", namespace).stream() + .filter(e -> + "package-test".equals(e.getChild("id", namespace).getText())) + .findFirst() + .orElse(null); + assertNotNull(packageExecution); + assertEquals( + "package", packageExecution.getChild("phase", namespace).getText()); + } + } + @Nested @DisplayName("Downgrade Handling") class DowngradeHandlingTests {