CompatibilityFixStrategy.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.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import eu.maveniverse.domtrip.Comment;
import eu.maveniverse.domtrip.Document;
import eu.maveniverse.domtrip.Editor;
import eu.maveniverse.domtrip.Element;
import eu.maveniverse.domtrip.maven.Coordinates;
import eu.maveniverse.domtrip.maven.MavenPomElements;
import org.apache.maven.api.cli.mvnup.UpgradeOptions;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Priority;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;

import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_APPEND;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_CHILDREN;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_MERGE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_OVERRIDE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_SELF;
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.MODULE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.MODULES;
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.PLUGIN_REPOSITORIES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_REPOSITORY;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROPERTIES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.RELATIVE_PATH;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.REPOSITORIES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.REPOSITORY;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.SUBPROJECT;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.SUBPROJECTS;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.VERSION;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Files.DEFAULT_PARENT_RELATIVE_PATH;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Plugins.DEFAULT_MAVEN_PLUGIN_GROUP_ID;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Plugins.MAVEN_PLUGIN_PREFIX;

/**
 * Strategy for applying Maven 4 compatibility fixes to POM files.
 * Fixes issues that prevent POMs from being processed by Maven 4.
 */
@Named
@Singleton
@Priority(20)
public class CompatibilityFixStrategy extends AbstractUpgradeStrategy {

    private static final Pattern EXPRESSION_PATTERN = Pattern.compile("\\$\\{([^}]+)}");

    private static final Set<String> VALID_COMBINE_SELF_VALUES = Set.of(COMBINE_OVERRIDE, COMBINE_MERGE, "remove");

    private static final Set<String> VALID_COMBINE_CHILDREN_VALUES = Set.of(COMBINE_APPEND, COMBINE_MERGE);

    /**
     * Known incompatible plugins where even the latest version fails with Maven 4.
     * Maps plugin key (groupId:artifactId) to a description of the incompatibility.
     * <p>
     * Note: plugins that have a fixed version available should be added to
     * {@link PluginUpgradeStrategy} instead, so mvnup can auto-upgrade them.
     */
    private static final Map<String, String> KNOWN_INCOMPATIBLE_PLUGINS = Map.of();

    @Override
    public boolean isApplicable(UpgradeContext context) {
        UpgradeOptions options = getOptions(context);

        // Handle --all option (overrides individual options)
        boolean useAll = options.all().orElse(false);
        if (useAll) {
            return true;
        }

        // Apply default behavior: if no specific options are provided, enable --model
        // OR if all options are explicitly disabled, still apply default behavior
        boolean noOptionsSpecified = options.all().isEmpty()
                && options.infer().isEmpty()
                && options.model().isEmpty()
                && options.plugins().isEmpty()
                && options.modelVersion().isEmpty();

        boolean allOptionsDisabled = options.all().map(v -> !v).orElse(false)
                && options.infer().map(v -> !v).orElse(false)
                && options.model().map(v -> !v).orElse(false)
                && options.plugins().map(v -> !v).orElse(false)
                && options.modelVersion().isEmpty();

        if (noOptionsSpecified || allOptionsDisabled) {
            return true;
        }

        // Check if --model is explicitly set (and not part of "all disabled" scenario)
        if (options.model().isPresent()) {
            return options.model().get();
        }

        return false;
    }

    @Override
    public String getDescription() {
        return "Applying Maven 4 compatibility fixes";
    }

    @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<>();

        Set<String> allDefinedProperties = collectAllDefinedProperties(pomMap);
        allDefinedProperties.addAll(collectEffectiveProperties(context, pomMap));

        for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
            Path pomPath = entry.getKey();
            Document pomDocument = entry.getValue();
            processedPoms.add(pomPath);

            context.info(pomPath + " (checking for Maven 4 compatibility issues)");
            context.indent();

            try {
                boolean hasIssues = false;

                hasIssues |= fixUnsupportedCombineChildrenAttributes(pomDocument, context);
                hasIssues |= fixUnsupportedCombineSelfAttributes(pomDocument, context);
                hasIssues |= fixUnsupportedRepositoryExpressions(pomDocument, context);
                hasIssues |= fixDeprecatedPropertyExpressions(pomDocument, context);
                hasIssues |= fixIncorrectParentRelativePaths(pomDocument, pomPath, pomMap, context);
                hasIssues |= fixUndefinedPropertyExpressions(pomDocument, allDefinedProperties, context);
                hasIssues |= fixUndefinedPropertyExpressionsInRepositories(pomDocument, allDefinedProperties, context);

                // Warning-only checks: emit warnings for issues that cannot be auto-fixed
                // These do not modify the POM and do not affect hasIssues
                warnAboutIncompatiblePlugins(pomDocument, context);
                warnAboutPropertyInterpolatedModulePaths(pomDocument, context);
                warnAboutCiFriendlyMissingDependencyVersions(pomDocument, context);

                if (hasIssues) {
                    context.success("Maven 4 compatibility issues fixed");
                    modifiedPoms.add(pomPath);
                } else {
                    context.success("No Maven 4 compatibility issues found");
                }
            } catch (Exception e) {
                context.failure("Failed to fix Maven 4 compatibility issues" + ": " + e.getMessage());
                errorPoms.add(pomPath);
            } finally {
                context.unindent();
            }
        }

        return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
    }

    /**
     * Fixes unsupported combine.children attribute values.
     * Maven 4 only supports 'append' and 'merge' (default is merge).
     * Invalid values are removed entirely since Maven 3 silently ignored them.
     */
    private boolean fixUnsupportedCombineChildrenAttributes(Document pomDocument, UpgradeContext context) {
        Element root = pomDocument.root();

        List<Element> invalidElements = findElementsWithInvalidAttribute(
                        root, COMBINE_CHILDREN, VALID_COMBINE_CHILDREN_VALUES)
                .toList();

        for (Element element : invalidElements) {
            String invalidValue = element.attribute(COMBINE_CHILDREN);
            element.removeAttribute(COMBINE_CHILDREN);
            context.detail(
                    "Fixed: removed invalid " + COMBINE_CHILDREN + "='" + invalidValue + "' from " + element.name());
        }

        return !invalidElements.isEmpty();
    }

    /**
     * Fixes unsupported combine.self attribute values.
     * Maven 4 only supports 'override', 'merge', and 'remove' (default is merge).
     * Invalid values are removed entirely since Maven 3 silently ignored them.
     */
    private boolean fixUnsupportedCombineSelfAttributes(Document pomDocument, UpgradeContext context) {
        Element root = pomDocument.root();

        List<Element> invalidElements = findElementsWithInvalidAttribute(root, COMBINE_SELF, VALID_COMBINE_SELF_VALUES)
                .toList();

        for (Element element : invalidElements) {
            String invalidValue = element.attribute(COMBINE_SELF);
            element.removeAttribute(COMBINE_SELF);
            context.detail("Fixed: removed invalid " + COMBINE_SELF + "='" + invalidValue + "' from " + element.name());
        }

        return !invalidElements.isEmpty();
    }

    /**
     * Fixes unsupported repository URL expressions.
     */
    private boolean fixUnsupportedRepositoryExpressions(Document pomDocument, UpgradeContext context) {
        Element root = pomDocument.root();

        // Collect all repository containers to process
        Stream<Element> repositoryContainers = Stream.concat(
                // Root level repositories
                Stream.of(
                                root.childElement(REPOSITORIES).orElse(null),
                                root.childElement(PLUGIN_REPOSITORIES).orElse(null))
                        .filter(Objects::nonNull),
                // Profile repositories
                root.childElement(PROFILES).stream()
                        .flatMap(profiles -> profiles.childElements(PROFILE))
                        .flatMap(profile -> Stream.of(
                                        profile.childElement(REPOSITORIES).orElse(null),
                                        profile.childElement(PLUGIN_REPOSITORIES)
                                                .orElse(null))
                                .filter(Objects::nonNull)));

        return repositoryContainers
                .map(container -> fixRepositoryExpressions(container, pomDocument, context))
                .reduce(false, Boolean::logicalOr);
    }

    /**
     * Fixes incorrect parent relative paths.
     */
    private boolean fixIncorrectParentRelativePaths(
            Document pomDocument, Path pomPath, Map<Path, Document> pomMap, UpgradeContext context) {
        Element root = pomDocument.root();

        Element parentElement = root.childElement(PARENT).orElse(null);
        if (parentElement == null) {
            return false; // No parent to fix
        }

        Element relativePathElement = parentElement.childElement(RELATIVE_PATH).orElse(null);
        String currentRelativePath =
                relativePathElement != null ? relativePathElement.textContent().trim() : DEFAULT_PARENT_RELATIVE_PATH;

        // Try to find the correct parent POM
        String parentGroupId = parentElement.childText(MavenPomElements.Elements.GROUP_ID);
        String parentArtifactId = parentElement.childText(MavenPomElements.Elements.ARTIFACT_ID);
        String parentVersion = parentElement.childText(MavenPomElements.Elements.VERSION);

        Path correctParentPath = findParentPomInMap(context, parentGroupId, parentArtifactId, parentVersion, pomMap);
        if (correctParentPath != null) {
            try {
                Path correctRelativePath = pomPath.getParent().relativize(correctParentPath);
                String correctRelativePathStr = correctRelativePath.toString().replace('\\', '/');

                if (!correctRelativePathStr.equals(currentRelativePath)) {
                    // Update or create relativePath element using DomUtils convenience method
                    DomUtils.updateOrCreateChildElement(parentElement, RELATIVE_PATH, correctRelativePathStr);
                    context.detail("Fixed: " + "relativePath corrected from '" + currentRelativePath + "' to '"
                            + correctRelativePathStr + "'");
                    return true;
                }
            } catch (Exception e) {
                context.failure("Failed to compute correct relativePath" + ": " + e.getMessage());
            }
        }

        return false;
    }

    private Set<String> collectAllDefinedProperties(Map<Path, Document> pomMap) {
        Set<String> properties = new HashSet<>();
        for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
            collectPropertiesFromDom(entry.getValue(), properties);
        }
        return properties;
    }

    private void collectPropertiesFromDom(Document document, Set<String> properties) {
        Element root = document.root();

        root.childElement(PROPERTIES)
                .ifPresent(propsElement -> propsElement.childElements().forEach(child -> properties.add(child.name())));

        root.childElement(PROFILES)
                .ifPresent(profiles -> profiles.childElements(PROFILE)
                        .forEach(profile -> profile.childElement(PROPERTIES)
                                .ifPresent(propsElement ->
                                        propsElement.childElements().forEach(child -> properties.add(child.name())))));
    }

    private Set<String> collectEffectiveProperties(UpgradeContext context, Map<Path, Document> pomMap) {
        Set<String> properties = new HashSet<>();
        for (Path pomPath : pomMap.keySet()) {
            try {
                org.apache.maven.api.model.Model effectiveModel = buildEffectiveModel(pomPath);
                properties.addAll(effectiveModel.getProperties().keySet());
            } catch (Exception e) {
                context.debug("Failed to build effective model for " + pomPath + ": " + e.getMessage());
            }
        }
        return properties;
    }

    private static class DependencyContainer {
        final Element element;
        final String sectionName;

        DependencyContainer(Element element, String sectionName) {
            this.element = element;
            this.sectionName = sectionName;
        }
    }

    /**
     * Fixes dependencies with undefined property expressions by commenting them out.
     */
    private boolean fixUndefinedPropertyExpressions(
            Document pomDocument, Set<String> allDefinedProperties, UpgradeContext context) {
        Element root = pomDocument.root();

        Stream<DependencyContainer> dependencyContainers = Stream.concat(
                Stream.of(
                                new DependencyContainer(
                                        root.childElement(DEPENDENCIES).orElse(null), DEPENDENCIES),
                                new DependencyContainer(
                                        root.childElement(DEPENDENCY_MANAGEMENT)
                                                .flatMap(dm -> dm.childElement(DEPENDENCIES))
                                                .orElse(null),
                                        DEPENDENCY_MANAGEMENT))
                        .filter(container -> container.element != null),
                root.childElement(PROFILES).stream()
                        .flatMap(profiles -> profiles.childElements(PROFILE))
                        .flatMap(profile -> Stream.of(
                                        new DependencyContainer(
                                                profile.childElement(DEPENDENCIES)
                                                        .orElse(null),
                                                "profile dependencies"),
                                        new DependencyContainer(
                                                profile.childElement(DEPENDENCY_MANAGEMENT)
                                                        .flatMap(dm -> dm.childElement(DEPENDENCIES))
                                                        .orElse(null),
                                                "profile dependencyManagement"))
                                .filter(container -> container.element != null)));

        return dependencyContainers
                .map(container -> fixUndefinedPropertyExpressionsInSection(
                        container.element, allDefinedProperties, pomDocument, context, container.sectionName))
                .reduce(false, Boolean::logicalOr);
    }

    /**
     * Fixes repositories with undefined property expressions by commenting them out.
     */
    private boolean fixUndefinedPropertyExpressionsInRepositories(
            Document pomDocument, Set<String> allDefinedProperties, UpgradeContext context) {
        Element root = pomDocument.root();

        Stream<RepositoryContainer> repositoryContainers = Stream.concat(
                Stream.of(
                                new RepositoryContainer(
                                        root.childElement(REPOSITORIES).orElse(null), REPOSITORY, REPOSITORIES),
                                new RepositoryContainer(
                                        root.childElement(PLUGIN_REPOSITORIES).orElse(null),
                                        PLUGIN_REPOSITORY,
                                        PLUGIN_REPOSITORIES))
                        .filter(c -> c.element != null),
                root.childElement(PROFILES).stream()
                        .flatMap(profiles -> profiles.childElements(PROFILE))
                        .flatMap(profile -> Stream.of(
                                        new RepositoryContainer(
                                                profile.childElement(REPOSITORIES)
                                                        .orElse(null),
                                                REPOSITORY,
                                                "profile repositories"),
                                        new RepositoryContainer(
                                                profile.childElement(PLUGIN_REPOSITORIES)
                                                        .orElse(null),
                                                PLUGIN_REPOSITORY,
                                                "profile pluginRepositories"))
                                .filter(c -> c.element != null)));

        return repositoryContainers
                .map(c -> fixUndefinedPropertyExpressionsInRepositorySection(
                        c.element, c.elementType, allDefinedProperties, pomDocument, context, c.sectionName))
                .reduce(false, Boolean::logicalOr);
    }

    private record RepositoryContainer(Element element, String elementType, String sectionName) {}

    private boolean fixUndefinedPropertyExpressionsInRepositorySection(
            Element repositoriesElement,
            String elementType,
            Set<String> allDefinedProperties,
            Document pomDocument,
            UpgradeContext context,
            String sectionName) {
        boolean fixed = false;
        List<Element> repositories =
                repositoriesElement.childElements(elementType).toList();
        Editor editor = new Editor(pomDocument);

        for (Element repository : repositories) {
            Set<String> undefinedProps = findUndefinedPropertiesInRepository(repository, allDefinedProperties);
            if (!undefinedProps.isEmpty()) {
                String propLabel = undefinedProps.size() > 1 ? "properties" : "property";
                String propsStr = "'" + String.join("', '", undefinedProps) + "'";

                Comment comment = editor.commentOutElement(repository);
                String elementXml = comment.content().trim();
                comment.content(
                        " mvnup: commented out - undefined " + propLabel + " " + propsStr + "\n" + elementXml + " ");

                context.detail("Fixed: Commented out " + elementType + " with undefined " + propLabel + " " + propsStr
                        + " in " + sectionName);
                fixed = true;
            }
        }

        return fixed;
    }

    private Set<String> findUndefinedPropertiesInRepository(Element repository, Set<String> allDefinedProperties) {
        Set<String> undefinedProperties = new HashSet<>();

        String id = repository.childText("id");
        String url = repository.childText("url");

        collectUndefinedExpressions(id, allDefinedProperties, undefinedProperties);
        collectUndefinedExpressions(url, allDefinedProperties, undefinedProperties);

        return undefinedProperties;
    }

    /**
     * Fixes undefined property expressions in a specific dependencies section.
     */
    private boolean fixUndefinedPropertyExpressionsInSection(
            Element dependenciesElement,
            Set<String> allDefinedProperties,
            Document pomDocument,
            UpgradeContext context,
            String sectionName) {
        boolean fixed = false;
        List<Element> dependencies =
                dependenciesElement.childElements(DEPENDENCY).toList();
        Editor editor = new Editor(pomDocument);

        for (Element dependency : dependencies) {
            Set<String> undefinedProps = findUndefinedProperties(dependency, allDefinedProperties);
            if (!undefinedProps.isEmpty()) {
                String propLabel = undefinedProps.size() > 1 ? "properties" : "property";
                String propsStr = "'" + String.join("', '", undefinedProps) + "'";

                Comment comment = editor.commentOutElement(dependency);
                String elementXml = comment.content().trim();
                comment.content(
                        " mvnup: commented out - undefined " + propLabel + " " + propsStr + "\n" + elementXml + " ");

                context.detail("Fixed: Commented out dependency with undefined " + propLabel + " " + propsStr + " in "
                        + sectionName);
                fixed = true;
            }
        }

        return fixed;
    }

    /**
     * Finds undefined property expressions in a dependency's coordinate fields.
     */
    private Set<String> findUndefinedProperties(Element dependency, Set<String> allDefinedProperties) {
        Set<String> undefinedProperties = new HashSet<>();

        String groupId = dependency.childText(MavenPomElements.Elements.GROUP_ID);
        String artifactId = dependency.childText(MavenPomElements.Elements.ARTIFACT_ID);
        String version = dependency.childText(MavenPomElements.Elements.VERSION);

        collectUndefinedExpressions(groupId, allDefinedProperties, undefinedProperties);
        collectUndefinedExpressions(artifactId, allDefinedProperties, undefinedProperties);
        collectUndefinedExpressions(version, allDefinedProperties, undefinedProperties);

        return undefinedProperties;
    }

    private void collectUndefinedExpressions(String value, Set<String> allDefinedProperties, Set<String> result) {
        if (value == null) {
            return;
        }
        Matcher matcher = EXPRESSION_PATTERN.matcher(value);
        while (matcher.find()) {
            String propertyName = matcher.group(1);
            if (!isWellKnownProperty(propertyName) && !allDefinedProperties.contains(propertyName)) {
                result.add(propertyName);
            }
        }
    }

    private static boolean isWellKnownProperty(String propertyName) {
        if (propertyName.startsWith("project.")
                || propertyName.startsWith("pom.")
                || propertyName.startsWith("env.")
                || propertyName.startsWith("settings.")
                || propertyName.startsWith("maven.")) {
            return true;
        }
        if (propertyName.startsWith("java.")
                || propertyName.startsWith("os.")
                || propertyName.startsWith("user.")
                || propertyName.startsWith("file.")
                || propertyName.startsWith("line.")
                || propertyName.startsWith("path.")
                || propertyName.startsWith("sun.")) {
            return true;
        }
        return "basedir".equals(propertyName)
                || "revision".equals(propertyName)
                || "sha1".equals(propertyName)
                || "changelist".equals(propertyName);
    }

    /**
     * Recursively finds all elements with a specific attribute value.
     */
    private Stream<Element> findElementsWithAttribute(Element element, String attributeName, String attributeValue) {
        return Stream.concat(
                // Check current element
                Stream.of(element).filter(e -> {
                    String attr = e.attribute(attributeName);
                    return attr != null && attributeValue.equals(attr);
                }),
                // Recursively check children
                element.childElements()
                        .flatMap(child -> findElementsWithAttribute(child, attributeName, attributeValue)));
    }

    /**
     * Recursively finds all elements with an attribute whose value is not in the set of valid values.
     */
    private Stream<Element> findElementsWithInvalidAttribute(
            Element element, String attributeName, Set<String> validValues) {
        return Stream.concat(
                Stream.of(element).filter(e -> {
                    String attr = e.attribute(attributeName);
                    return attr != null && !validValues.contains(attr);
                }),
                element.childElements()
                        .flatMap(child -> findElementsWithInvalidAttribute(child, attributeName, validValues)));
    }

    private boolean fixRepositoryExpressions(
            Element repositoriesElement, Document pomDocument, UpgradeContext context) {
        if (repositoriesElement == null) {
            return false;
        }

        boolean fixed = false;
        String elementType = repositoriesElement.name().equals(REPOSITORIES) ? REPOSITORY : PLUGIN_REPOSITORY;
        List<Element> repositories =
                repositoriesElement.childElements(elementType).toList();

        for (Element repository : repositories) {
            Element urlElement = repository.childElement("url").orElse(null);
            if (urlElement != null) {
                String url = urlElement.textContent().trim();
                String fixedUrl =
                        url.replace("${basedir}", "${project.basedir}").replace("${pom.basedir}", "${project.basedir}");
                if (!fixedUrl.equals(url)) {
                    urlElement.textContent(fixedUrl);
                    String repositoryId = repository.childText("id");
                    context.detail("Fixed: replaced deprecated expression in " + elementType + " URL (id: "
                            + repositoryId + "): " + url + " ��� " + fixedUrl);
                    fixed = true;
                }
            }
        }

        return fixed;
    }

    /**
     * Fixes deprecated Maven 2/3 shorthand property expressions throughout the POM.
     * Replaces ${version}, ${groupId}, and ${artifactId} with their ${project.*} equivalents,
     * which are the only forms supported in Maven 4.
     */
    private boolean fixDeprecatedPropertyExpressions(Document pomDocument, UpgradeContext context) {
        return fixDeprecatedPropertyExpressionsInElement(pomDocument.root(), context);
    }

    private boolean fixDeprecatedPropertyExpressionsInElement(Element element, UpgradeContext context) {
        boolean fixed = false;

        List<Element> children = element.childElements().toList();

        if (children.isEmpty()) {
            String text = element.textContent();
            if (text != null && !text.isEmpty()) {
                String fixedText = replaceDeprecatedExpressions(text);
                if (!fixedText.equals(text)) {
                    element.textContent(fixedText);
                    context.detail("Fixed: replaced deprecated property expression in <" + element.name() + ">: "
                            + text.trim() + " ��� " + fixedText.trim());
                    fixed = true;
                }
            }
        } else {
            for (Element child : children) {
                fixed |= fixDeprecatedPropertyExpressionsInElement(child, context);
            }
        }

        return fixed;
    }

    private static String replaceDeprecatedExpressions(String text) {
        return text.replace("${version}", "${project.version}")
                .replace("${groupId}", "${project.groupId}")
                .replace("${artifactId}", "${project.artifactId}");
    }

    private Path findParentPomInMap(
            UpgradeContext context, String groupId, String artifactId, String version, Map<Path, Document> pomMap) {
        return pomMap.entrySet().stream()
                .filter(entry -> {
                    Coordinates gav = AbstractUpgradeStrategy.extractArtifactCoordinatesWithParentResolution(
                            context, entry.getValue());
                    return gav != null
                            && Objects.equals(gav.groupId(), groupId)
                            && Objects.equals(gav.artifactId(), artifactId)
                            && (version == null || Objects.equals(gav.version(), version));
                })
                .findFirst()
                .map(Map.Entry::getKey)
                .orElse(null);
    }

    // ---- Warning-only checks for Maven 4 compatibility issues that cannot be auto-fixed ----

    /**
     * Warns about plugins known to be incompatible with Maven 4 even at their latest version.
     * These plugins call methods on immutable API objects or use removed internal APIs
     * and require upstream fixes before they can work with Maven 4.
     *
     * @see <a href="https://github.com/apache/maven/issues/12432">#12432</a>
     */
    private void warnAboutIncompatiblePlugins(Document pomDocument, UpgradeContext context) {
        Element root = pomDocument.root();

        Stream<Element> pluginContainers = Stream.concat(
                // Root level build
                root.childElement(BUILD).stream()
                        .flatMap(build -> Stream.concat(
                                build.childElement(PLUGINS).stream(),
                                build.childElement(PLUGIN_MANAGEMENT).stream()
                                        .flatMap(pm -> pm.childElement(PLUGINS).stream()))),
                // Profile builds
                root.childElement(PROFILES).stream()
                        .flatMap(profiles -> profiles.childElements(PROFILE))
                        .flatMap(profile -> profile.childElement(BUILD).stream())
                        .flatMap(build -> Stream.concat(
                                build.childElement(PLUGINS).stream(),
                                build.childElement(PLUGIN_MANAGEMENT).stream()
                                        .flatMap(pm -> pm.childElement(PLUGINS).stream()))));

        pluginContainers.forEach(pluginsElement -> pluginsElement
                .childElements(PLUGIN)
                .forEach(pluginElement -> {
                    String groupId = pluginElement.childText(MavenPomElements.Elements.GROUP_ID);
                    String artifactId = pluginElement.childText(MavenPomElements.Elements.ARTIFACT_ID);

                    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;
                        String warning = KNOWN_INCOMPATIBLE_PLUGINS.get(pluginKey);
                        if (warning != null) {
                            context.warning("Known Maven 4 incompatibility: " + pluginKey + " ��� " + warning);
                        }
                    }
                }));
    }

    /**
     * Warns about {@code <module>} (or {@code <subproject>}) elements that contain property
     * expressions ({@code ${...}}). Maven 4 validates module paths during POM parsing,
     * before profiles can set property values, so these paths are rejected as non-existent.
     *
     * @see <a href="https://github.com/apache/maven/issues/12434">#12434</a>
     */
    private void warnAboutPropertyInterpolatedModulePaths(Document pomDocument, UpgradeContext context) {
        Element root = pomDocument.root();

        // Check root-level modules/subprojects
        Stream.concat(
                        root.childElement(MODULES).stream().flatMap(m -> m.childElements(MODULE)),
                        root.childElement(SUBPROJECTS).stream().flatMap(s -> s.childElements(SUBPROJECT)))
                .forEach(moduleElement -> {
                    String path = moduleElement.textContentTrimmed();
                    if (path != null && EXPRESSION_PATTERN.matcher(path).find()) {
                        context.warning("Module path '" + path
                                + "' contains a property expression. Maven 4 validates module paths"
                                + " before property interpolation, which will cause a build failure"
                                + " if the expression cannot be resolved at parse time.");
                    }
                });

        // Check profile-level modules/subprojects
        root.childElement(PROFILES)
                .ifPresent(profiles -> profiles.childElements(PROFILE).forEach(profile -> {
                    Stream.concat(
                                    profile.childElement(MODULES).stream().flatMap(m -> m.childElements(MODULE)),
                                    profile.childElement(SUBPROJECTS).stream()
                                            .flatMap(s -> s.childElements(SUBPROJECT)))
                            .forEach(moduleElement -> {
                                String path = moduleElement.textContentTrimmed();
                                if (path != null
                                        && EXPRESSION_PATTERN.matcher(path).find()) {
                                    context.warning("Profile module path '" + path
                                            + "' contains a property expression. Maven 4 validates module"
                                            + " paths before profile-driven property interpolation, which"
                                            + " will cause a build failure when no profile is active.");
                                }
                            });
                }));
    }

    /**
     * Warns about CI-friendly projects using {@code ${revision}} (or {@code ${sha1}},
     * {@code ${changelist}}) where child module dependencies lack explicit {@code <version>}
     * elements and rely on {@code dependencyManagement} inherited from the parent.
     * Maven 4 validates dependency completeness before fully resolving the parent's
     * {@code dependencyManagement} chain when the parent's own version is a CI-friendly
     * expression.
     *
     * @see <a href="https://github.com/apache/maven/issues/12435">#12435</a>
     */
    private void warnAboutCiFriendlyMissingDependencyVersions(Document pomDocument, UpgradeContext context) {
        Element root = pomDocument.root();

        // Only check if this POM has a parent with a CI-friendly version
        Element parentElement = root.childElement(PARENT).orElse(null);
        if (parentElement == null) {
            return;
        }
        String parentVersion = parentElement.childText(VERSION);
        if (parentVersion == null || !isCiFriendlyExpression(parentVersion)) {
            return;
        }

        // Check for dependencies without explicit versions
        List<String> versionlessDeps = root.childElement(DEPENDENCIES).stream()
                .flatMap(deps -> deps.childElements(DEPENDENCY))
                .filter(dep -> dep.childElement(VERSION).isEmpty())
                .map(dep -> {
                    String gid = dep.childText(MavenPomElements.Elements.GROUP_ID);
                    String aid = dep.childText(MavenPomElements.Elements.ARTIFACT_ID);
                    return (gid != null ? gid : "?") + ":" + (aid != null ? aid : "?");
                })
                .toList();

        if (!versionlessDeps.isEmpty()) {
            context.warning("This module uses CI-friendly version '" + parentVersion
                    + "' in its parent and has " + versionlessDeps.size()
                    + " dependencies without explicit <version> elements (e.g., "
                    + versionlessDeps.get(0)
                    + "). Maven 4 may fail with 'dependencies.dependency.version is missing'"
                    + " because dependency management inheritance is validated before the"
                    + " CI-friendly parent version is fully resolved.");
        }
    }

    /**
     * Checks if a version string is a CI-friendly expression.
     */
    private static boolean isCiFriendlyExpression(String version) {
        return version.contains("${revision}") || version.contains("${sha1}") || version.contains("${changelist}");
    }
}