ExtensionsXmlReconciler.java

package network.ike.plugin.ws.reconcile;

import network.ike.plugin.ws.bootstrap.WorkspaceBootstrap;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Properties;

/**
 * Keeps the managed {@code ike-workspace-extension} entry in
 * {@code .mvn/extensions.xml} in lockstep with the
 * {@code ike-workspace-extension.version} property declared in
 * {@code ike-parent} (IKE-Network/ike-issues#460).
 *
 * <p>Maven 4 does not interpolate POM properties inside
 * {@code .mvn/extensions.xml} at extension-load time — the version
 * must be a literal string. So the literal is rewritten in place by
 * this reconciler whenever {@code ws:scaffold-publish} runs.
 *
 * <p>On a workspace that predates the managed-block convention (the
 * file is missing the sentinel markers and the extension entry),
 * the reconciler migrates it: it inserts the managed block before the
 * closing {@code </extensions>} tag, preserving any other entries
 * (e.g. {@code wagon-ssh-external}).
 *
 * <p>The extension version is read from {@code ws-plugin.properties}
 * (filtered at build time by Maven from the
 * {@code ike-workspace-extension.version} property in ike-parent).
 */
public class ExtensionsXmlReconciler implements Reconciler {

    private static final String EXTENSIONS_XML = ".mvn/extensions.xml";
    private static final String PROPERTIES_RESOURCE =
            "/network/ike/plugin/ws/ws-plugin.properties";

    @Override
    public String dimension() {
        return ".mvn/extensions.xml ike-workspace-extension version";
    }

    @Override
    public String optOutFlag() {
        return "updateExtensions";
    }

    @Override
    public DriftReport detect(WorkspaceContext ctx) {
        Path xml = ctx.workspaceRoot().toPath().resolve(EXTENSIONS_XML);
        if (!Files.exists(xml)) {
            return DriftReport.noDrift(dimension());
        }
        String target = resolveExtensionVersion();
        try {
            String existing = Files.readString(xml);
            if (matchesTarget(existing, target)) {
                return DriftReport.noDrift(dimension());
            }
            return new DriftReport(
                    dimension(),
                    true,
                    "ike-workspace-extension entry not at " + target,
                    List.of(EXTENSIONS_XML
                            + ": rewrite managed block to <version>"
                            + target + "</version>"),
                    "rewrite the managed block in " + EXTENSIONS_XML
                            + " to ike-workspace-extension:" + target,
                    "-D" + optOutFlag() + "=false");
        } catch (IOException e) {
            return DriftReport.noDrift(dimension());
        }
    }

    @Override
    public void apply(WorkspaceContext ctx) {
        Path xml = ctx.workspaceRoot().toPath().resolve(EXTENSIONS_XML);
        if (!Files.exists(xml)) {
            ctx.log().debug(dimension() + ": no " + EXTENSIONS_XML + " — skipping");
            return;
        }
        String target = resolveExtensionVersion();
        try {
            boolean refreshed = WorkspaceBootstrap
                    .refreshExtensionsManagedBlock(xml, target);
            if (refreshed) {
                ctx.log().info("  ✓ " + EXTENSIONS_XML
                        + " → ike-workspace-extension:" + target);
            }
        } catch (IOException e) {
            ctx.log().warn(dimension() + ": refresh failed: " + e.getMessage());
        }
    }

    private static boolean matchesTarget(String content, String target) {
        int begin = content.indexOf(WorkspaceBootstrap.EXTENSIONS_MANAGED_BEGIN);
        if (begin < 0) {
            return false;
        }
        int end = content.indexOf(WorkspaceBootstrap.EXTENSIONS_MANAGED_END, begin);
        if (end < 0) {
            return false;
        }
        String block = content.substring(begin, end);
        return block.contains("<version>" + target + "</version>");
    }

    private static String resolveExtensionVersion() {
        try (InputStream is = ExtensionsXmlReconciler.class
                .getResourceAsStream(PROPERTIES_RESOURCE)) {
            if (is != null) {
                Properties props = new Properties();
                props.load(is);
                String v = props.getProperty("ike-workspace-extension.version");
                if (v != null && !v.isBlank() && !v.startsWith("${")) {
                    return v;
                }
            }
        } catch (IOException e) {
            // Fall through to fallback.
        }
        // Fallback for tests / unfiltered classpath.
        return "1";
    }
}