PluginUpgradeStrategy.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.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import eu.maveniverse.domtrip.Document;
import eu.maveniverse.domtrip.Editor;
import eu.maveniverse.domtrip.Element;
import org.apache.maven.api.cli.mvnup.UpgradeOptions;
import org.apache.maven.api.di.Inject;
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.api.model.Build;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Parent;
import org.apache.maven.api.model.Plugin;
import org.apache.maven.api.model.PluginManagement;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.ARTIFACT_ID;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.BUILD;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.DEPENDENCIES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.DEPENDENCY;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.DEPENDENCY_MANAGEMENT;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.GROUP_ID;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PARENT;
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.PROPERTIES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.VERSION;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Plugins.DEFAULT_MAVEN_PLUGIN_GROUP_ID;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Plugins.MAVEN_4_COMPATIBILITY_REASON;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Plugins.MAVEN_PLUGIN_PREFIX;
/**
* Strategy for upgrading Maven plugins to recommended versions. Handles plugin version upgrades in build/plugins and
* build/pluginManagement sections.
*/
@Named
@Singleton
@Priority(10)
public class PluginUpgradeStrategy extends AbstractUpgradeStrategy {
private static final List<PluginUpgrade> PLUGIN_UPGRADES = List.of(
new PluginUpgrade(
DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-compiler-plugin", "3.2", MAVEN_4_COMPATIBILITY_REASON),
new PluginUpgrade("org.codehaus.mojo", "exec-maven-plugin", "3.5.0", MAVEN_4_COMPATIBILITY_REASON),
new PluginUpgrade(
DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-enforcer-plugin", "3.5.0", MAVEN_4_COMPATIBILITY_REASON),
new PluginUpgrade("org.codehaus.mojo", "flatten-maven-plugin", "1.2.7", MAVEN_4_COMPATIBILITY_REASON),
new PluginUpgrade(
DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-shade-plugin", "3.5.0", MAVEN_4_COMPATIBILITY_REASON),
new PluginUpgrade(
DEFAULT_MAVEN_PLUGIN_GROUP_ID,
"maven-remote-resources-plugin",
"3.0.0",
MAVEN_4_COMPATIBILITY_REASON),
new PluginUpgrade(
DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-surefire-plugin", "3.5.2", MAVEN_4_COMPATIBILITY_REASON),
new PluginUpgrade(
DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-failsafe-plugin", "3.5.2", MAVEN_4_COMPATIBILITY_REASON),
new PluginUpgrade(
DEFAULT_MAVEN_PLUGIN_GROUP_ID,
"maven-surefire-report-plugin",
"3.5.2",
MAVEN_4_COMPATIBILITY_REASON),
new PluginUpgrade(
"net.alchim31.maven",
"scala-maven-plugin",
"4.9.5",
"Versions before 4.9.5 call add() on immutable lists returned by Maven 4 API"),
new PluginUpgrade(
DEFAULT_MAVEN_PLUGIN_GROUP_ID,
"maven-resources-plugin",
"3.3.1",
"Beta/RC versions compiled against different Maven 4 API signatures"),
new PluginUpgrade(
"org.codehaus.mojo",
"jaxb2-maven-plugin",
"3.2.0",
"Versions before 3.2.0 depend on jaxb-parent:3.0.0 which contains invalid XML rejected by Maven 4"),
new PluginUpgrade(
"io.quarkus", "quarkus-maven-plugin", "3.26.0", "Maven 4 compatibility (Aether API changes)"),
new PluginUpgrade(
"io.quarkus.platform",
"quarkus-maven-plugin",
"3.26.0",
"Maven 4 compatibility (Aether API changes)"),
new PluginUpgrade(
"org.codehaus.gmavenplus",
"gmavenplus-plugin",
"4.2.0",
"Versions before 4.2.0 call mutating methods on immutable lists returned by Maven 4 API"));
private static final List<PluginUpgrade> PLUGIN_DEPENDENCY_UPGRADES = List.of(new PluginUpgrade(
"org.codehaus.mojo",
"extra-enforcer-rules",
"1.4",
"Versions before 1.4 use a removed DependencyGraphBuilder API incompatible with Maven 4"));
@Inject
public PluginUpgradeStrategy() {}
@Override
public boolean isApplicable(UpgradeContext context) {
UpgradeOptions options = getOptions(context);
return isOptionEnabled(options, options.plugins(), true);
}
@Override
public String getDescription() {
return "Upgrading Maven plugins to recommended versions";
}
@Override
public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
Set<Path> processedPoms = new HashSet<>();
Set<Path> modifiedPoms = new HashSet<>();
Set<Path> errorPoms = new HashSet<>();
try {
// Phase 1: Write all modifications to temp directory (keeping project structure)
Path tempDir = createTempProjectStructure(context, pomMap);
// Phase 2: For each POM, build effective model using the session and analyze plugins
PluginAnalysisResults analysisResults = analyzePluginsUsingEffectiveModels(context, pomMap, tempDir);
// Collect locally declared plugin keys so we can add comments for remote-parent overrides
Set<String> localPluginKeys = collectLocallyDeclaredPluginKeys(pomMap);
// Phase 3: Add plugin management and direct overrides to the last local parent in hierarchy
for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Path pomPath = entry.getKey();
Document pomDocument = entry.getValue();
processedPoms.add(pomPath);
context.info(pomPath + " (checking for plugin upgrades)");
context.indent();
try {
boolean hasUpgrades = false;
// Apply direct plugin upgrades in the document
hasUpgrades |= upgradePluginsInDocument(pomDocument, context);
// Add plugin management based on effective model analysis
Set<String> pluginsForManagement =
analysisResults.pluginsNeedingManagement().get(pomPath);
if (pluginsForManagement != null && !pluginsForManagement.isEmpty()) {
hasUpgrades |= addPluginManagementForEffectivePlugins(
context, pomDocument, pluginsForManagement, localPluginKeys);
context.detail("Added plugin management to " + pomPath + " (target parent for "
+ pluginsForManagement.size() + " plugins)");
}
// Add direct plugin overrides in build/plugins for inherited plugins
// whose versions cannot be overridden via pluginManagement alone
Set<String> pluginsForDirectOverride =
analysisResults.pluginsNeedingDirectOverride().get(pomPath);
if (pluginsForDirectOverride != null && !pluginsForDirectOverride.isEmpty()) {
hasUpgrades |= addDirectPluginOverrides(
context, pomDocument, pluginsForDirectOverride, localPluginKeys);
}
if (hasUpgrades) {
modifiedPoms.add(pomPath);
context.success("Plugin upgrades applied");
} else {
context.success("No plugin upgrades needed");
}
} catch (Exception e) {
context.failure("Failed to upgrade plugins: " + e.getMessage());
errorPoms.add(pomPath);
} finally {
context.unindent();
}
}
// Clean up temp directory
cleanupTempDirectory(tempDir);
} catch (Exception e) {
context.failure("Failed to create temp project structure: " + e.getMessage());
// Mark all POMs as errors
errorPoms.addAll(pomMap.keySet());
}
return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
}
/**
* Upgrades plugins in the document. Checks both build/plugins and build/pluginManagement/plugins sections. Only
* processes plugins explicitly defined in the current POM document.
*/
private boolean upgradePluginsInDocument(Document pomDocument, UpgradeContext context) {
Element root = pomDocument.root();
boolean hasUpgrades = false;
// Define the plugins that need to be upgraded for Maven 4 compatibility
Map<String, PluginUpgradeInfo> pluginUpgrades = getPluginUpgradesMap();
// Check build/plugins
Element buildElement = root.childElement(BUILD).orElse(null);
if (buildElement != null) {
Element pluginsElement = buildElement.childElement(PLUGINS).orElse(null);
if (pluginsElement != null) {
hasUpgrades |= upgradePluginsInSection(
pluginsElement, pluginUpgrades, pomDocument, BUILD + "/" + PLUGINS, context);
}
// Check build/pluginManagement/plugins
Element pluginManagementElement =
buildElement.childElement(PLUGIN_MANAGEMENT).orElse(null);
if (pluginManagementElement != null) {
Element managedPluginsElement =
pluginManagementElement.childElement(PLUGINS).orElse(null);
if (managedPluginsElement != null) {
hasUpgrades |= upgradePluginsInSection(
managedPluginsElement,
pluginUpgrades,
pomDocument,
BUILD + "/" + PLUGIN_MANAGEMENT + "/" + PLUGINS,
context);
}
}
}
return hasUpgrades;
}
/**
* Returns the map of plugins that need to be upgraded for Maven 4 compatibility.
*/
private Map<String, PluginUpgradeInfo> getPluginUpgradesMap() {
Map<String, PluginUpgradeInfo> upgrades = new HashMap<>();
upgrades.put(
DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-compiler-plugin",
new PluginUpgradeInfo(DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-compiler-plugin", "3.2"));
upgrades.put(
"org.codehaus.mojo:exec-maven-plugin",
new PluginUpgradeInfo("org.codehaus.mojo", "exec-maven-plugin", "3.5.0"));
upgrades.put(
DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-enforcer-plugin",
new PluginUpgradeInfo(DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-enforcer-plugin", "3.5.0"));
upgrades.put(
"org.codehaus.mojo:flatten-maven-plugin",
new PluginUpgradeInfo("org.codehaus.mojo", "flatten-maven-plugin", "1.2.7"));
upgrades.put(
DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-shade-plugin",
new PluginUpgradeInfo(DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-shade-plugin", "3.5.0"));
upgrades.put(
DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-remote-resources-plugin",
new PluginUpgradeInfo(DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-remote-resources-plugin", "3.0.0"));
upgrades.put(
DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-surefire-plugin",
new PluginUpgradeInfo(DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-surefire-plugin", "3.5.2"));
upgrades.put(
DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-failsafe-plugin",
new PluginUpgradeInfo(DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-failsafe-plugin", "3.5.2"));
upgrades.put(
DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-surefire-report-plugin",
new PluginUpgradeInfo(DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-surefire-report-plugin", "3.5.2"));
upgrades.put(
"net.alchim31.maven:scala-maven-plugin",
new PluginUpgradeInfo("net.alchim31.maven", "scala-maven-plugin", "4.9.5"));
upgrades.put(
DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-resources-plugin",
new PluginUpgradeInfo(DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-resources-plugin", "3.3.1"));
upgrades.put(
"org.codehaus.mojo:jaxb2-maven-plugin",
new PluginUpgradeInfo("org.codehaus.mojo", "jaxb2-maven-plugin", "3.2.0"));
upgrades.put(
"io.quarkus:quarkus-maven-plugin",
new PluginUpgradeInfo("io.quarkus", "quarkus-maven-plugin", "3.26.0"));
upgrades.put(
"io.quarkus.platform:quarkus-maven-plugin",
new PluginUpgradeInfo("io.quarkus.platform", "quarkus-maven-plugin", "3.26.0"));
upgrades.put(
"org.codehaus.gmavenplus:gmavenplus-plugin",
new PluginUpgradeInfo("org.codehaus.gmavenplus", "gmavenplus-plugin", "4.2.0"));
return upgrades;
}
/**
* Upgrades plugins in a specific plugins section (either build/plugins or build/pluginManagement/plugins).
*/
private boolean upgradePluginsInSection(
Element pluginsElement,
Map<String, PluginUpgradeInfo> pluginUpgrades,
Document pomDocument,
String sectionName,
UpgradeContext context) {
return pluginsElement
.childElements(PLUGIN)
.map(pluginElement -> {
boolean upgraded = false;
String groupId = getChildText(pluginElement, GROUP_ID);
String artifactId = getChildText(pluginElement, ARTIFACT_ID);
// Default groupId for Maven plugins
if (groupId == null && artifactId != null && artifactId.startsWith(MAVEN_PLUGIN_PREFIX)) {
groupId = DEFAULT_MAVEN_PLUGIN_GROUP_ID;
}
if (groupId != null && artifactId != null) {
String pluginKey = groupId + ":" + artifactId;
PluginUpgradeInfo upgrade = pluginUpgrades.get(pluginKey);
if (upgrade != null) {
upgraded = upgradePluginVersion(pluginElement, upgrade, pomDocument, sectionName, context);
}
}
upgraded |= upgradePluginDependencies(pluginElement, pomDocument, sectionName, context);
return upgraded;
})
.reduce(false, Boolean::logicalOr);
}
/**
* Upgrades a specific plugin's version if needed.
*/
private boolean upgradePluginVersion(
Element pluginElement,
PluginUpgradeInfo upgrade,
Document pomDocument,
String sectionName,
UpgradeContext context) {
Element versionElement = pluginElement.childElement(VERSION).orElse(null);
String currentVersion;
boolean isProperty = false;
String propertyName = null;
if (versionElement != null) {
currentVersion = versionElement.textContentTrimmed();
// Check if version is a property reference
if (currentVersion.startsWith("${") && currentVersion.endsWith("}")) {
isProperty = true;
propertyName = currentVersion.substring(2, currentVersion.length() - 1);
}
} else {
// Plugin version might be inherited from parent or pluginManagement
context.debug("Plugin " + upgrade.groupId + ":" + upgrade.artifactId
+ " has no explicit version, may inherit from parent");
return false;
}
// For Quarkus plugins, check the platform version before upgrading.
// Upgrading quarkus-maven-plugin to 3.x when the project uses Quarkus 2.x
// causes NoSuchMethodError and build failures.
if (isQuarkusPlugin(upgrade.groupId, upgrade.artifactId)) {
String platformVersion = detectQuarkusPlatformVersion(pomDocument);
if (platformVersion != null) {
int majorVersion = extractMajorVersion(platformVersion);
if (majorVersion >= 0 && majorVersion < 3) {
context.warning("Skipping quarkus-maven-plugin upgrade: project uses Quarkus platform "
+ majorVersion + ".x (" + platformVersion
+ ") which is incompatible with plugin 3.x");
return false;
}
} else {
context.warning("Could not determine Quarkus platform version ��� if the project uses "
+ "Quarkus 2.x, the plugin upgrade may cause build failures");
}
}
if (isProperty) {
// For Quarkus plugins, check if the property is shared with a Quarkus BOM
if (isQuarkusPlugin(upgrade.groupId, upgrade.artifactId)
&& isPropertyUsedByQuarkusBom(pomDocument, propertyName)) {
return decoupleQuarkusPluginVersion(
pomDocument, versionElement, propertyName, upgrade, sectionName, context);
}
// Update property value if it's below minimum version
return upgradePropertyVersion(pomDocument, propertyName, upgrade, sectionName, context);
} else {
// Direct version comparison and upgrade
if (isVersionBelow(currentVersion, upgrade.minVersion)) {
Editor editor = new Editor(pomDocument);
editor.setTextContent(versionElement, upgrade.minVersion);
context.detail("Upgraded " + upgrade.groupId + ":" + upgrade.artifactId + " from " + currentVersion
+ " to " + upgrade.minVersion + " in " + sectionName);
return true;
} else {
context.debug("Plugin " + upgrade.groupId + ":" + upgrade.artifactId + " version " + currentVersion
+ " is already >= " + upgrade.minVersion);
}
}
return false;
}
/**
* Upgrades a property value if it represents a plugin version below the minimum.
*/
private boolean upgradePropertyVersion(
Document pomDocument,
String propertyName,
PluginUpgradeInfo upgrade,
String sectionName,
UpgradeContext context) {
Editor editor = new Editor(pomDocument);
Element root = editor.root();
Element propertiesElement = root.childElement(PROPERTIES).orElse(null);
if (propertiesElement != null) {
Element propertyElement =
propertiesElement.childElement(propertyName).orElse(null);
if (propertyElement != null) {
String currentVersion = propertyElement.textContentTrimmed();
if (isVersionBelow(currentVersion, upgrade.minVersion)) {
editor.setTextContent(propertyElement, upgrade.minVersion);
context.detail(
"Upgraded property " + propertyName + " (for " + upgrade.groupId + ":" + upgrade.artifactId
+ ") from " + currentVersion + " to " + upgrade.minVersion + " in " + sectionName);
return true;
} else {
context.debug("Property " + propertyName + " version " + currentVersion + " is already >= "
+ upgrade.minVersion);
}
} else {
context.warning("Property " + propertyName + " not found in POM properties");
}
} else {
context.warning("No properties section found in POM for property " + propertyName);
}
return false;
}
/**
* Upgrades plugin dependencies (e.g., extra-enforcer-rules inside maven-enforcer-plugin).
*/
private boolean upgradePluginDependencies(
Element pluginElement, Document pomDocument, String sectionName, UpgradeContext context) {
Element dependenciesElement = pluginElement.childElement(DEPENDENCIES).orElse(null);
if (dependenciesElement == null) {
return false;
}
Map<String, PluginUpgradeInfo> depUpgrades = getPluginDependencyUpgradesMap();
return dependenciesElement
.childElements(DEPENDENCY)
.map(depElement -> {
String groupId = getChildText(depElement, GROUP_ID);
String artifactId = getChildText(depElement, ARTIFACT_ID);
if (groupId != null && artifactId != null) {
String depKey = groupId + ":" + artifactId;
PluginUpgradeInfo upgrade = depUpgrades.get(depKey);
if (upgrade != null) {
return upgradePluginVersion(
depElement, upgrade, pomDocument, sectionName + "/plugin/dependencies", context);
}
}
return false;
})
.reduce(false, Boolean::logicalOr);
}
private Map<String, PluginUpgradeInfo> getPluginDependencyUpgradesMap() {
return PLUGIN_DEPENDENCY_UPGRADES.stream()
.collect(Collectors.toMap(
upgrade -> upgrade.groupId() + ":" + upgrade.artifactId(),
upgrade ->
new PluginUpgradeInfo(upgrade.groupId(), upgrade.artifactId(), upgrade.minVersion())));
}
/**
* Simple version comparison to check if current version is below minimum version. This is a basic implementation
* that works for most Maven plugin versions.
*/
private boolean isVersionBelow(String currentVersion, String minVersion) {
if (currentVersion == null || minVersion == null) {
return false;
}
return getSession().parseVersion(currentVersion).compareTo(getSession().parseVersion(minVersion)) < 0;
}
/**
* Helper method to get child element text.
*/
private String getChildText(Element parent, String childName) {
Element child = parent.childElement(childName).orElse(null);
return child != null ? child.textContentTrimmed() : null;
}
/**
* Gets the list of plugin upgrades to apply.
*/
public static List<PluginUpgrade> getPluginUpgrades() {
return PLUGIN_UPGRADES;
}
/**
* Analyzes plugins using effective models built from the temp directory. Returns analysis results with two maps:
* plugins needing pluginManagement entries and plugins needing direct build/plugins overrides.
*/
private PluginAnalysisResults analyzePluginsUsingEffectiveModels(
UpgradeContext context, Map<Path, Document> pomMap, Path tempDir) {
Map<Path, Set<String>> managementResult = new HashMap<>();
Map<Path, Set<String>> directOverrideResult = new HashMap<>();
Map<String, PluginUpgrade> pluginUpgrades = getPluginUpgradesAsMap();
for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Path originalPomPath = entry.getKey();
try {
// Find the corresponding temp POM path
Path commonRoot = findCommonRoot(pomMap.keySet());
Path relativePath = commonRoot.relativize(originalPomPath);
Path tempPomPath = tempDir.resolve(relativePath);
// Build effective model using Maven 4 API
PluginAnalysis analysis = analyzeEffectiveModelForPlugins(context, tempPomPath, pluginUpgrades);
// Determine where to add plugin management (last local parent)
Path targetPom =
findLastLocalParentForPluginManagement(context, tempPomPath, pomMap, tempDir, commonRoot);
if (targetPom != null) {
managementResult
.computeIfAbsent(targetPom, k -> new HashSet<>())
.addAll(analysis.needsManagement());
directOverrideResult
.computeIfAbsent(targetPom, k -> new HashSet<>())
.addAll(analysis.needsDirectOverride());
if (!analysis.needsManagement().isEmpty()) {
context.debug("Will add plugin management to " + targetPom + " for plugins: "
+ analysis.needsManagement());
}
if (!analysis.needsDirectOverride().isEmpty()) {
context.debug("Will add direct plugin overrides to " + targetPom + " for plugins: "
+ analysis.needsDirectOverride());
}
}
} catch (Exception e) {
context.warning("Failed to analyze effective model for " + originalPomPath + ": " + e.getMessage());
}
}
return new PluginAnalysisResults(managementResult, directOverrideResult);
}
/**
* Converts PluginUpgradeInfo map to PluginUpgrade map for compatibility.
*/
private Map<String, PluginUpgrade> getPluginUpgradesAsMap() {
return PLUGIN_UPGRADES.stream()
.collect(Collectors.toMap(
upgrade -> upgrade.groupId() + ":" + upgrade.artifactId(), upgrade -> upgrade));
}
private PluginAnalysis analyzeEffectiveModelForPlugins(
UpgradeContext context, Path tempPomPath, Map<String, PluginUpgrade> pluginUpgrades) {
Model effectiveModel = buildEffectiveModel(tempPomPath);
return analyzePluginsFromEffectiveModel(context, effectiveModel, pluginUpgrades);
}
/**
* Analyzes plugins from the effective model and determines which ones need upgrades. Separates plugins into those
* overridable via pluginManagement and those requiring a direct build/plugins entry (because the version is set
* explicitly in an inherited parent's build/plugins, not via pluginManagement).
*/
private PluginAnalysis analyzePluginsFromEffectiveModel(
UpgradeContext context, Model effectiveModel, Map<String, PluginUpgrade> pluginUpgrades) {
Set<String> needsManagement = new HashSet<>();
Set<String> needsDirectOverride = new HashSet<>();
Build build = effectiveModel.getBuild();
if (build != null) {
// Collect managed plugin versions for comparison
Map<String, String> managedVersions = new HashMap<>();
PluginManagement pluginManagement = build.getPluginManagement();
if (pluginManagement != null) {
for (Plugin plugin : pluginManagement.getPlugins()) {
String pluginKey = getPluginKey(plugin);
managedVersions.put(pluginKey, plugin.getVersion());
}
}
// Check build/plugins - these are the actual plugins used in the build
for (Plugin plugin : build.getPlugins()) {
String pluginKey = getPluginKey(plugin);
PluginUpgrade upgrade = pluginUpgrades.get(pluginKey);
if (upgrade != null) {
String effectiveVersion = plugin.getVersion();
if (isVersionBelow(effectiveVersion, upgrade.minVersion())) {
needsManagement.add(pluginKey);
String managedVersion = managedVersions.get(pluginKey);
if (managedVersion == null || !managedVersion.equals(effectiveVersion)) {
// Version differs from pluginManagement (or not in PM at all):
// the parent sets an explicit version in build/plugins that
// pluginManagement alone cannot override
needsDirectOverride.add(pluginKey);
context.debug("Plugin " + pluginKey + " version " + effectiveVersion
+ " has explicit version in inherited build/plugins"
+ " ��� needs direct override to " + upgrade.minVersion());
} else {
context.debug("Plugin " + pluginKey + " version " + effectiveVersion
+ " is managed via pluginManagement ��� needs upgrade to " + upgrade.minVersion());
}
}
}
}
// Check build/pluginManagement/plugins for managed-only plugins
if (pluginManagement != null) {
for (Plugin plugin : pluginManagement.getPlugins()) {
String pluginKey = getPluginKey(plugin);
PluginUpgrade upgrade = pluginUpgrades.get(pluginKey);
if (upgrade != null && !needsManagement.contains(pluginKey)) {
String effectiveVersion = plugin.getVersion();
if (isVersionBelow(effectiveVersion, upgrade.minVersion())) {
needsManagement.add(pluginKey);
context.debug("Managed plugin " + pluginKey + " version " + effectiveVersion
+ " needs upgrade to " + upgrade.minVersion());
}
}
}
}
}
return new PluginAnalysis(needsManagement, needsDirectOverride);
}
/**
* Gets the plugin key (groupId:artifactId) for a plugin, handling default groupId.
*/
private String getPluginKey(Plugin plugin) {
String groupId = plugin.getGroupId();
String artifactId = plugin.getArtifactId();
// Default groupId for Maven plugins
if (groupId == null && artifactId != null && artifactId.startsWith(MAVEN_PLUGIN_PREFIX)) {
groupId = DEFAULT_MAVEN_PLUGIN_GROUP_ID;
}
return groupId + ":" + artifactId;
}
/**
* Finds the last local parent in the hierarchy where plugin management should be added. This implements the
* algorithm: start with the effective model, check if parent is in pomMap, if so continue to its parent, else
* that's the target.
*/
private Path findLastLocalParentForPluginManagement(
UpgradeContext context, Path tempPomPath, Map<Path, Document> pomMap, Path tempDir, Path commonRoot) {
Model effectiveModel = buildEffectiveModel(tempPomPath);
// Convert the temp path back to the original path
Path relativePath = tempDir.relativize(tempPomPath);
Path currentOriginalPath = commonRoot.resolve(relativePath);
// Start with current POM as the candidate
Path lastLocalParent = currentOriginalPath;
// Walk up the parent hierarchy
Model currentModel = effectiveModel;
while (currentModel.getParent() != null) {
Parent parent = currentModel.getParent();
// Check if this parent is in our local pomMap
Path parentPath = findParentInPomMap(parent, pomMap);
if (parentPath != null) {
// Parent is local, so it becomes our new candidate
lastLocalParent = parentPath;
Path parentTempPath = tempDir.resolve(commonRoot.relativize(parentPath));
currentModel = buildEffectiveModel(parentTempPath);
} else {
// Parent is external, stop here
break;
}
}
context.debug("Last local parent for " + currentOriginalPath + " is " + lastLocalParent);
return lastLocalParent;
}
/**
* Finds a parent POM in the pomMap based on its coordinates.
*/
private Path findParentInPomMap(Parent parent, Map<Path, Document> pomMap) {
String parentGroupId = parent.getGroupId();
String parentArtifactId = parent.getArtifactId();
String parentVersion = parent.getVersion();
for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Document doc = entry.getValue();
Element root = doc.root();
// Extract GAV from this POM
String groupId = getChildText(root, GROUP_ID);
String artifactId = getChildText(root, ARTIFACT_ID);
String version = getChildText(root, VERSION);
// Handle inheritance from parent
Element parentElement = root.childElement(PARENT).orElse(null);
if (parentElement != null) {
if (groupId == null) {
groupId = getChildText(parentElement, GROUP_ID);
}
if (version == null) {
version = getChildText(parentElement, VERSION);
}
}
// Check if this POM matches the parent coordinates
if (parentGroupId.equals(groupId) && parentArtifactId.equals(artifactId) && parentVersion.equals(version)) {
return entry.getKey();
}
}
return null; // Parent not found in local project
}
/**
* Adds plugin management entries for plugins found through effective model analysis.
*/
private boolean addPluginManagementForEffectivePlugins(
UpgradeContext context, Document pomDocument, Set<String> pluginKeys, Set<String> localPluginKeys) {
Map<String, PluginUpgrade> pluginUpgrades = getPluginUpgradesAsMap();
boolean hasUpgrades = false;
Element root = pomDocument.root();
// Ensure build/pluginManagement/plugins structure exists
Element buildElement = root.childElement(BUILD).orElse(null);
if (buildElement == null) {
buildElement = DomUtils.insertNewElement(BUILD, root);
}
Element pluginManagementElement =
buildElement.childElement(PLUGIN_MANAGEMENT).orElse(null);
if (pluginManagementElement == null) {
pluginManagementElement = DomUtils.insertNewElement(PLUGIN_MANAGEMENT, buildElement);
}
Element managedPluginsElement =
pluginManagementElement.childElement(PLUGINS).orElse(null);
if (managedPluginsElement == null) {
managedPluginsElement = DomUtils.insertNewElement(PLUGINS, pluginManagementElement);
}
// Add plugin management entries for each plugin
for (String pluginKey : pluginKeys) {
PluginUpgrade upgrade = pluginUpgrades.get(pluginKey);
if (upgrade != null) {
// Check if plugin is already managed
if (!isPluginAlreadyManagedInElement(managedPluginsElement, upgrade)) {
boolean fromRemoteParent = !localPluginKeys.contains(pluginKey);
addPluginManagementEntryFromUpgrade(managedPluginsElement, upgrade, context, fromRemoteParent);
hasUpgrades = true;
}
}
}
return hasUpgrades;
}
/**
* Checks if a plugin is already managed in the given plugins element.
*/
private boolean isPluginAlreadyManagedInElement(Element pluginsElement, PluginUpgrade upgrade) {
List<Element> pluginElements = pluginsElement.childElements(PLUGIN).toList();
for (Element pluginElement : pluginElements) {
String groupId = getChildText(pluginElement, GROUP_ID);
String artifactId = getChildText(pluginElement, ARTIFACT_ID);
// Default groupId for Maven plugins
if (groupId == null && artifactId != null && artifactId.startsWith(MAVEN_PLUGIN_PREFIX)) {
groupId = DEFAULT_MAVEN_PLUGIN_GROUP_ID;
}
if (upgrade.groupId().equals(groupId) && upgrade.artifactId().equals(artifactId)) {
return true;
}
}
return false;
}
/**
* Adds a plugin management entry from a PluginUpgrade.
*/
private void addPluginManagementEntryFromUpgrade(
Element managedPluginsElement, PluginUpgrade upgrade, UpgradeContext context, boolean fromRemoteParent) {
Element plugin = DomUtils.createPlugin(
managedPluginsElement, upgrade.groupId(), upgrade.artifactId(), upgrade.minVersion());
if (fromRemoteParent) {
new Editor(managedPluginsElement.document())
.insertCommentBefore(plugin, " Override version inherited from parent ");
context.detail("Added plugin management for " + upgrade.groupId() + ":" + upgrade.artifactId() + " version "
+ upgrade.minVersion() + " (overrides version inherited from parent)");
} else {
context.detail("Added plugin management for " + upgrade.groupId() + ":" + upgrade.artifactId() + " version "
+ upgrade.minVersion() + " (found through effective model analysis)");
}
}
/**
* Adds direct plugin entries in build/plugins for plugins inherited from remote parents. This is necessary when a
* parent POM sets an explicit version in its build/plugins that pluginManagement alone cannot override.
*/
private boolean addDirectPluginOverrides(
UpgradeContext context, Document pomDocument, Set<String> pluginKeys, Set<String> localPluginKeys) {
Map<String, PluginUpgrade> pluginUpgrades = getPluginUpgradesAsMap();
boolean hasUpgrades = false;
Element root = pomDocument.root();
Element buildElement = root.childElement(BUILD).orElse(null);
if (buildElement == null) {
buildElement = DomUtils.insertNewElement(BUILD, root);
}
Element pluginsElement = buildElement.childElement(PLUGINS).orElse(null);
if (pluginsElement == null) {
pluginsElement = DomUtils.insertNewElement(PLUGINS, buildElement);
}
for (String pluginKey : pluginKeys) {
PluginUpgrade upgrade = pluginUpgrades.get(pluginKey);
if (upgrade != null) {
if (!isPluginAlreadyManagedInElement(pluginsElement, upgrade)) {
Element plugin = DomUtils.createPlugin(
pluginsElement, upgrade.groupId(), upgrade.artifactId(), upgrade.minVersion());
if (!localPluginKeys.contains(pluginKey)) {
new Editor(pluginsElement.document())
.insertCommentBefore(plugin, " Override version inherited from parent ");
}
hasUpgrades = true;
context.detail("Added " + upgrade.groupId() + ":" + upgrade.artifactId() + " version "
+ upgrade.minVersion() + " in build/plugins (overrides version locked by parent)");
}
}
}
return hasUpgrades;
}
private Set<String> collectLocallyDeclaredPluginKeys(Map<Path, Document> pomMap) {
Set<String> localPluginKeys = new HashSet<>();
for (Document doc : pomMap.values()) {
Element root = doc.root();
Element buildElement = root.childElement(BUILD).orElse(null);
if (buildElement != null) {
Element pluginsElement = buildElement.childElement(PLUGINS).orElse(null);
if (pluginsElement != null) {
collectPluginKeysFromElement(pluginsElement, localPluginKeys);
}
Element pmElement = buildElement.childElement(PLUGIN_MANAGEMENT).orElse(null);
if (pmElement != null) {
Element managedPluginsElement =
pmElement.childElement(PLUGINS).orElse(null);
if (managedPluginsElement != null) {
collectPluginKeysFromElement(managedPluginsElement, localPluginKeys);
}
}
}
}
return localPluginKeys;
}
private void collectPluginKeysFromElement(Element pluginsElement, Set<String> keys) {
pluginsElement.childElements(PLUGIN).forEach(pluginElement -> {
String groupId = getChildText(pluginElement, GROUP_ID);
String artifactId = getChildText(pluginElement, ARTIFACT_ID);
if (groupId == null && artifactId != null && artifactId.startsWith(MAVEN_PLUGIN_PREFIX)) {
groupId = DEFAULT_MAVEN_PLUGIN_GROUP_ID;
}
if (groupId != null && artifactId != null) {
keys.add(groupId + ":" + artifactId);
}
});
}
private record PluginAnalysis(Set<String> needsManagement, Set<String> needsDirectOverride) {}
private record PluginAnalysisResults(
Map<Path, Set<String>> pluginsNeedingManagement, Map<Path, Set<String>> pluginsNeedingDirectOverride) {}
/**
* Checks if the given plugin is a Quarkus Maven plugin.
*/
private boolean isQuarkusPlugin(String groupId, String artifactId) {
return "quarkus-maven-plugin".equals(artifactId)
&& ("io.quarkus".equals(groupId) || "io.quarkus.platform".equals(groupId));
}
/**
* Detects the Quarkus platform version used by the project.
*
* <p>Checks the following sources in order:
* <ol>
* <li>{@code <dependencyManagement>} for {@code io.quarkus.platform:quarkus-bom}
* or {@code io.quarkus:quarkus-bom} ��� extracts the version (resolving property references)</li>
* <li>Properties: {@code quarkus.platform.version}, {@code quarkus.version},
* {@code quarkus-plugin.version}</li>
* </ol>
*
* @param pomDocument the POM document to inspect
* @return the detected Quarkus platform version string, or {@code null} if not found
*/
String detectQuarkusPlatformVersion(Document pomDocument) {
Element root = pomDocument.root();
// 1. Check dependencyManagement for Quarkus BOM
Element depManagement = root.childElement(DEPENDENCY_MANAGEMENT).orElse(null);
if (depManagement != null) {
Element dependencies = depManagement.childElement(DEPENDENCIES).orElse(null);
if (dependencies != null) {
String bomVersion = dependencies
.childElements(DEPENDENCY)
.filter(dep -> {
String gid = getChildText(dep, GROUP_ID);
String aid = getChildText(dep, ARTIFACT_ID);
return ("io.quarkus.platform".equals(gid) || "io.quarkus".equals(gid))
&& "quarkus-bom".equals(aid);
})
.map(dep -> getChildText(dep, VERSION))
.filter(v -> v != null && !v.isEmpty())
.findFirst()
.orElse(null);
if (bomVersion != null) {
// Resolve property reference if needed
String resolved = resolvePropertyValue(root, bomVersion);
if (resolved != null) {
return resolved;
}
}
}
}
// 2. Check well-known properties
Element propertiesElement = root.childElement(PROPERTIES).orElse(null);
if (propertiesElement != null) {
for (String propName : List.of("quarkus.platform.version", "quarkus.version", "quarkus-plugin.version")) {
Element prop = propertiesElement.childElement(propName).orElse(null);
if (prop != null) {
String value = prop.textContentTrimmed();
if (value != null && !value.isEmpty() && !value.startsWith("${")) {
return value;
}
}
}
}
return null;
}
/**
* Resolves a version string that may be a property reference (e.g., {@code ${quarkus.version}}).
* Returns the resolved value, or the original string if not a property reference,
* or {@code null} if the property cannot be resolved.
*/
private String resolvePropertyValue(Element root, String value) {
if (value == null || value.isEmpty()) {
return null;
}
if (!value.startsWith("${") || !value.endsWith("}")) {
return value;
}
String propertyName = value.substring(2, value.length() - 1);
Element propertiesElement = root.childElement(PROPERTIES).orElse(null);
if (propertiesElement != null) {
Element prop = propertiesElement.childElement(propertyName).orElse(null);
if (prop != null) {
String resolved = prop.textContentTrimmed();
if (resolved != null && !resolved.isEmpty() && !resolved.startsWith("${")) {
return resolved;
}
}
}
return null;
}
/**
* Extracts the major version number from a version string (e.g., "2" from "2.16.7.Final").
*
* @return the major version number, or -1 if it cannot be parsed
*/
private int extractMajorVersion(String version) {
if (version == null || version.isEmpty()) {
return -1;
}
String[] parts = version.split("\\.");
try {
return Integer.parseInt(parts[0]);
} catch (NumberFormatException e) {
return -1;
}
}
/**
* Checks if a property is used as the version of a Quarkus BOM in dependencyManagement.
* Quarkus BOMs are identified by having groupId io.quarkus or io.quarkus.platform,
* type "pom", and scope "import".
*/
private boolean isPropertyUsedByQuarkusBom(Document pomDocument, String propertyName) {
Element root = pomDocument.root();
String propertyRef = "${" + propertyName + "}";
Element depManagement = root.childElement(DEPENDENCY_MANAGEMENT).orElse(null);
if (depManagement == null) {
return false;
}
Element dependencies = depManagement.childElement(DEPENDENCIES).orElse(null);
if (dependencies == null) {
return false;
}
return dependencies.childElements(DEPENDENCY).anyMatch(dep -> {
String groupId = getChildText(dep, GROUP_ID);
String version = getChildText(dep, VERSION);
String type = getChildText(dep, "type");
String scope = getChildText(dep, "scope");
return ("io.quarkus".equals(groupId) || "io.quarkus.platform".equals(groupId))
&& "pom".equals(type)
&& "import".equals(scope)
&& propertyRef.equals(version);
});
}
/**
* Decouples the Quarkus plugin version from a shared BOM property.
* Introduces a new property for the plugin version and updates the plugin's version element,
* leaving the BOM property unchanged.
*/
private boolean decoupleQuarkusPluginVersion(
Document pomDocument,
Element versionElement,
String sharedPropertyName,
PluginUpgradeInfo upgrade,
String sectionName,
UpgradeContext context) {
// Resolve the current version from the shared property
Editor editor = new Editor(pomDocument);
Element root = editor.root();
Element propertiesElement = root.childElement(PROPERTIES).orElse(null);
String currentVersion = null;
if (propertiesElement != null) {
Element sharedProp =
propertiesElement.childElement(sharedPropertyName).orElse(null);
if (sharedProp != null) {
currentVersion = sharedProp.textContentTrimmed();
}
}
if (currentVersion == null) {
// Property is inherited from parent ��� we cannot resolve its actual value here,
// so skip decoupling to avoid introducing a potentially unnecessary property
// that could downgrade an already-sufficient inherited version.
context.debug("Shared property " + sharedPropertyName
+ " not found in current POM (may be inherited) ��� skipping version decoupling");
return false;
}
if (!isVersionBelow(currentVersion, upgrade.minVersion)) {
context.debug("Quarkus plugin version (via shared property " + sharedPropertyName + ") " + currentVersion
+ " is already >= " + upgrade.minVersion);
return false;
}
// Introduce a new property for the plugin version
String newPropertyName = "quarkus-plugin.version";
if (propertiesElement == null) {
propertiesElement = DomUtils.insertNewElement(PROPERTIES, root);
}
// Add the new property if it doesn't already exist
Element existingProp = propertiesElement.childElement(newPropertyName).orElse(null);
if (existingProp != null) {
// Property already exists ��� update its value
editor.setTextContent(existingProp, upgrade.minVersion);
} else {
DomUtils.insertContentElement(propertiesElement, newPropertyName, upgrade.minVersion);
}
// Update the plugin's version element to reference the new property
editor.setTextContent(versionElement, "${" + newPropertyName + "}");
context.detail("Decoupled " + upgrade.groupId + ":" + upgrade.artifactId + " version from shared property "
+ sharedPropertyName + ": introduced " + newPropertyName + "=" + upgrade.minVersion + " in "
+ sectionName);
// Emit version gap warning
if (currentVersion != null) {
emitVersionGapWarning(context, currentVersion, upgrade.minVersion);
}
return true;
}
/**
* Emits a warning if the Quarkus platform BOM version is significantly older than the plugin version.
* Mismatched plugin and platform versions may cause unexpected behavior because the plugin
* is designed and tested against a specific platform version.
*/
private void emitVersionGapWarning(UpgradeContext context, String platformVersion, String pluginVersion) {
// Only warn when there's a meaningful gap (different minor version)
String platformMinor = extractMinorVersion(platformVersion);
String pluginMinor = extractMinorVersion(pluginVersion);
if (platformMinor != null && pluginMinor != null && !platformMinor.equals(pluginMinor)) {
context.warning("quarkus-maven-plugin upgraded to " + pluginVersion
+ " for Maven 4 compatibility. Your Quarkus platform is still at " + platformVersion
+ ". Consider upgrading the platform to match ��� mismatched plugin and platform"
+ " versions may cause unexpected behavior.");
}
}
/**
* Extracts the minor version component (e.g., "26" from "3.26.0").
*/
private String extractMinorVersion(String version) {
if (version == null) {
return null;
}
String[] parts = version.split("\\.");
if (parts.length >= 2) {
return parts[1];
}
return null;
}
/**
* Holds plugin upgrade information for Maven 4 compatibility. This class contains the minimum version requirements
* for plugins that need to be upgraded to work properly with Maven 4.
*/
public static class PluginUpgradeInfo {
/** The Maven groupId of the plugin */
final String groupId;
/** The Maven artifactId of the plugin */
final String artifactId;
/** The minimum version required for Maven 4 compatibility */
final String minVersion;
/**
* Creates a new plugin upgrade information holder.
*
* @param groupId
* the Maven groupId of the plugin
* @param artifactId
* the Maven artifactId of the plugin
* @param minVersion
* the minimum version required for Maven 4 compatibility
*/
PluginUpgradeInfo(String groupId, String artifactId, String minVersion) {
this.groupId = groupId;
this.artifactId = artifactId;
this.minVersion = minVersion;
}
}
}