Micronaut Build Plugin Sourcegen

Project for generating sources of build plugin tasks

Version: 0.0.1-SNAPSHOT

1 Introduction

Micronaut Build plugin sourcegen allows generating sources of Gradle and Maven plugins.

This is most useful for resource-intensive tasks that have a considerable amount of parameters, but are not closely coupled with plugin logic. An example of such task is generating sources or resources. The idea is that developer writes task logic and describes the API, while plugin sources are generated for this project to start the task using the API.

The main advantage of using the generator is that parameters do not need to be copied manually to plugin implementations separately avoiding human error. For each parameter, default value, whether it is required and javadoc will be copied.

This project is based on Micronaut sourcegen.

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Getting Started

The most likely structure for a project that wants to utilize this consists of 3 modules:

  • Common module with task logic.

  • Module with generated Gradle plugin sources and possible user extensions.

  • Module with generated Maven plugin sources and possible user extensions.

The 3 modules might be in different repositories. It is possible to use a single module, but it is not recommended.

3.1 Common Module

The common module should have the following dependencies:

implementation("io.micronaut.build.plugin.sourcegen:micronaut-build-plugin-sourcegen-annotations")
<dependency>
    <groupId>io.micronaut.build.plugin.sourcegen</groupId>
    <artifactId>micronaut-build-plugin-sourcegen-annotations</artifactId>
</dependency>

annotationProcessor("io.micronaut.build.plugin.sourcegen:micronaut-build-plugin-sourcegen-generator")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.build.plugin.sourcegen</groupId>
        <artifactId>micronaut-build-plugin-sourcegen-generator</artifactId>
    </path>
</annotationProcessorPaths>

Use the PluginTask annotation to define a plugin task. In this example we will create a task that can generate simple record sources. User specifies the type name, properties and javadoc information and then record is generated and added to their sources.

import io.micronaut.sourcegen.annotations.PluginTask;
import io.micronaut.sourcegen.annotations.PluginTaskExecutable;
import io.micronaut.sourcegen.annotations.PluginTaskParameter;
import io.micronaut.sourcegen.annotations.PluginTaskParameter.OutputType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is a configuration for a plugin task run.
 * The properties are parameters and the single method defines the task execution.
 * The plugin generates a simple record.
 *
 * @param typeName The generated class name
 * @param version The version
 * @param packageName The package name
 * @param properties The properties
 * @param javadoc The javadoc
 * @param outputFolder The output folder
 */
@PluginTask // (1)
public record GenerateSimpleRecordTask(
    @PluginTaskParameter(required = true, globalProperty = "typeName")
    String typeName, // (2)
    @PluginTaskParameter(defaultValue = "1", globalProperty = "version")
    Integer version, // (3)
    @PluginTaskParameter(defaultValue = "com.example", globalProperty = "packageName")
    String packageName,
    Map<String, String> properties,
    List<String> javadoc,
    @PluginTaskParameter(output = OutputType.JAVA_SOURCES, directory = true, required = true, internal = true)
    File outputFolder // (4)
) {

    private static final Logger LOG = LoggerFactory.getLogger(GenerateSimpleRecordTask.class);

    private static final String CONTENT = """
package %s;

/**
 * Version: %s
%s
 */
public record %s(
%s
) {
}
""";

    /**
     * Generate a simple record in the supplied package and with the specified version.
     * This javadoc will be copied to the respected plugin implementations.
     */
    @PluginTaskExecutable // (5)
    public void generateSimpleRecord() {
        LOG.info("Generating record {}", typeName);

        File packageFolder = new File(outputFolder, packageName.replace(".", File.separator));
        packageFolder.mkdirs();
        // Create the content of the file using the CONTENT template
        String content = String.format(
            CONTENT,
            packageName,
            version,
            javadoc.stream().map(v -> " * " + v).collect(Collectors.joining("\n")),
            typeName,
            properties.entrySet().stream().map(e -> "    " + e.getValue() + " " + e.getKey())
                .collect(Collectors.joining(",\n"))
        );
        File outputFile = new File(packageFolder, typeName + ".java");

        // Write the file
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) {
            writer.write(content);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        LOG.info("Finished record {}", typeName);
    }

}
1 Define the task. The task can be a record or a Java class.
2 Use the PluginTaskParameter annotation to define a parameter. Set required = true for mandatory parameters.
3 Define another parameter. Set the default value if any.
4 Specify output = OutputType.JAVA_SOURCES for outputs of the task. Specify it as internal to later customize it in particular plugins.
5 Use the PluginTaskExecutable to define the executable for task. The executable will use the parameters defined for the task.

See documentation for PluginTask, PluginTaskParameter and PluginTaskExecutable to view all the configurable properties.

3.2 Gradle Module

The Gradle module should have the same dependencies and also the dependency on the common module.

implementation("io.micronaut.build.plugin.sourcegen:micronaut-build-plugin-sourcegen-annotations")
<dependency>
    <groupId>io.micronaut.build.plugin.sourcegen</groupId>
    <artifactId>micronaut-build-plugin-sourcegen-annotations</artifactId>
</dependency>

compileOnly("io.micronaut.test:micronaut-test-plugin-common")
<dependency>
    <groupId>io.micronaut.test</groupId>
    <artifactId>micronaut-test-plugin-common</artifactId>
    <scope>provided</scope>
</dependency>

annotationProcessor("io.micronaut.build.plugin.sourcegen:micronaut-build-plugin-sourcegen-generator")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.build.plugin.sourcegen</groupId>
        <artifactId>micronaut-build-plugin-sourcegen-generator</artifactId>
    </path>
</annotationProcessorPaths>

annotationProcessor("io.micronaut.test:micronaut-test-plugin-common")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.test</groupId>
        <artifactId>micronaut-test-plugin-common</artifactId>
    </path>
</annotationProcessorPaths>

Adding the common plugin to the annotation processor paths is currently required to retrieve javadoc.

Use the GenerateGradlePlugin annotation to trigger generation of Gradle Plugin sources.

import io.micronaut.sourcegen.annotations.GenerateGradlePlugin;
import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.GenerateGradleTask;

@GenerateGradlePlugin(
    namePrefix = "Test", // (1)
    micronautPlugin = false,
    tasks = {
        @GenerateGradleTask(
            namePrefix = "GenerateSimpleRecord",
            extensionMethodName = "generateSimpleRecord",
            source = "io.micronaut.sourcegen.example.plugin.GenerateSimpleRecordTask" // (2)
        ),
        @GenerateGradleTask(
            namePrefix = "GenerateSimpleResource",
            extensionMethodName = "generateSimpleResource",
            source = "io.micronaut.sourcegen.example.plugin.GenerateSimpleResourceTask" // (3)
        )
    }
)
public final class GeneratePluginTrigger {
}
1 Specify the name prefix for all generated sources. TestPlugin and TestExtension will be generated based on this.
2 Use the GenerateGradleTask annotation to define generation of a task. Specify the task from common module annotated with PluginTask as source. Based on the prefix, GenerateSimpleRecordTask and GenerateSimpleRecordSpec will be generated.
3 If you create another task, you can add it to the same plugin.

The following sources will be generated based on this:

  1. TestPlugin - the Gradle plugin base that adds the extension to user project.

  2. TestExtension and DefaultTestExtension - extension and its implementation that allow calling tasks. Each task can be configured with the corresponding extension method.

  3. GenerateSimpleRecordTask - the Gradle task that is responsible for actually calling your task logic.

  4. GenerateSimpleRecordSpec - the specification with all the task parameters that user can configure when calling the extension method.

You can see the javadoc for the generated sources in this example in io.micronaut.sourcegen.example.plugin.maven package javadoc.

See documentation for GenerateGradleTask to view all the configurable properties.

Plugin Customization

Plugin and extension can be extended to add custom Gradle-specific behavior.

import org.gradle.api.Action;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.TaskProvider;

import java.util.function.Consumer;

/**
 * This extends a generated class to modify some behavior.
 */
public abstract class TestExtensionImpl extends DefaultTestExtension { // (1)

    public TestExtensionImpl(Project project, Configuration classpath) {
        super(project, classpath);
    }


    /**
     * This is an example of how you can add a utility method to the generated extension.
     * Inside it calls the generated method.
     *
     * @param typeName The type name
     * @param packageName The package name
     * @param action The spec action
     */ // (2)
    public void generateRecordWithName(String typeName, String packageName, Action<GenerateSimpleRecordSpec> action) {
        super.generateSimpleRecord("generate" + typeName, spec -> {
            spec.getTypeName().set(typeName);
            spec.getPackageName().set(packageName);
            action.execute(spec);
        });
    }

    /**
     * Overriding a method to make sure that output directory has a correct default value.
     * We are also adding to source sets here.
     *
     * @param name The task name
     * @param configurator The configurator action
     * @return The task
     */
    @Override
    TaskProvider<? extends GenerateSimpleRecordTask> createGenerateSimpleRecordTask(
            String name, Action<GenerateSimpleRecordTask> configurator
    ) {
        return super.createGenerateSimpleRecordTask(name, t -> {
            configurator.execute(t);
            t.getOutputFolder().convention(
                project.getLayout().getBuildDirectory().dir("generated/" + t.getName() + "/java")
            );
        });
    }

}
1 Extend the generated TestExtension class.
2 Create a utility extension method that users could call instead.
3 Add the default value for the outputFolder parameter.

3.3 Maven Module

The Maven module should have the same dependencies and also the dependency on the common module.

implementation("io.micronaut.build.plugin.sourcegen:micronaut-build-plugin-sourcegen-annotations")
<dependency>
    <groupId>io.micronaut.build.plugin.sourcegen</groupId>
    <artifactId>micronaut-build-plugin-sourcegen-annotations</artifactId>
</dependency>

compileOnly("io.micronaut.test:micronaut-test-plugin-common")
<dependency>
    <groupId>io.micronaut.test</groupId>
    <artifactId>micronaut-test-plugin-common</artifactId>
    <scope>provided</scope>
</dependency>

annotationProcessor("io.micronaut.build.plugin.sourcegen:micronaut-build-plugin-sourcegen-generator")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.build.plugin.sourcegen</groupId>
        <artifactId>micronaut-build-plugin-sourcegen-generator</artifactId>
    </path>
</annotationProcessorPaths>

annotationProcessor("io.micronaut.test:micronaut-test-plugin-common")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.test</groupId>
        <artifactId>micronaut-test-plugin-common</artifactId>
    </path>
</annotationProcessorPaths>

Adding the common plugin to the annotation processor paths is currently required to retrieve javadoc.

Use the GenerateMavenMojo annotation to trigger generation of Maven Plugin sources.

import io.micronaut.sourcegen.annotations.GenerateMavenMojo;

/**
 * A class that triggers Maven Mojo generation.
 */
@GenerateMavenMojo( // (1)
    namePrefix = "AbstractGenerateSimpleRecord",
    micronautPlugin = false,
    source = "io.micronaut.sourcegen.example.plugin.GenerateSimpleRecordTask",
    propertyPrefix = "test.generate.simple.record"
)
@GenerateMavenMojo( // (2)
    namePrefix = "AbstractGenerateSimpleResource",
    micronautPlugin = false,
    source = "io.micronaut.sourcegen.example.plugin.GenerateSimpleResourceTask",
    propertyPrefix = "test.generate.simple.resource"
)
public final class GenerateMojoTrigger {
}
1 Trigger generation of a mojo. Specify the task from common module annotated with PluginTask as source. Based on the prefix, AbstractGenerateSimpleRecordMojo will be generated.
2 If you create another task, you can generate another Mojo for it.

Only AbstractGenerateSimpleRecordMojo class will be generated. The mojo will have all the specified task parameters and will call the defined task as its action. You can see the javadoc for the generated sources in this example in io.micronaut.sourcegen.example.plugin.maven package javadoc.

See documentation for GenerateMavenMojo to view all the configurable properties.

Mojo Customization

Extend the Mojo to add custom Maven-specific behavior:

/**
 * An extension of the generated mojo that configures the output folder.
 */
@Mojo(name = "generateSimpleRecord") // (1)
public class GenerateSimpleRecordMojo extends AbstractGenerateSimpleRecordMojo {

    @Parameter(
        required = true,
        defaultValue = "${project.build.directory}/generated/simpleRecord/java" // (2)
    )
    protected File outputFolder;

    @Override
    protected File getOutputFolder() {
        return outputFolder;
    }
}
1 Specify a name for Mojo.
2 Add the default value for the outputFolder parameter.

4 Repository

You can find the source code of this project in this repository: