IkeSiteDraftMojo.java

package network.ike.plugin;

import network.ike.plugin.reconcile.SiteContext;
import network.ike.plugin.reconcile.SiteDriftReport;
import network.ike.plugin.reconcile.SiteReconciler;
import network.ike.plugin.reconcile.SiteReconcilerOptions;
import network.ike.plugin.reconcile.SiteReconcilerRegistry;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Mojo;
import org.apache.maven.api.plugin.annotations.Parameter;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

/**
 * Diagnostic that reports drift in the project's deployed-site state.
 *
 * <p>Reads the current deployed site at
 * {@code https://ike.network/<artifactId>/} and the project's entry on
 * the IKE Network landing page, compares them to the current POM
 * version, and prints a drift report with copy-paste opt-out commands
 * inline.
 *
 * <p>Read-only — no remote mutation. Use {@link IkeSitePublishMojo}
 * to apply.
 *
 * <p>Subsumes the retired {@code ike:deploy-site-draft},
 * {@code ike:register-site-draft}, {@code ike:deregister-site-draft},
 * and {@code ike:clean-site} (in their preview-only roles) goals. See
 * IKE-Network/ike-issues#398.
 *
 * <p>Usage:
 * <pre>{@code
 * mvn ike:site-draft        # report deployed-site drift
 * mvn ike:site-publish      # apply (deploy + register)
 * }</pre>
 *
 * @see IkeSitePublishMojo
 */
@Mojo(name = "site-draft", projectRequired = true, aggregator = true)
public class IkeSiteDraftMojo implements org.apache.maven.api.plugin.Mojo {

    @org.apache.maven.api.di.Inject
    private org.apache.maven.api.plugin.Log log;

    /**
     * Access the Maven logger.
     *
     * @return the logger
     */
    protected org.apache.maven.api.plugin.Log getLog() { return log; }

    /**
     * Git URL of the org-site SOURCE repository (has the Maven pom
     * and src/site/ tree where per-project fragments live).
     */
    @Parameter(property = "srcRepo",
               defaultValue = "https://github.com/IKE-Network/ike-network-site.git")
    String srcRepo;

    /** Branch for source content in the source repo. */
    @Parameter(property = "srcBranch", defaultValue = "main")
    String srcBranch;

    /**
     * Git URL of the org-site PUBLISH repository (rendered HTML
     * served at https://ike.network/).
     */
    @Parameter(property = "pubRepo",
               defaultValue = "https://github.com/IKE-Network/IKE-Network.github.io.git")
    String pubRepo;

    /** Branch for rendered content in the publish repo. */
    @Parameter(property = "pubBranch", defaultValue = "main")
    String pubBranch;

    /**
     * Human-readable project name. Defaults to the project's POM
     * {@code <name>}. Read at execute time because
     * {@code defaultValue = "${project.name}"} does not interpolate
     * under Maven 4 with {@code aggregator = true} (the field arrives
     * as a literal {@code null} — same bug the retired
     * {@code RegisterSiteDraftMojo} worked around before #398).
     */
    @Parameter(property = "projectName")
    String projectName;

    /** One-line project description. Same caveat as {@link #projectName}. */
    @Parameter(property = "projectDescription")
    String projectDescription;

    /** Site URL on ike.network. Derived from artifact ID if not set. */
    @Parameter(property = "projectSiteUrl")
    String projectSiteUrl;

    /** GitHub repository URL. Derived from artifact ID if not set. */
    @Parameter(property = "githubUrl")
    String githubUrl;

    /** Version to reconcile against. Defaults to release version (SNAPSHOT stripped). */
    @Parameter(property = "releaseVersion")
    String releaseVersion;

    /**
     * Internal toggle: {@code true} for {@code site-publish},
     * {@code false} for {@code site-draft}.
     */
    @Parameter(property = "publish", defaultValue = "false")
    boolean publish;

    /** Creates this goal instance. */
    public IkeSiteDraftMojo() {}

    @Override
    public void execute() throws MojoException {
        SiteContext ctx = buildContext();

        getLog().info("");
        getLog().info(publish ? "ike:site-publish" : "ike:site-draft");
        getLog().info("══════════════════════════════════════════════════════════════");
        getLog().info("  Project:        " + ctx.projectId());
        getLog().info("  Version:        " + ctx.projectVersion());
        getLog().info("  Site URL:       " + ctx.projectSiteUrl());
        if (publish && ctx.options().isUninstall()) {
            getLog().info("  Mode:           uninstall (-Dsite=removed)");
        }
        getLog().info("");

        if (publish) {
            runPublish(ctx);
        } else {
            runDraft(ctx);
        }
    }

    /** Iterate reconcilers in detect-mode and render reports. */
    private void runDraft(SiteContext ctx) {
        int driftCount = 0;
        for (SiteReconciler reconciler : SiteReconcilerRegistry.all()) {
            SiteDriftReport report = reconciler.detect(ctx);
            printDriftReport(report);
            if (report.hasDrift()) driftCount++;
        }
        getLog().info("");
        getLog().info("══════════════════════════════════════════════════════════════");
        if (driftCount == 0) {
            getLog().info("  No drift detected.");
        } else {
            getLog().info("  " + driftCount + " dimension(s) drift; "
                    + "run ike:site-publish to apply.");
        }
    }

    /** Iterate reconcilers in apply-mode (or uninstall when -Dsite=removed). */
    private void runPublish(SiteContext ctx) {
        if (ctx.options().isUninstall()) {
            for (SiteReconciler reconciler : SiteReconcilerRegistry.uninstallOrder()) {
                reconciler.uninstall(ctx);
            }
        } else {
            for (SiteReconciler reconciler : SiteReconcilerRegistry.all()) {
                reconciler.apply(ctx);
            }
        }
        getLog().info("");
        getLog().info("══════════════════════════════════════════════════════════════");
        getLog().info("  Done.");
    }

    /**
     * Build the {@link SiteContext} from current Mojo parameters and
     * the project's POM. Mirrors the parameter-resolution logic of the
     * retired {@code RegisterSiteDraftMojo}.
     */
    private SiteContext buildContext() throws MojoException {
        File gitRoot = ReleaseSupport.gitRoot(new File("."));
        File rootPom = new File(gitRoot, "pom.xml");
        String projectId = ReleaseSupport.readPomArtifactId(rootPom);

        String version;
        if (releaseVersion != null && !releaseVersion.isBlank()) {
            version = releaseVersion;
        } else {
            String pomVersion = ReleaseSupport.readPomVersion(rootPom);
            version = pomVersion.replace("-SNAPSHOT", "");
        }

        String resolvedName = projectName;
        if (resolvedName == null || resolvedName.isBlank()) {
            resolvedName = ReleaseSupport.readPomName(rootPom);
            if (resolvedName == null || resolvedName.isBlank()) {
                resolvedName = projectId;
            }
        }

        String resolvedDescription = projectDescription;
        if (resolvedDescription == null || resolvedDescription.isBlank()) {
            resolvedDescription = ReleaseSupport.readPomDescription(rootPom);
        }

        String resolvedSiteUrl = projectSiteUrl;
        if (resolvedSiteUrl == null || resolvedSiteUrl.isBlank()) {
            resolvedSiteUrl = "https://ike.network/" + projectId + "/";
        }

        String resolvedGithub = githubUrl;
        if (resolvedGithub == null || resolvedGithub.isBlank()) {
            resolvedGithub = "https://github.com/IKE-Network/" + projectId;
        }

        return new SiteContext(
                gitRoot, projectId, version,
                resolvedName, resolvedDescription,
                resolvedSiteUrl, resolvedGithub,
                srcRepo, srcBranch, pubRepo, pubBranch,
                readReconcilerOptions(), getLog());
    }

    /**
     * Collect Maven system properties into a {@link SiteReconcilerOptions}
     * bag so reconcilers can query their opt-out flags
     * (e.g., {@code -DupdateSite=false}) and the {@code -Dsite=removed}
     * uninstall switch.
     */
    private static SiteReconcilerOptions readReconcilerOptions() {
        Map<String, String> flags = new HashMap<>();
        for (String name : System.getProperties().stringPropertyNames()) {
            flags.put(name, System.getProperty(name));
        }
        return new SiteReconcilerOptions(flags);
    }

    /**
     * Render a {@link SiteDriftReport} for {@code site-draft} output
     * with the copy-paste opt-out command inline. Identical formatting
     * shape to {@code ws:scaffold-draft}'s {@code printDriftReport}.
     */
    private void printDriftReport(SiteDriftReport report) {
        getLog().info("");
        if (!report.hasDrift()) {
            getLog().info("  ✓ " + report.dimension());
            return;
        }
        getLog().info("  ⚠ " + report.dimension());
        if (!report.summary().isEmpty()) {
            getLog().info("     " + report.summary());
        }
        for (String line : report.detailLines()) {
            getLog().info("       " + line);
        }
        if (!report.defaultAction().isEmpty()) {
            getLog().info("     Default: " + report.defaultAction());
        }
        if (!report.optOutCommand().isEmpty()) {
            getLog().info("     Opt out: " + report.optOutCommand());
        }
    }
}