ModelUpgradeStrategy.java
/*
* 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.cling.invoker.mvnup.goals;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import eu.maveniverse.domtrip.Document;
import eu.maveniverse.domtrip.Editor;
import eu.maveniverse.domtrip.Element;
import eu.maveniverse.domtrip.maven.MavenPomElements;
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;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.BUILD;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.EXECUTIONS;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.MODEL_VERSION;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.MODULE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.MODULES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGINS;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_MANAGEMENT;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.SUBPROJECT;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.SUBPROJECTS;
import static eu.maveniverse.domtrip.maven.MavenPomElements.ModelVersions.MODEL_VERSION_4_0_0;
import static eu.maveniverse.domtrip.maven.MavenPomElements.ModelVersions.MODEL_VERSION_4_1_0;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Namespaces.MAVEN_4_0_0_NAMESPACE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Namespaces.MAVEN_4_1_0_NAMESPACE;
import static org.apache.maven.cling.invoker.mvnup.goals.ModelVersionUtils.getSchemaLocationForModelVersion;
/**
* Strategy for upgrading Maven model versions (e.g., 4.0.0 ��� 4.1.0).
* Handles namespace updates, schema location changes, and element conversions.
*/
@Named
@Singleton
@Priority(40)
public class ModelUpgradeStrategy extends AbstractUpgradeStrategy {
public ModelUpgradeStrategy() {
// Target model version will be determined from context
}
@Override
public boolean isApplicable(UpgradeContext context) {
UpgradeOptions options = getOptions(context);
// Handle --all option (overrides individual options)
if (options.all().orElse(false)) {
return true;
}
String targetModel = determineTargetModelVersion(context);
// Only applicable if we're not staying at 4.0.0
return !MODEL_VERSION_4_0_0.equals(targetModel);
}
@Override
public String getDescription() {
return "Upgrading POM model version";
}
@Override
public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
String targetModelVersion = determineTargetModelVersion(context);
Set<Path> processedPoms = new HashSet<>();
Set<Path> modifiedPoms = new HashSet<>();
Set<Path> errorPoms = new HashSet<>();
for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Path pomPath = entry.getKey();
Document pomDocument = entry.getValue();
processedPoms.add(pomPath);
String currentVersion = ModelVersionUtils.detectModelVersion(pomDocument);
context.info(pomPath + " (current: " + currentVersion + ")");
context.indent();
try {
if (currentVersion.equals(targetModelVersion)) {
context.success("Already at target version " + targetModelVersion);
} else if (ModelVersionUtils.canUpgrade(currentVersion, targetModelVersion)) {
context.action("Upgrading from " + currentVersion + " to " + targetModelVersion);
// Perform the actual upgrade
context.indent();
try {
Document upgradedDocument =
performModelUpgrade(pomDocument, context, currentVersion, targetModelVersion);
// Update the map with the modified document
pomMap.put(pomPath, upgradedDocument);
} finally {
context.unindent();
}
context.success("Model upgrade completed");
modifiedPoms.add(pomPath);
} else {
// Treat invalid upgrades (including downgrades) as errors, not warnings
context.failure("Cannot upgrade from " + currentVersion + " to " + targetModelVersion);
errorPoms.add(pomPath);
}
} catch (Exception e) {
context.failure("Model upgrade failed: " + e.getMessage());
errorPoms.add(pomPath);
} finally {
context.unindent();
}
}
return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
}
/**
* Performs the core model upgrade from current version to target version.
* This includes namespace updates and module conversion using domtrip.
* Returns the upgraded document.
*/
private Document performModelUpgrade(
Document pomDocument, UpgradeContext context, String currentVersion, String targetModelVersion) {
// Create Editor from domtrip Document
Editor editor = new Editor(pomDocument);
// Update model version element
Element root = editor.root();
Element modelVersionElement = root.child(MODEL_VERSION).orElse(null);
if (modelVersionElement != null) {
editor.setTextContent(modelVersionElement, targetModelVersion);
context.detail("Updated modelVersion to " + targetModelVersion);
} else {
// Create new modelVersion element if it doesn't exist
DomUtils.insertContentElement(root, MODEL_VERSION, targetModelVersion);
context.detail("Added modelVersion " + targetModelVersion);
}
// Update namespace and schema location
upgradeNamespaceAndSchemaLocation(editor, context, targetModelVersion);
// Convert modules to subprojects (for 4.1.0 and higher)
if (ModelVersionUtils.isVersionGreaterOrEqual(targetModelVersion, MODEL_VERSION_4_1_0)) {
convertModulesToSubprojects(editor, context);
upgradeDeprecatedPhases(editor, context);
}
// Return the modified document from the editor
return editor.document();
}
/**
* Updates namespace and schema location for the target model version using domtrip.
*/
private void upgradeNamespaceAndSchemaLocation(Editor editor, UpgradeContext context, String targetModelVersion) {
Element root = editor.root();
if (root == null) {
return;
}
// Update namespace based on target model version
String targetNamespace = getNamespaceForModelVersion(targetModelVersion);
// Use element's attribute method to set the namespace declaration
// This modifies the element in place and marks it as modified
root.attribute("xmlns", targetNamespace);
context.detail("Updated namespace to " + targetNamespace);
// Update schema location if present
String currentSchemaLocation = root.attribute("xsi:schemaLocation");
if (currentSchemaLocation != null) {
String newSchemaLocation = getSchemaLocationForModelVersion(targetModelVersion);
root.attribute("xsi:schemaLocation", newSchemaLocation);
context.detail("Updated xsi:schemaLocation");
}
}
/**
* Converts modules to subprojects for 4.1.0 compatibility using domtrip.
*/
private void convertModulesToSubprojects(Editor editor, UpgradeContext context) {
Element root = editor.root();
if (root == null) {
return;
}
// Convert modules element to subprojects
Element modulesElement = root.child(MODULES).orElse(null);
if (modulesElement != null) {
// domtrip makes this much simpler - just change the element name
// The formatting and structure are preserved automatically
modulesElement.name(SUBPROJECTS);
context.detail("Converted <modules> to <subprojects>");
// Convert all module children to subproject
var moduleElements = modulesElement.children(MODULE).toList();
for (Element moduleElement : moduleElements) {
moduleElement.name(SUBPROJECT);
}
if (!moduleElements.isEmpty()) {
context.detail("Converted " + moduleElements.size() + " <module> elements to <subproject>");
}
}
// Also check inside profiles
Element profilesElement = root.child(PROFILES).orElse(null);
if (profilesElement != null) {
var profileElements = profilesElement.children(PROFILE).toList();
for (Element profileElement : profileElements) {
Element profileModulesElement = profileElement.child(MODULES).orElse(null);
if (profileModulesElement != null) {
profileModulesElement.name(SUBPROJECTS);
var profileModuleElements =
profileModulesElement.children(MODULE).toList();
for (Element moduleElement : profileModuleElements) {
moduleElement.name(SUBPROJECT);
}
if (!profileModuleElements.isEmpty()) {
context.detail("Converted " + profileModuleElements.size()
+ " <module> elements to <subproject> in profiles");
}
}
}
}
}
/**
* Determines the target model version from the upgrade context.
*/
private String determineTargetModelVersion(UpgradeContext context) {
UpgradeOptions options = getOptions(context);
if (options.modelVersion().isPresent()) {
return options.modelVersion().get();
} else if (options.all().orElse(false)) {
return MODEL_VERSION_4_1_0;
} else {
return MODEL_VERSION_4_0_0;
}
}
/**
* Gets the namespace URI for a model version.
*/
private String getNamespaceForModelVersion(String modelVersion) {
if (MavenPomElements.ModelVersions.MODEL_VERSION_4_2_0.equals(modelVersion)) {
return MavenPomElements.Namespaces.MAVEN_4_2_0_NAMESPACE;
} else if (MODEL_VERSION_4_1_0.equals(modelVersion)) {
return MAVEN_4_1_0_NAMESPACE;
} else {
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(Editor editor, UpgradeContext context) {
// Create mapping of deprecated phases to their Maven 4 equivalents
Map<String, String> phaseUpgrades = createPhaseUpgradeMap();
Element root = editor.root();
if (root == null) {
return;
}
int totalUpgrades = 0;
// Upgrade phases in main build section
Element buildElement = root.child(BUILD).orElse(null);
if (buildElement != null) {
totalUpgrades += upgradePhaseElements(buildElement, phaseUpgrades, context);
}
// Upgrade phases in profiles
Element profilesElement = root.child(PROFILES).orElse(null);
if (profilesElement != null) {
var profileElements = profilesElement.children(PROFILE).toList();
for (Element profileElement : profileElements) {
Element profileBuildElement = profileElement.child(BUILD).orElse(null);
if (profileBuildElement != null) {
totalUpgrades += upgradePhaseElements(profileBuildElement, 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<String, String> createPhaseUpgradeMap() {
Map<String, String> 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, Map<String, String> phaseUpgrades, UpgradeContext context) {
if (buildElement == null) {
return 0;
}
int upgrades = 0;
// Check plugins section
Element pluginsElement = buildElement.child(PLUGINS).orElse(null);
if (pluginsElement != null) {
upgrades += upgradePhaseElementsInPlugins(pluginsElement, phaseUpgrades, context);
}
// Check pluginManagement section
Element pluginManagementElement = buildElement.child(PLUGIN_MANAGEMENT).orElse(null);
if (pluginManagementElement != null) {
Element managedPluginsElement =
pluginManagementElement.child(PLUGINS).orElse(null);
if (managedPluginsElement != null) {
upgrades += upgradePhaseElementsInPlugins(managedPluginsElement, phaseUpgrades, context);
}
}
return upgrades;
}
/**
* Upgrades phase elements within a plugins section.
*/
private int upgradePhaseElementsInPlugins(
Element pluginsElement, Map<String, String> phaseUpgrades, UpgradeContext context) {
int upgrades = 0;
var pluginElements = pluginsElement.children(PLUGIN).toList();
for (Element pluginElement : pluginElements) {
Element executionsElement = pluginElement.child(EXECUTIONS).orElse(null);
if (executionsElement != null) {
var executionElements = executionsElement
.children(MavenPomElements.Elements.EXECUTION)
.toList();
for (Element executionElement : executionElements) {
Element phaseElement = executionElement
.child(MavenPomElements.Elements.PHASE)
.orElse(null);
if (phaseElement != null) {
String currentPhase = phaseElement.textContent().trim();
String newPhase = phaseUpgrades.get(currentPhase);
if (newPhase != null) {
phaseElement.textContent(newPhase);
context.detail("Upgraded phase: " + currentPhase + " ��� " + newPhase);
upgrades++;
}
}
}
}
}
return upgrades;
}
}