KnowledgeAttachMojo.java

/*
 * Copyright © 2026 IKE Network (support@ike.network)
 *
 * Licensed 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 network.ike.plugin;

import org.apache.maven.api.ProducedArtifact;
import org.apache.maven.api.Project;
import org.apache.maven.api.Session;
import org.apache.maven.api.di.Inject;
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.ProjectManager;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

/**
 * Attaches produced knowledge artifacts to <em>another</em> reactor project — typically
 * the aggregator — so a knowledge set's released coordinate carries the set's own name
 * even though a subproject produced the files (IKE-Network/ike-issues#933). An
 * aggregator builds before its subprojects and cannot run the exports itself; this goal
 * runs in the producing subproject (after {@code ike:knowledge-export} and
 * {@code ike:kb-assemble}) and attaches each file under the target project's GAV, so
 * the released coordinate reads, for example,
 * {@code network.ike.foundation:ike-starter-set:<v>:reasoned-pb}.
 * <p>
 * <b>Pair with {@code deployAtEnd}/{@code installAtEnd}.</b> The target project's own
 * {@code install}/{@code deploy} executions run at its reactor position — before this
 * goal attaches — so the consuming reactor must configure
 * {@code maven-install-plugin} {@code installAtEnd=true} and {@code maven-deploy-plugin}
 * {@code deployAtEnd=true}; both defer publication to session end, where the late
 * attachments are included. Without that pairing the attachments never publish — this
 * goal cannot detect the misconfiguration, so it is stated loudly here and in the
 * consuming POM.
 */
@Mojo(name = IkeGoal.NAME_KNOWLEDGE_ATTACH, defaultPhase = "verify")
public class KnowledgeAttachMojo implements org.apache.maven.api.plugin.Mojo {

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

    @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;
    }

    @Inject
    private Session session;

    @Inject
    private Project project;

    /**
     * The target project's groupId — the GAV the attachments publish under. Defaults to
     * this project's own groupId (aggregator and subprojects usually share one).
     */
    @Parameter(property = "ike.knowledgeAttach.targetGroupId",
               defaultValue = "${project.groupId}")
    String targetGroupId;

    /**
     * The target project's artifactId — typically the aggregator whose name is the
     * knowledge set's released coordinate.
     */
    @Parameter(property = "ike.knowledgeAttach.targetArtifactId", required = true)
    String targetArtifactId;

    /**
     * The attachments, each {@code "<classifier> <file>"} — mirroring the
     * {@code ike:kb-assemble} input idiom of a marker followed by a spec. Example:
     * {@code <attachment>reasoned-pb ${project.build.directory}/x-reasoned-pb.zip</attachment>}.
     * Extension is always {@code zip}.
     */
    @Parameter
    List<String> attachments = List.of();

    /**
     * Skip attaching.
     */
    @Parameter(property = "ike.knowledgeAttach.skip", defaultValue = "false")
    boolean skip;

    /**
     * Resolves the target reactor project and attaches each declared file under its
     * GAV.
     *
     * @throws MojoException if the target project is not in the reactor, an attachment
     *                       entry is malformed, or a declared file does not exist
     */
    @Override
    public void execute() {
        if (skip) {
            getLog().info("ike:knowledge-attach skipped (ike.knowledgeAttach.skip=true)");
            return;
        }
        if (attachments.isEmpty()) {
            throw new MojoException("ike:knowledge-attach requires at least one"
                    + " <attachments><attachment> entry (\"<classifier> <file>\")");
        }

        Project target = session.getProjects().stream()
                .filter(candidate -> candidate.getGroupId().equals(targetGroupId)
                        && candidate.getArtifactId().equals(targetArtifactId))
                .findFirst()
                .orElseThrow(() -> new MojoException("Attach target " + targetGroupId + ":"
                        + targetArtifactId + " is not in this reactor — ike:knowledge-attach"
                        + " publishes under a reactor project's GAV (run from the full"
                        + " reactor, not with -pl alone)"));

        ProjectManager projectManager = session.getService(ProjectManager.class);
        for (String entry : attachments) {
            String stripped = entry.strip();
            int space = stripped.indexOf(' ');
            if (space <= 0) {
                throw new MojoException("Attachment entry needs \"<classifier> <file>\": " + entry);
            }
            String classifier = stripped.substring(0, space);
            Path file = Path.of(stripped.substring(space + 1).strip());
            if (!Files.isRegularFile(file)) {
                throw new MojoException("Attachment file does not exist: " + file
                        + " (classifier " + classifier + ") — run the producing goal first");
            }
            ProducedArtifact artifact = session.createProducedArtifact(
                    target.getGroupId(), target.getArtifactId(), target.getVersion(),
                    classifier, "zip", "zip");
            projectManager.attachArtifact(target, artifact, file);
            getLog().info("Attached " + file.getFileName() + " under " + target.getGroupId()
                    + ":" + target.getArtifactId() + ":" + target.getVersion()
                    + " (classifier: " + classifier + ", extension: zip)");
        }
    }
}