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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

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 org.jdom2.Attribute;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Namespace;

import static org.apache.maven.cling.invoker.mvnup.goals.ModelVersionUtils.getSchemaLocationForModelVersion;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.ModelVersions.MODEL_VERSION_4_0_0;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.ModelVersions.MODEL_VERSION_4_1_0;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.ModelVersions.MODEL_VERSION_4_2_0;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Namespaces.MAVEN_4_0_0_NAMESPACE;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Namespaces.MAVEN_4_1_0_NAMESPACE;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Namespaces.MAVEN_4_2_0_NAMESPACE;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.SCHEMA_LOCATION;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.XSI_NAMESPACE_PREFIX;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.XSI_NAMESPACE_URI;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODULE;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODULES;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILE;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILES;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SUBPROJECT;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SUBPROJECTS;

/**
 * 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 {
                        performModelUpgrade(pomDocument, context, currentVersion, targetModelVersion);
                    } 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.
     */
    private void performModelUpgrade(
            Document pomDocument, UpgradeContext context, String currentVersion, String targetModelVersion) {
        // Update namespace and schema location to target version
        upgradeNamespaceAndSchemaLocation(pomDocument, context, targetModelVersion);

        // Convert modules to subprojects (for 4.1.0 and higher)
        if (ModelVersionUtils.isVersionGreaterOrEqual(targetModelVersion, MODEL_VERSION_4_1_0)) {
            convertModulesToSubprojects(pomDocument, context);
        }

        // Update modelVersion to target version (perhaps removed later during inference step)
        ModelVersionUtils.updateModelVersion(pomDocument, targetModelVersion);
        context.detail("Updated modelVersion to " + targetModelVersion);
    }

    /**
     * Updates namespace and schema location for the target model version.
     */
    private void upgradeNamespaceAndSchemaLocation(
            Document pomDocument, UpgradeContext context, String targetModelVersion) {
        Element root = pomDocument.getRootElement();

        // Update namespace based on target model version
        String targetNamespace = getNamespaceForModelVersion(targetModelVersion);
        Namespace newNamespace = Namespace.getNamespace(targetNamespace);
        updateElementNamespace(root, newNamespace);
        context.detail("Updated namespace to " + targetNamespace);

        // Update schema location
        Attribute schemaLocationAttr =
                root.getAttribute(SCHEMA_LOCATION, Namespace.getNamespace(XSI_NAMESPACE_PREFIX, XSI_NAMESPACE_URI));
        if (schemaLocationAttr != null) {
            schemaLocationAttr.setValue(getSchemaLocationForModelVersion(targetModelVersion));
            context.detail("Updated xsi:schemaLocation");
        }
    }

    /**
     * Recursively updates the namespace of an element and all its children.
     */
    private void updateElementNamespace(Element element, Namespace newNamespace) {
        element.setNamespace(newNamespace);
        for (Element child : element.getChildren()) {
            updateElementNamespace(child, newNamespace);
        }
    }

    /**
     * Converts modules to subprojects for 4.1.0 compatibility.
     */
    private void convertModulesToSubprojects(Document pomDocument, UpgradeContext context) {
        Element root = pomDocument.getRootElement();
        Namespace namespace = root.getNamespace();

        // Convert modules element to subprojects
        Element modulesElement = root.getChild(MODULES, namespace);
        if (modulesElement != null) {
            modulesElement.setName(SUBPROJECTS);
            context.detail("Converted <modules> to <subprojects>");

            // Convert all module children to subproject
            List<Element> moduleElements = modulesElement.getChildren(MODULE, namespace);
            for (Element moduleElement : moduleElements) {
                moduleElement.setName(SUBPROJECT);
            }

            if (!moduleElements.isEmpty()) {
                context.detail("Converted " + moduleElements.size() + " <module> elements to <subproject>");
            }
        }

        // Also check inside profiles
        Element profilesElement = root.getChild(PROFILES, namespace);
        if (profilesElement != null) {
            List<Element> profileElements = profilesElement.getChildren(PROFILE, namespace);
            for (Element profileElement : profileElements) {
                Element profileModulesElement = profileElement.getChild(MODULES, namespace);
                if (profileModulesElement != null) {
                    profileModulesElement.setName(SUBPROJECTS);

                    List<Element> profileModuleElements = profileModulesElement.getChildren(MODULE, namespace);
                    for (Element moduleElement : profileModuleElements) {
                        moduleElement.setName(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 (MODEL_VERSION_4_2_0.equals(modelVersion)) {
            return 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;
        }
    }
}