PackageDocMojo.java

package network.ike.docs.plugin;

import org.apache.maven.api.ProducedArtifact;
import org.apache.maven.api.Project;
import org.apache.maven.api.Session;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Mojo;
import org.apache.maven.api.plugin.annotations.Parameter;
import org.apache.maven.api.services.ArtifactManager;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * Package AsciiDoc sources as the primary artifact for {@code ike-doc} packaging.
 *
 * <p>Zips the contents of {@code src/docs/asciidoc/} into a {@code .zip}
 * file and sets it as the project's primary artifact. This makes the
 * AsciiDoc source the canonical artifact — rendered outputs (HTML, PDF)
 * are attached as classifier artifacts by the rendering pipeline.
 *
 * <p>This goal is bound to the {@code package} phase in the
 * {@code ike-doc} lifecycle mapping. It can also be invoked directly:
 *
 * <pre>
 * mvn ike:package-doc
 * </pre>
 */
@Mojo(name = "package-doc",
      defaultPhase = "package",
      projectRequired = true)
public class PackageDocMojo 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; }

    /** The current project (injected by Maven 4). */
    @org.apache.maven.api.di.Inject
    private Project project;

    /** The current Maven session (injected by Maven 4). */
    @org.apache.maven.api.di.Inject
    private Session session;

    /**
     * AsciiDoc source directory to package.
     * Defaults to the standard IKE documentation source location.
     */
    @Parameter(property = "ike.doc.sourceDir",
               defaultValue = "${project.basedir}/src/docs/asciidoc")
    private File sourceDir;

    /** Skip execution. */
    @Parameter(property = "ike.doc.skip", defaultValue = "false")
    private boolean skip;

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

    @Override
    public void execute() throws MojoException {
        if (skip) {
            getLog().info("package-doc: skipped");
            return;
        }

        if (!sourceDir.isDirectory()) {
            getLog().warn("package-doc: source directory does not exist — "
                    + sourceDir + ". Producing empty artifact.");
        }

        Path source = sourceDir.toPath();
        String finalName = project.getBuild().getFinalName();
        Path outputDir = Path.of(project.getBuild().getDirectory());
        Path zipFile = outputDir.resolve(finalName + ".zip");

        try {
            Files.createDirectories(outputDir);
            int count = zipDirectory(source, zipFile);
            getLog().info("package-doc: packaged " + count
                    + " file(s) into " + zipFile.getFileName());
        } catch (IOException e) {
            throw new MojoException(
                    "Failed to create documentation archive", e);
        }

        // Set the zip as this project's main artifact. For ike-doc
        // packaging, the ArtifactHandler declared in components.xml
        // gives the main artifact a .zip extension; we just supply
        // the file.
        ProducedArtifact mainArtifact = project.getMainArtifact()
                .orElseThrow(() -> new MojoException(
                        "package-doc: project has no main artifact. "
                                + "Is packaging set to ike-doc?"));
        ArtifactManager artifactManager =
                session.getService(ArtifactManager.class);
        artifactManager.setPath(mainArtifact, zipFile);
    }

    // ── Pure testable function ───────────────────────────────────────

    /**
     * Zip the contents of a directory into a zip file.
     *
     * <p>If the source directory does not exist or is empty, an empty
     * zip file is produced (valid for Maven install/deploy).
     *
     * @param sourceDir directory to zip (may not exist)
     * @param zipFile   output zip file path
     * @return number of files added to the zip
     * @throws IOException if an I/O error occurs
     */
    static int zipDirectory(Path sourceDir, Path zipFile) throws IOException {
        var count = new AtomicInteger();
        try (OutputStream fos = Files.newOutputStream(zipFile);
             ZipOutputStream zos = new ZipOutputStream(fos)) {

            if (Files.isDirectory(sourceDir)) {
                Files.walkFileTree(sourceDir, new SimpleFileVisitor<>() {
                    @Override
                    public FileVisitResult visitFile(Path file,
                            BasicFileAttributes attrs) throws IOException {
                        String entryName = sourceDir.relativize(file)
                                .toString();
                        zos.putNextEntry(new ZipEntry(entryName));
                        Files.copy(file, zos);
                        zos.closeEntry();
                        count.incrementAndGet();
                        return FileVisitResult.CONTINUE;
                    }
                });
            }
        }
        return count.get();
    }
}