KnowledgeBindingsMojo.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.Language;
import org.apache.maven.api.PathScope;
import org.apache.maven.api.Project;
import org.apache.maven.api.ProjectScope;
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.DependencyResolver;
import org.apache.maven.api.services.DependencyResolverResult;
import org.apache.maven.api.services.ProjectManager;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

/**
 * Generates the bindings class for a ledger-form knowledge set — the {@code TinkarTerm}
 * idiom: one {@code EntityProxy} constant per declaration with its identity embedded as
 * a resolved UUID literal and its javadoc generated from the declaration's definition —
 * into {@code target/generated-sources/ike-knowledge}, registered as a compile source
 * root so it compiles the Maven way and autocompletes in any IDE that includes the
 * bindings artifact.
 *
 * <p>The generator itself lives tinkar-side ({@code
 * dev.ikm.tinkar.entity.builder.BindingsMain}) because this plugin stays tinkar-free
 * (the foundation boundary). This goal builds a classloader over the project's
 * {@code MAIN_RUNTIME} dependency classpath and invokes that one stable entry point
 * reflectively, in-process. The project's ledger dependency provides a
 * {@code KnowledgeSetSource} via {@code META-INF/services}; composition is store-free, so
 * no datastore or providers are needed on the classpath.
 *
 * <p>Typical use — a {@code *-bindings} module depending on its {@code *-terms} ledger
 * module:
 *
 * <pre>{@code
 * <plugin>
 *     <groupId>network.ike.tooling</groupId>
 *     <artifactId>ike-maven-plugin</artifactId>
 *     <executions>
 *         <execution>
 *             <goals><goal>knowledge-bindings</goal></goals>
 *             <configuration>
 *                 <packageName>network.ike.richsurface.bindings</packageName>
 *                 <className>RichSurfaceTerms</className>
 *             </configuration>
 *         </execution>
 *     </executions>
 * </plugin>
 * }</pre>
 *
 * @since 234
 */
@Mojo(name = IkeGoal.NAME_KNOWLEDGE_BINDINGS,
      defaultPhase = "generate-sources")
public class KnowledgeBindingsMojo implements org.apache.maven.api.plugin.Mojo {

    /** The stable tinkar-side entry point — change in lockstep with tinkar-core. */
    static final String BINDINGS_MAIN = "dev.ikm.tinkar.entity.builder.BindingsMain";

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

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

    /**
     * Package of the generated bindings class.
     */
    @Parameter(property = "ike.knowledgeBindings.packageName", required = true)
    String packageName;

    /**
     * Simple name of the generated bindings class, for example {@code RichSurfaceTerms}.
     */
    @Parameter(property = "ike.knowledgeBindings.className", required = true)
    String className;

    /**
     * Fully qualified name of the {@code KnowledgeSetSource} implementation to compose.
     * Optional: when absent, exactly one implementation must be discoverable via
     * {@code META-INF/services} on the project's dependency classpath.
     */
    @Parameter(property = "ike.knowledgeBindings.sourceClass")
    String sourceClass;

    /**
     * Root directory for the generated sources; registered as a compile source root.
     */
    @Parameter(property = "ike.knowledgeBindings.outputDirectory",
               defaultValue = "${project.build.directory}/generated-sources/ike-knowledge")
    String outputDirectory;

    /**
     * Skip bindings generation.
     */
    @Parameter(property = "ike.knowledgeBindings.skip", defaultValue = "false")
    boolean skip;

    /**
     * Resolves the project's runtime dependency classpath, invokes the tinkar-side
     * generator in a classloader over it, and registers the generated-sources root.
     *
     * @throws MojoException if the classpath cannot be resolved, the generator entry
     *                       point is absent (the ledger dependency chain does not include
     *                       tinkar entity), or generation fails
     */
    @Override
    public void execute() {
        if (skip) {
            getLog().info("ike:knowledge-bindings skipped (ike.knowledgeBindings.skip=true)");
            return;
        }

        DependencyResolverResult resolved = session.getService(DependencyResolver.class)
                .resolve(session, project, PathScope.MAIN_RUNTIME);
        List<URL> classpath = new ArrayList<>();
        for (Path path : resolved.getPaths()) {
            try {
                classpath.add(path.toUri().toURL());
            } catch (Exception e) {
                throw new MojoException("Cannot convert classpath entry to URL: " + path, e);
            }
        }
        getLog().debug("knowledge-bindings classpath: " + classpath);

        List<String> args = new ArrayList<>(List.of(outputDirectory, packageName, className));
        if (sourceClass != null && !sourceClass.isBlank()) {
            args.add(sourceClass);
        }

        ClassLoader originalContext = Thread.currentThread().getContextClassLoader();
        try (URLClassLoader loader = new URLClassLoader("ike-knowledge-bindings",
                classpath.toArray(new URL[0]), ClassLoader.getPlatformClassLoader())) {
            Thread.currentThread().setContextClassLoader(loader);
            Class<?> mainClass = Class.forName(BINDINGS_MAIN, true, loader);
            Method main = mainClass.getMethod("main", String[].class);
            main.invoke(null, (Object) args.toArray(new String[0]));
        } catch (ClassNotFoundException e) {
            throw new MojoException(BINDINGS_MAIN + " is not on the project's dependency"
                    + " classpath — the ledger dependency chain must include dev.ikm.tinkar:entity"
                    + " (with the knowledge-set builder API)", e);
        } catch (InvocationTargetException e) {
            throw new MojoException("Bindings generation failed: "
                    + e.getCause().getMessage(), e.getCause());
        } catch (MojoException e) {
            throw e;
        } catch (Exception e) {
            throw new MojoException("Bindings generation failed", e);
        } finally {
            Thread.currentThread().setContextClassLoader(originalContext);
        }

        Path sourceRoot = Path.of(outputDirectory);
        session.getService(ProjectManager.class)
                .addSourceRoot(project, ProjectScope.MAIN, Language.JAVA_FAMILY, sourceRoot);
        getLog().info("Generated " + packageName + "." + className
                + " into " + sourceRoot + " (registered as compile source root)");
    }
}