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.io.File;
import java.io.FileWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
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.RemoteRepository;
import org.apache.maven.api.Session;
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.api.model.Repository;
import org.apache.maven.api.model.RepositoryPolicy;
import org.apache.maven.api.services.ModelBuilder;
import org.apache.maven.api.services.ModelBuilderRequest;
import org.apache.maven.api.services.ModelBuilderResult;
import org.apache.maven.api.services.RepositoryFactory;
import org.apache.maven.api.services.Sources;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
import org.apache.maven.impl.standalone.ApiRunner;
import org.codehaus.plexus.components.secdispatcher.Dispatcher;
import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher;
import org.eclipse.aether.internal.impl.DefaultPathProcessor;
import org.eclipse.aether.internal.impl.DefaultTransporterProvider;
import org.eclipse.aether.internal.impl.transport.http.DefaultChecksumExtractor;
import org.eclipse.aether.spi.connector.transport.TransporterProvider;
import org.eclipse.aether.transport.file.FileTransporterFactory;
import org.eclipse.aether.transport.jdk.JdkTransporterFactory;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.output.XMLOutputter;

import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Plugins.DEFAULT_MAVEN_PLUGIN_GROUP_ID;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Plugins.MAVEN_4_COMPATIBILITY_REASON;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Plugins.MAVEN_PLUGIN_PREFIX;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.ARTIFACT_ID;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.BUILD;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.GROUP_ID;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PARENT;
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.VERSION;

/**
 * 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.0", MAVEN_4_COMPATIBILITY_REASON),
            new PluginUpgrade(
                    DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-exec-plugin", "3.2.0", MAVEN_4_COMPATIBILITY_REASON),
            new PluginUpgrade(
                    DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-enforcer-plugin", "3.0.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));

    private Session session;

    @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
            Map<Path, Set<String>> pluginsNeedingManagement =
                    analyzePluginsUsingEffectiveModels(context, pomMap, tempDir);

            // Phase 3: Add plugin management 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
                    // Note: pluginsNeedingManagement only contains entries for POMs that should receive plugin
                    // management
                    // (i.e., the "last local parent" for each plugin that needs management)
                    Set<String> pluginsForThisPom = pluginsNeedingManagement.get(pomPath);
                    if (pluginsForThisPom != null && !pluginsForThisPom.isEmpty()) {
                        hasUpgrades |= addPluginManagementForEffectivePlugins(context, pomDocument, pluginsForThisPom);
                        context.detail("Added plugin management to " + pomPath + " (target parent for "
                                + pluginsForThisPom.size() + " plugins)");
                    }

                    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.getRootElement();
        Namespace namespace = root.getNamespace();
        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.getChild(UpgradeConstants.XmlElements.BUILD, namespace);
        if (buildElement != null) {
            Element pluginsElement = buildElement.getChild(PLUGINS, namespace);
            if (pluginsElement != null) {
                hasUpgrades |= upgradePluginsInSection(
                        pluginsElement, namespace, pluginUpgrades, pomDocument, BUILD + "/" + PLUGINS, context);
            }

            // Check build/pluginManagement/plugins
            Element pluginManagementElement = buildElement.getChild(PLUGIN_MANAGEMENT, namespace);
            if (pluginManagementElement != null) {
                Element managedPluginsElement = pluginManagementElement.getChild(PLUGINS, namespace);
                if (managedPluginsElement != null) {
                    hasUpgrades |= upgradePluginsInSection(
                            managedPluginsElement,
                            namespace,
                            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.2.0"));
        upgrades.put(
                DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-enforcer-plugin",
                new PluginUpgradeInfo(DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-enforcer-plugin", "3.0.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"));
        return upgrades;
    }

    /**
     * Upgrades plugins in a specific plugins section (either build/plugins or build/pluginManagement/plugins).
     */
    private boolean upgradePluginsInSection(
            Element pluginsElement,
            Namespace namespace,
            Map<String, PluginUpgradeInfo> pluginUpgrades,
            Document pomDocument,
            String sectionName,
            UpgradeContext context) {
        boolean hasUpgrades = false;
        List<Element> pluginElements = pluginsElement.getChildren(PLUGIN, namespace);

        for (Element pluginElement : pluginElements) {
            String groupId = getChildText(pluginElement, GROUP_ID, namespace);
            String artifactId = getChildText(pluginElement, ARTIFACT_ID, namespace);

            // 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) {
                    if (upgradePluginVersion(pluginElement, namespace, upgrade, pomDocument, sectionName, context)) {
                        hasUpgrades = true;
                    }
                }
            }
        }

        return hasUpgrades;
    }

    /**
     * Upgrades a specific plugin's version if needed.
     */
    private boolean upgradePluginVersion(
            Element pluginElement,
            Namespace namespace,
            PluginUpgradeInfo upgrade,
            Document pomDocument,
            String sectionName,
            UpgradeContext context) {
        Element versionElement = pluginElement.getChild(VERSION, namespace);
        String currentVersion;
        boolean isProperty = false;
        String propertyName = null;

        if (versionElement != null) {
            currentVersion = versionElement.getTextTrim();
            // 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;
        }

        if (isProperty) {
            // 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)) {
                versionElement.setText(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) {
        Element root = pomDocument.getRootElement();
        Namespace namespace = root.getNamespace();
        Element propertiesElement = root.getChild(UpgradeConstants.XmlElements.PROPERTIES, namespace);

        if (propertiesElement != null) {
            Element propertyElement = propertiesElement.getChild(propertyName, namespace);
            if (propertyElement != null) {
                String currentVersion = propertyElement.getTextTrim();
                if (isVersionBelow(currentVersion, upgrade.minVersion)) {
                    propertyElement.setText(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;
    }

    /**
     * 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;
        }

        // Remove any qualifiers like -SNAPSHOT, -alpha, etc. for comparison
        String cleanCurrent = currentVersion.split("-")[0];
        String cleanMin = minVersion.split("-")[0];

        try {
            String[] currentParts = cleanCurrent.split("\\.");
            String[] minParts = cleanMin.split("\\.");

            int maxLength = Math.max(currentParts.length, minParts.length);

            for (int i = 0; i < maxLength; i++) {
                int currentPart = i < currentParts.length ? Integer.parseInt(currentParts[i]) : 0;
                int minPart = i < minParts.length ? Integer.parseInt(minParts[i]) : 0;

                if (currentPart < minPart) {
                    return true;
                } else if (currentPart > minPart) {
                    return false;
                }
            }

            return false; // Versions are equal
        } catch (NumberFormatException e) {
            // Fallback to string comparison if parsing fails
            return currentVersion.compareTo(minVersion) < 0;
        }
    }

    /**
     * Helper method to get child element text.
     */
    private String getChildText(Element parent, String childName, Namespace namespace) {
        Element child = parent.getChild(childName, namespace);
        return child != null ? child.getTextTrim() : null;
    }

    /**
     * Gets the list of plugin upgrades to apply.
     */
    public static List<PluginUpgrade> getPluginUpgrades() {
        return PLUGIN_UPGRADES;
    }

    /**
     * Gets or creates the cached Maven 4 session.
     */
    private Session getSession() {
        if (session == null) {
            session = createMaven4Session();
        }
        return session;
    }

    /**
     * Creates a new Maven 4 session for effective POM computation.
     */
    private Session createMaven4Session() {
        Session session = ApiRunner.createSession(injector -> {
            injector.bindInstance(Dispatcher.class, new LegacyDispatcher());

            injector.bindInstance(
                    TransporterProvider.class,
                    new DefaultTransporterProvider(Map.of(
                            "https",
                            new JdkTransporterFactory(
                                    new DefaultChecksumExtractor(Map.of()), new DefaultPathProcessor()),
                            "file",
                            new FileTransporterFactory())));
        });

        // Configure repositories
        // TODO: we should read settings
        RemoteRepository central =
                session.createRemoteRepository(RemoteRepository.CENTRAL_ID, "https://repo.maven.apache.org/maven2");
        RemoteRepository snapshots = session.getService(RepositoryFactory.class)
                .createRemote(Repository.newBuilder()
                        .id("apache-snapshots")
                        .url("https://repository.apache.org/content/repositories/snapshots/")
                        .releases(RepositoryPolicy.newBuilder().enabled("false").build())
                        .snapshots(RepositoryPolicy.newBuilder().enabled("true").build())
                        .build());

        return session.withRemoteRepositories(List.of(central, snapshots));
    }

    /**
     * Creates a temporary project structure with all POMs written to preserve relative paths.
     * This allows Maven 4 API to properly resolve the project hierarchy.
     */
    private Path createTempProjectStructure(UpgradeContext context, Map<Path, Document> pomMap) throws Exception {
        Path tempDir = Files.createTempDirectory("mvnup-project-");
        context.debug("Created temp project directory: " + tempDir);

        // Find the common root of all POM paths to preserve relative structure
        Path commonRoot = findCommonRoot(pomMap.keySet());
        context.debug("Common root: " + commonRoot);

        // Write each POM to the temp directory, preserving relative structure
        for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
            Path originalPath = entry.getKey();
            Document document = entry.getValue();

            // Calculate the relative path from common root
            Path relativePath = commonRoot.relativize(originalPath);
            Path tempPomPath = tempDir.resolve(relativePath);

            // Ensure parent directories exist
            Files.createDirectories(tempPomPath.getParent());

            // Write POM to temp location
            writePomToFile(document, tempPomPath);
            context.debug("Wrote POM to temp location: " + tempPomPath);
        }

        return tempDir;
    }

    /**
     * Finds the common root directory of all POM paths.
     */
    private Path findCommonRoot(Set<Path> pomPaths) {
        Path commonRoot = null;
        for (Path pomPath : pomPaths) {
            Path parent = pomPath.getParent();
            if (parent == null) {
                parent = Path.of(".");
            }
            if (commonRoot == null) {
                commonRoot = parent;
            } else {
                // Find common ancestor
                while (!parent.startsWith(commonRoot)) {
                    commonRoot = commonRoot.getParent();
                    if (commonRoot == null) {
                        break;
                    }
                }
            }
        }
        return commonRoot;
    }

    /**
     * Writes a JDOM Document to a file using the same format as the existing codebase.
     */
    private void writePomToFile(Document document, Path filePath) throws Exception {
        try (FileWriter writer = new FileWriter(filePath.toFile())) {
            XMLOutputter outputter = new XMLOutputter();
            outputter.output(document, writer);
        }
    }

    /**
     * Analyzes plugins using effective models built from the temp directory.
     * Returns a map of POM path to the set of plugin keys that need management.
     */
    private Map<Path, Set<String>> analyzePluginsUsingEffectiveModels(
            UpgradeContext context, Map<Path, Document> pomMap, Path tempDir) {
        Map<Path, Set<String>> result = 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
                Set<String> pluginsNeedingUpgrade =
                        analyzeEffectiveModelForPlugins(context, tempPomPath, pluginUpgrades);

                // Determine where to add plugin management (last local parent)
                Path targetPomForManagement =
                        findLastLocalParentForPluginManagement(context, tempPomPath, pomMap, tempDir, commonRoot);

                if (targetPomForManagement != null) {
                    result.computeIfAbsent(targetPomForManagement, k -> new HashSet<>())
                            .addAll(pluginsNeedingUpgrade);

                    if (!pluginsNeedingUpgrade.isEmpty()) {
                        context.debug("Will add plugin management to " + targetPomForManagement + " for plugins: "
                                + pluginsNeedingUpgrade);
                    }
                }

            } catch (Exception e) {
                context.debug("Failed to analyze effective model for " + originalPomPath + ": " + e.getMessage());
            }
        }

        return result;
    }

    /**
     * Converts PluginUpgradeInfo map to PluginUpgrade map for compatibility.
     */
    private Map<String, PluginUpgrade> getPluginUpgradesAsMap() {
        Map<String, PluginUpgrade> result = new HashMap<>();
        for (PluginUpgrade upgrade : PLUGIN_UPGRADES) {
            String key = upgrade.groupId() + ":" + upgrade.artifactId();
            result.put(key, upgrade);
        }
        return result;
    }

    /**
     * Analyzes the effective model for a single POM to find plugins that need upgrades.
     */
    private Set<String> analyzeEffectiveModelForPlugins(
            UpgradeContext context, Path tempPomPath, Map<String, PluginUpgrade> pluginUpgrades) {

        // Use the cached Maven 4 session
        Session session = getSession();
        ModelBuilder modelBuilder = session.getService(ModelBuilder.class);

        // Build effective model
        ModelBuilderRequest request = ModelBuilderRequest.builder()
                .session(session)
                .source(Sources.buildSource(tempPomPath))
                .requestType(ModelBuilderRequest.RequestType.BUILD_EFFECTIVE)
                .recursive(false) // We only want this POM, not its modules
                .build();

        ModelBuilderResult result = modelBuilder.newSession().build(request);
        Model effectiveModel = result.getEffectiveModel();

        // Analyze plugins from effective model
        return analyzePluginsFromEffectiveModel(context, effectiveModel, pluginUpgrades);
    }

    /**
     * Analyzes plugins from the effective model and determines which ones need upgrades.
     */
    private Set<String> analyzePluginsFromEffectiveModel(
            UpgradeContext context, Model effectiveModel, Map<String, PluginUpgrade> pluginUpgrades) {
        Set<String> pluginsNeedingUpgrade = new HashSet<>();

        Build build = effectiveModel.getBuild();
        if (build != null) {
            // 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())) {
                        pluginsNeedingUpgrade.add(pluginKey);
                        context.debug("Plugin " + pluginKey + " version " + effectiveVersion + " needs upgrade to "
                                + upgrade.minVersion());
                    }
                }
            }

            // Check build/pluginManagement/plugins - these provide version management
            PluginManagement pluginManagement = build.getPluginManagement();
            if (pluginManagement != null) {
                for (Plugin plugin : pluginManagement.getPlugins()) {
                    String pluginKey = getPluginKey(plugin);
                    PluginUpgrade upgrade = pluginUpgrades.get(pluginKey);
                    if (upgrade != null) {
                        String effectiveVersion = plugin.getVersion();
                        if (isVersionBelow(effectiveVersion, upgrade.minVersion())) {
                            pluginsNeedingUpgrade.add(pluginKey);
                            context.debug("Managed plugin " + pluginKey + " version " + effectiveVersion
                                    + " needs upgrade to " + upgrade.minVersion());
                        }
                    }
                }
            }
        }

        return pluginsNeedingUpgrade;
    }

    /**
     * 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) {

        // Build effective model to get parent information
        Session session = getSession();
        ModelBuilder modelBuilder = session.getService(ModelBuilder.class);

        ModelBuilderRequest request = ModelBuilderRequest.builder()
                .session(session)
                .source(Sources.buildSource(tempPomPath))
                .requestType(ModelBuilderRequest.RequestType.BUILD_EFFECTIVE)
                .recursive(false)
                .build();

        ModelBuilderResult result = modelBuilder.newSession().build(request);
        Model effectiveModel = result.getEffectiveModel();

        // 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;

                // Load the parent model to continue walking up
                Path parentTempPath = tempDir.resolve(commonRoot.relativize(parentPath));
                ModelBuilderRequest parentRequest = ModelBuilderRequest.builder()
                        .session(session)
                        .source(Sources.buildSource(parentTempPath))
                        .requestType(ModelBuilderRequest.RequestType.BUILD_EFFECTIVE)
                        .recursive(false)
                        .build();

                ModelBuilderResult parentResult = modelBuilder.newSession().build(parentRequest);
                currentModel = parentResult.getEffectiveModel();
            } 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.getRootElement();
            Namespace namespace = root.getNamespace();

            // Extract GAV from this POM
            String groupId = getChildText(root, GROUP_ID, namespace);
            String artifactId = getChildText(root, ARTIFACT_ID, namespace);
            String version = getChildText(root, VERSION, namespace);

            // Handle inheritance from parent
            Element parentElement = root.getChild(PARENT, namespace);
            if (parentElement != null) {
                if (groupId == null) {
                    groupId = getChildText(parentElement, GROUP_ID, namespace);
                }
                if (version == null) {
                    version = getChildText(parentElement, VERSION, namespace);
                }
            }

            // 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) {

        Map<String, PluginUpgrade> pluginUpgrades = getPluginUpgradesAsMap();
        boolean hasUpgrades = false;

        Element root = pomDocument.getRootElement();
        Namespace namespace = root.getNamespace();

        // Ensure build/pluginManagement/plugins structure exists
        Element buildElement = root.getChild(BUILD, namespace);
        if (buildElement == null) {
            buildElement = JDomUtils.insertNewElement(BUILD, root);
        }

        Element pluginManagementElement = buildElement.getChild(PLUGIN_MANAGEMENT, namespace);
        if (pluginManagementElement == null) {
            pluginManagementElement = JDomUtils.insertNewElement(PLUGIN_MANAGEMENT, buildElement);
        }

        Element managedPluginsElement = pluginManagementElement.getChild(PLUGINS, namespace);
        if (managedPluginsElement == null) {
            managedPluginsElement = JDomUtils.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, namespace, upgrade)) {
                    addPluginManagementEntryFromUpgrade(managedPluginsElement, upgrade, context);
                    hasUpgrades = true;
                }
            }
        }

        return hasUpgrades;
    }

    /**
     * Checks if a plugin is already managed in the given plugins element.
     */
    private boolean isPluginAlreadyManagedInElement(
            Element pluginsElement, Namespace namespace, PluginUpgrade upgrade) {
        List<Element> pluginElements = pluginsElement.getChildren(PLUGIN, namespace);
        for (Element pluginElement : pluginElements) {
            String groupId = getChildText(pluginElement, GROUP_ID, namespace);
            String artifactId = getChildText(pluginElement, ARTIFACT_ID, namespace);

            // 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) {
        // Create plugin element using JDomUtils for proper formatting
        Element pluginElement = JDomUtils.insertNewElement(PLUGIN, managedPluginsElement);

        // Add child elements using JDomUtils for proper formatting
        JDomUtils.insertContentElement(pluginElement, GROUP_ID, upgrade.groupId());
        JDomUtils.insertContentElement(pluginElement, ARTIFACT_ID, upgrade.artifactId());
        JDomUtils.insertContentElement(pluginElement, VERSION, upgrade.minVersion());

        context.detail("Added plugin management for " + upgrade.groupId() + ":" + upgrade.artifactId() + " version "
                + upgrade.minVersion() + " (found through effective model analysis)");
    }

    /**
     * Cleans up the temporary directory.
     */
    private void cleanupTempDirectory(Path tempDir) {
        try {
            Files.walk(tempDir)
                    .sorted(Comparator.reverseOrder())
                    .map(Path::toFile)
                    .forEach(File::delete);
        } catch (Exception e) {
            // Best effort cleanup - don't fail the whole operation
        }
    }

    /**
     * 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;
        }
    }
}