diff --git a/.github/workflows/graalvm-latest.yml b/.github/workflows/graalvm-latest.yml index e3053b0..a1582b5 100644 --- a/.github/workflows/graalvm-latest.yml +++ b/.github/workflows/graalvm-latest.yml @@ -8,11 +8,11 @@ on: push: branches: - master - - '[1-9]+.[0-9]+.x' + - '[0-9]+.[0-9]+.x' pull_request: branches: - master - - '[1-9]+.[0-9]+.x' + - '[0-9]+.[0-9]+.x' jobs: build_matrix: if: github.repository != 'micronaut-projects/micronaut-project-template' diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 91016fd..afca08e 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -8,11 +8,11 @@ on: push: branches: - master - - '[1-9]+.[0-9]+.x' + - '[0-9]+.[0-9]+.x' pull_request: branches: - master - - '[1-9]+.[0-9]+.x' + - '[0-9]+.[0-9]+.x' jobs: build: if: github.repository != 'micronaut-projects/micronaut-project-template' @@ -33,13 +33,13 @@ jobs: OSS_INDEX_USERNAME: ${{ secrets.OSS_INDEX_USERNAME }} OSS_INDEX_PASSWORD: ${{ secrets.OSS_INDEX_PASSWORD }} steps: - # https://github.com/actions/virtual-environments/issues/709 + # https://github.com/actions/virtual-environments/issues/709 - name: "🗑 Free disk space" run: | - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - sudo apt-get clean - df -h + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo apt-get clean + df -h - name: "📥 Checkout repository" uses: actions/checkout@v4 diff --git a/README.md b/README.md index 7c10fb5..96a2b10 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ # Micronaut build-plugin-sourcegen -[![Maven Central](https://img.shields.io/maven-central/v/io.micronaut.build-plugin-sourcegen/micronaut-project-template.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22io.micronaut.project-template%22%20AND%20a:%22micronaut-project-template%22) -[![Build Status](https://github.com/micronaut-projects/micronaut-build-plugin-sourcegen/workflows/Java%20CI/badge.svg)](https://github.com/micronaut-projects/micronaut-project-template/actions) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=micronaut-projects_micronaut-template&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=micronaut-projects_micronaut-template) +[![Maven Central](https://img.shields.io/maven-central/v/io.micronaut.build-plugin-sourcegen/micronaut-project-template.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22io.micronaut.build.plugin.sourcegen%22%20AND%20a:%22micronaut-build-plugin-sourcegen%22) +[![Build Status](https://github.com/micronaut-projects/micronaut-build-plugin-sourcegen/workflows/Java%20CI/badge.svg)](https://github.com/micronaut-projects/micronaut-build-plugin-sourcegen/actions) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=micronaut-projects_micronaut-template&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=micronaut-projects_micronaut-build-plugin-sourcegen) [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.micronaut.io/scans) Micronaut build-plugin-sourcegen diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 6784052..4675969 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -1,3 +1,11 @@ plugins { id 'groovy-gradle-plugin' } + +repositories { + mavenCentral() +} + +dependencies { + implementation(libs.sonatype.scan) +} diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle new file mode 100644 index 0000000..6f31e6e --- /dev/null +++ b/buildSrc/settings.gradle @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + libs { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-module.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-module.gradle index 4605dad..cd679a3 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-module.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-module.gradle @@ -1,4 +1,28 @@ plugins { id 'io.micronaut.build.internal.build-plugin-sourcegen-base' id "io.micronaut.build.internal.module" + id "org.sonatype.gradle.plugins.scan" } + +repositories { + maven { + setUrl("https://repo.gradle.org/gradle/libs-releases") + } +} + +micronautBuild { + binaryCompatibility { + enabledAfter("1.0.0") + } +} + +String ossIndexUsername = System.getenv("OSS_INDEX_USERNAME") ?: project.properties["ossIndexUsername"] +String ossIndexPassword = System.getenv("OSS_INDEX_PASSWORD") ?: project.properties["ossIndexPassword"] +boolean sonatypePluginConfigured = ossIndexUsername != null && ossIndexPassword != null +if (sonatypePluginConfigured) { + ossIndexAudit { + username = ossIndexUsername + password = ossIndexPassword + } +} + diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-testsuite.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-testsuite.gradle new file mode 100644 index 0000000..495b8be --- /dev/null +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-testsuite.gradle @@ -0,0 +1,18 @@ +plugins { + id("io.micronaut.build.internal.build-plugin-sourcegen-base") + id("io.micronaut.build.internal.common") +} + +repositories { + maven { + setUrl("https://repo.gradle.org/gradle/libs-releases") + } +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() +} + +tasks.withType(Checkstyle).configureEach { + enabled = false +} diff --git a/config/spotless.license.java b/config/spotless.license.java index 91ec22a..7d445b0 100644 --- a/config/spotless.license.java +++ b/config/spotless.license.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-$YEAR original authors + * Copyright 2025 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,4 +12,4 @@ * 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. - */ \ No newline at end of file + */ diff --git a/gradle.properties b/gradle.properties index 912a7ee..aa50a0d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,11 @@ projectVersion=0.0.1-SNAPSHOT -projectGroup=io.micronaut.build-plugin-sourcegen +projectGroup=io.micronaut.build.plugin.sourcegen -title=Micronaut build-plugin-sourcegen -projectDesc=TODO +title=Micronaut Build Plugin Sourcegen +projectDesc=Project for generating sources of build plugin tasks projectUrl=https://micronaut.io githubSlug=micronaut-projects/micronaut-build-plugin-sourcegen -developers=Graeme Rocher +developers=Andriy Dmytruk org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 org.gradle.configuration-cache=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 42d5c28..89c4fb8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,8 +19,16 @@ micronaut = "4.7.10" micronaut-docs = "2.0.0" micronaut-test = "4.6.2" +micronaut-sourcegen = "1.6.1" + groovy = "4.0.24" spock = "2.3-groovy-4.0" +gradle-plugins-api = "8.11.1" +maven-plugin-annotations = "3.9.0" +maven-core = "3.9.4" +maven-plugin-testing-harness = "3.3.0" + +sonatype-scan = "3.0.0" # Managed versions appear in the BOM # managed-somelib = "1.0" @@ -29,6 +37,15 @@ spock = "2.3-groovy-4.0" [libraries] # Core micronaut-core = { module = 'io.micronaut:micronaut-core-bom', version.ref = 'micronaut' } +micronaut-sourcegen = { module = 'io.micronaut.sourcegen:micronaut-sourcegen-bom', version.ref = 'micronaut-sourcegen' } + +gradle-plugins-api = { module = 'dev.gradleplugins:gradle-api', version.ref = 'gradle-plugins-api' } +maven-plugin-annotations = { module = 'org.apache.maven.plugin-tools:maven-plugin-annotations', version.ref = 'maven-plugin-annotations' } +maven-plugin-api = { module = 'org.apache.maven:maven-plugin-api', version.ref = 'maven-core' } +maven-core = { module = 'org.apache.maven:maven-core', version.ref = 'maven-core' } +maven-plugin-testing-harness = { module = 'org.apache.maven.plugin-testing:maven-plugin-testing-harness', version.ref = 'maven-plugin-testing-harness' } + +sonatype-scan = { module = "org.sonatype.gradle.plugins:scan-gradle-plugin", version.ref = "sonatype-scan" } # # Managed dependencies appear in the BOM diff --git a/micronaut-build-plugin-sourcegen-annotations/build.gradle.kts b/micronaut-build-plugin-sourcegen-annotations/build.gradle.kts new file mode 100644 index 0000000..6d26691 --- /dev/null +++ b/micronaut-build-plugin-sourcegen-annotations/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("io.micronaut.build.internal.build-plugin-sourcegen-module") +} diff --git a/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/GenerateGradlePlugin.java b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/GenerateGradlePlugin.java new file mode 100644 index 0000000..289b92e --- /dev/null +++ b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/GenerateGradlePlugin.java @@ -0,0 +1,120 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * An annotation that triggers the generation of Gradle plugin and other types. + * Only single Gradle plugin should be generated. But a plugin may contain + * more than one task. Each task will have an corresponding extension method. + * + * @author Andriy Dmytruk + * @since 1.0.x + */ +@Documented +@Retention(CLASS) +@Target({ ElementType.TYPE }) +public @interface GenerateGradlePlugin { + + /** + * The prefix to use for all names. + * For example if the prefix is {@code Test}, task will be generated as {@code TestTask}. + * The default is the annotated class name. + * + * @return The prefix + */ + String namePrefix() default ""; + + /** + * Configure gradle tasks that will be generated. + * + * @return gradle task configurations + */ + GenerateGradleTask[] tasks(); + + /** + * The types of classes to generate. + * By default, all are generated. + * + * @return The plugin types to generate. + */ + Type[] types() default { + Type.GRADLE_TASK, Type.GRADLE_EXTENSION, Type.GRADLE_SPECIFICATION, Type.GRADLE_PLUGIN + }; + + /** + * @return The gradle task group that will be set for all tasks + */ + String taskGroup() default ""; + + /** + * @return Whether to extend the micronaut plugin + */ + boolean micronautPlugin() default true; + + /** + * The coordinate of dependency to add, like + * {@code io.micronaut.jsonschema:micronaut-jsonschema-generator}. + * + * @return The dependency + */ + String dependency() default ""; + + /** + * Enum defining the types that could be generated. + */ + enum Type { + GRADLE_TASK, + GRADLE_SPECIFICATION, + GRADLE_EXTENSION, + GRADLE_PLUGIN + } + + /** + * A configuration for generating a gradle task as part of the plugin. + */ + @interface GenerateGradleTask { + + /** + * @return The prefix to use for task name. + */ + String namePrefix() default ""; + + /** + * @return The task configuration class name that has {@link PluginTask} annotation + */ + String source(); + + /** + * @return The name to use for the generated extension + */ + String extensionMethodName() default ""; + + /** + * @return Whether the task is cacheable + */ + boolean cacheable() default true; + + } + + +} diff --git a/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/GenerateMavenMojo.java b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/GenerateMavenMojo.java new file mode 100644 index 0000000..adda180 --- /dev/null +++ b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/GenerateMavenMojo.java @@ -0,0 +1,80 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * An annotation that triggers the generation of Maven Mojo. + * A plugin can include multiple Mojos. + * + * @author Andriy Dmytruk + * @since 1.0.x + */ +@Documented +@Retention(CLASS) +@Target({ ElementType.TYPE }) +@Repeatable(GenerateMavenMojo.List.class) +public @interface GenerateMavenMojo { + + /** + * The prefix to use for the mojo name. + * For example if the prefix is {@code Test}, mojo will be generated as {@code TestMojo}. + * The default is the annotated class name. + * + * @return The prefix + */ + String namePrefix() default ""; + + /** + * @return The task configuration class name that has {@link PluginTask} annotation + */ + String source(); + + /** + * @return Whether to extend abstract micronaut mojo. + */ + boolean micronautPlugin() default true; + + /** + * The property prefix to use for parameters generated in Maven Mojo. + * + * @see PluginTaskParameter#globalProperty() + * @return The property prefix + */ + String mavenPropertyPrefix() default ""; + + /** + * A container for repeated MavenMojo. + */ + @Documented + @Retention(CLASS) + @Target({ ElementType.TYPE }) + @interface List { + + /** + * @return Repeated annotations + */ + GenerateMavenMojo[] value(); + } + +} diff --git a/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTask.java b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTask.java new file mode 100644 index 0000000..131de29 --- /dev/null +++ b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTask.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * An annotation that configures a plugin task. + * The annotation is used for generation of particular plugin implementations, like Maven + * Mojos or Gradle Tasks. + * + * @author Andriy Dmytruk + * @since 1.0.x + */ +@Documented +@Retention(CLASS) +@Target({ ElementType.TYPE }) +public @interface PluginTask { + +} diff --git a/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTaskExecutable.java b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTaskExecutable.java new file mode 100644 index 0000000..f30f87f --- /dev/null +++ b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTaskExecutable.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * An annotation that configures the executable method to run for a plugin task. + * Should be inside a type annotated with {@link PluginTask}. + * + * @author Andriy Dmytruk + * @since 1.0.x + */ +@Documented +@Retention(CLASS) +@Target({ ElementType.METHOD }) +public @interface PluginTaskExecutable { + +} diff --git a/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTaskParameter.java b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTaskParameter.java new file mode 100644 index 0000000..3f692dd --- /dev/null +++ b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTaskParameter.java @@ -0,0 +1,117 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * An annotation that configures a parameter for plugin task. + * Should be inside a type annotated with {@link PluginTask}. + * + *

The annotation is used during generation of particular plugin implementations, like Maven + * Mojos or Gradle Tasks.

+ * + *

Java primitives, strings, lists, maps, enums and simple records/POJOs are supported.

+ * + * @author Andriy Dmytruk + * @since 1.0.x + */ +@Documented +@Retention(CLASS) +@Target({ ElementType.FIELD }) +public @interface PluginTaskParameter { + + /** + * Whether the parameter is required. + * By default, parameters are not required to ensure a simple plugin API. + * Parameters that have a default value should not be required. + * + * @return Whether it is required + */ + boolean required() default false; + + /** + * The default value. + * Is allowed only for Java primitives or enums. + * + * @return The default value + */ + String defaultValue() default ""; + + /** + * Whether the parameter is plugin-internal. + * This means that the parameter won't get exposed as part of plugin API. + * Specific logic will be written by developer in plugin to set the value for this parameter. + * This is useful for parameters that depend on plugin-specific logic, like getting the + * build directory. + * + * @return Whether it is internal + */ + boolean internal() default false; + + /** + * The global property name. + * For maven Mojo it will correspond to {@code @Parameter(property='')} value. + * It has no current effect for Gradle. + * + * @return The property name + */ + String globalProperty() default ""; + + /** + * Whether the file is a directory. + * Will only work for parameters of type {@link java.io.File}. + * + * @return Whether it is a directory. + */ + boolean directory() default false; + + /** + * Whether the parameter is output of the task. + * Most likely, the parameter is a file or directory. + * + * @return Whether it is output + */ + boolean output() default false; + + /** + * @return Path sensitivity to use for file parameters. This would reflect on how + * task executions are cached. If the path is considered equal, task won't be executed again. + * This has no effect for Maven. + */ + PathSensitivity pathSensitivity() default PathSensitivity.ABSOLUTE; + + /** + * Path sensitivity options. + * The specified part of the file path is used when detecting if the property has changed. + */ + enum PathSensitivity { + /** The parameter is ignored for caching. **/ + NONE, + /** Only name of the file is compared. **/ + NAME_ONLY, + /** The relative path is compared. **/ + RELATIVE, + /** The absolute path is compared. **/ + ABSOLUTE + } + +} diff --git a/micronaut-build-plugin-sourcegen-bom/build.gradle b/micronaut-build-plugin-sourcegen-bom/build.gradle index c817783..c816630 100644 --- a/micronaut-build-plugin-sourcegen-bom/build.gradle +++ b/micronaut-build-plugin-sourcegen-bom/build.gradle @@ -2,3 +2,9 @@ plugins { id 'io.micronaut.build.internal.build-plugin-sourcegen-base' id "io.micronaut.build.internal.bom" } + +micronautBuild { + binaryCompatibility { + enabled = false + } +} diff --git a/micronaut-build-plugin-sourcegen-generator/build.gradle.kts b/micronaut-build-plugin-sourcegen-generator/build.gradle.kts new file mode 100644 index 0000000..e7f92fd --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("io.micronaut.build.internal.build-plugin-sourcegen-module") +} + +dependencies { + api(mnSourcegen.micronaut.sourcegen.model) + implementation(mnSourcegen.micronaut.sourcegen.generator) + api(mn.micronaut.core.processor) + implementation(projects.micronautBuildPluginSourcegenAnnotations) + + testImplementation(mnSourcegen.micronaut.sourcegen.annotations) + testImplementation(mn.micronaut.inject.java.test) + testImplementation(mnSourcegen.micronaut.sourcegen.generator.java) + + testImplementation(libs.gradle.plugins.api) { + exclude( "org.codehaus.groovy", "groovy") + exclude( "org.codehaus.groovy", "groovy-all") + } + testImplementation(libs.maven.plugin.annotations) + testImplementation(libs.maven.plugin.api) +} + diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/JavadocUtils.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/JavadocUtils.java new file mode 100644 index 0000000..c6d0e81 --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/JavadocUtils.java @@ -0,0 +1,184 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.javadoc.Javadoc; +import com.github.javaparser.javadoc.JavadocBlockTag; +import com.github.javaparser.javadoc.JavadocBlockTag.Type; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.inject.visitor.VisitorContext; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * A utility class for javadoc. + * Since processed task might be in a dependency, it helps with writing it javadoc info + * in a META-INF file and reading it from the file. + */ +@Internal +public class JavadocUtils { + + public static final String META_INF_FOLDER = "micronaut-plugin-gen/"; + public static final String META_INF_EXTENSION = ".javadoc.txt"; + + /** + * Get the javadoc for a task. Task class element may be in a dependency. + * It will read the {@code .javadoc.txt} file written by plugin task visitor. + * + * @param context The visitor context + * @param element The element annotated with {@link io.micronaut.sourcegen.annotations.PluginTask}. + * @return The javadoc + */ + public static @NonNull TypeJavadoc getTaskJavadoc(VisitorContext context, ClassElement element) { + String javadocMetaPath = "META-INF/" + META_INF_FOLDER + element.getName() + META_INF_EXTENSION; + ClassLoader classLoader = JavadocUtils.class.getClassLoader(); + + String javadoc = null; + Map elements = new LinkedHashMap<>(); + try (InputStream inputStream = classLoader.getResourceAsStream(javadocMetaPath)) { + if (inputStream != null) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + String line = reader.readLine(); + if (line != null) { + javadoc = parseJavadocInfo(line); + } + while ((line = reader.readLine()) != null) { + int i = line.indexOf(' '); + if (i > 0) { + elements.put(line.substring(0, i), line.substring(i + 1)); + } + } + } + } else { + context.warn("Could not find javadoc META-INF file " + javadocMetaPath + " for type", element); + } + } catch (IOException e) { + throw new ProcessingException(element, "Could not read javadoc META-INF file " + javadocMetaPath + " for type" , e); + } + + if (javadoc == null && elements.isEmpty()) { + return getSourceJavadoc(element); + } + if (javadoc != null && javadoc.isEmpty()) { + javadoc = null; + } + return new TypeJavadoc( + Optional.ofNullable(javadoc), + elements + ); + } + + /** + * Write javadoc meta info that could be parsed from a file. + * + * @param element The source element. + * @return The info + */ + public static String writeJavadocInfo(ClassElement element) { + StringBuilder result = new StringBuilder(); + + TypeJavadoc javadoc = getSourceJavadoc(element); + result.append(formatJavadocInfo(javadoc.javadoc.orElse(""))) + .append('\n'); + for (Entry entry: javadoc.elements.entrySet()) { + result.append(entry.getKey()) + .append(' ') + .append(formatJavadocInfo(entry.getValue())) + .append('\n'); + } + return result.toString(); + } + + private static @NonNull TypeJavadoc getSourceJavadoc(ClassElement element) { + Javadoc parsed = StaticJavaParser.parseJavadoc(element.getDocumentation().orElse("")); + String javadoc = parsed.getDescription().toText(); + Map elements = new LinkedHashMap<>(); + + for (JavadocBlockTag tag: parsed.getBlockTags()) { + if (tag.getType() == Type.PARAM) { + elements.put(tag.getName().orElse(null), tag.getContent().toText() + "."); + } + } + for (PropertyElement property: element.getBeanProperties()) { + Optional propertyDoc = property.getDocumentation(); + Optional fieldDoc = property.getField().flatMap(Element::getDocumentation); + if (propertyDoc.isPresent()) { + elements.put(property.getName(), propertyDoc.get()); + } else if (fieldDoc.isPresent()) { + elements.put(property.getName(), fieldDoc.get()); + } + } + for (MethodElement method: element.getMethods()) { + Optional methodDoc = method.getDocumentation(); + if (methodDoc.isPresent()) { + String key = method.getName() + Arrays.stream(method.getParameters()).map(p -> p.getType().getName()) + .collect(Collectors.joining(",")) + "()"; + elements.put(key, methodDoc.get()); + } + } + if (javadoc.isEmpty()) { + javadoc = null; + } + return new TypeJavadoc( + Optional.ofNullable(javadoc), + elements + ); + } + + private static String formatJavadocInfo(String value) { + if (value == null) { + return ""; + } + return value + .replaceAll("\n\\s+", "\n") + .replace("\\", "\\\\") + .replace("\n", "\\n"); + } + + private static String parseJavadocInfo(String line) { + return line.replaceAll("(? javadoc, + Map elements + ) { + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/ModelUtils.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/ModelUtils.java new file mode 100644 index 0000000..7e06927 --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/ModelUtils.java @@ -0,0 +1,352 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.EnumConstantElement; +import io.micronaut.inject.ast.EnumElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.sourcegen.generator.visitors.JavadocUtils.TypeJavadoc; +import io.micronaut.sourcegen.model.ClassDef; +import io.micronaut.sourcegen.model.ClassDef.ClassDefBuilder; +import io.micronaut.sourcegen.model.ClassTypeDef; +import io.micronaut.sourcegen.model.EnumDef; +import io.micronaut.sourcegen.model.EnumDef.EnumDefBuilder; +import io.micronaut.sourcegen.model.ExpressionDef; +import io.micronaut.sourcegen.model.ExpressionDef.ComparisonOperation.OpType; +import io.micronaut.sourcegen.model.ExpressionDef.MathBinaryOperation; +import io.micronaut.sourcegen.model.MethodDef; +import io.micronaut.sourcegen.model.ObjectDef; +import io.micronaut.sourcegen.model.ParameterDef; +import io.micronaut.sourcegen.model.PropertyDef; +import io.micronaut.sourcegen.model.StatementDef; +import io.micronaut.sourcegen.model.TypeDef; +import io.micronaut.sourcegen.model.VariableDef; +import io.micronaut.sourcegen.model.VariableDef.Local; + +import javax.lang.model.element.Modifier; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * A utility class for working with complex types, like enums and POJOs. + */ +@Internal +public class ModelUtils { + + private static final String CONVERT_METHOD_PREFIX = "convert"; + + /** + * A utility method for getting a parameter type. + * Complex types, like enums and POJOs get copied and re-mapped. + * + * @param context the Context + * @param packageName The package name to use for new created models + * @param element The element + * @param objects A mutable list that can be extended with new objects + * @return The type + */ + public static TypeDef getType( + VisitorContext context, String packageName, ClassElement element, List objects + ) { + Optional exists = objects.stream() + .filter(v -> v.source().getName().equals(element.getName())) + .findFirst(); + if (exists.isPresent()) { + return exists.get().type(); + } + if (element.isEnum()) { + context.info("Copying plugin model for enum: " + element.getName()); + return copyEnum(context, packageName, element, objects); + } + if (isPOJO(element)) { + context.info("Copying plugin model for POJO: " + element.getName()); + return copyPOJO(context, packageName, element, objects); + } + Map typeArgs = element.getTypeArguments(); + if (element.isAssignable(Collection.class) && typeArgs.containsKey("E") && isModel(typeArgs.get("E")) + ) { + return TypeDef.parameterized(ClassTypeDef.of(element.getType()), getType( + context, packageName, typeArgs.get("E"), objects + )); + } + return TypeDef.of(element); + } + + /** + * Copy an existing enum to the plugin generated sources. + * + * @param context The visitor context + * @param packageName The package name to copy to + * @param element The element to copy + * @param objects A mutable list of objects, where enum will be added to + * @return The type of copied enum + */ + private static ClassTypeDef copyEnum( + VisitorContext context, String packageName, ClassElement element, List objects + ) { + EnumDefBuilder enumDefBuilder = EnumDef.builder(packageName + "." + getSimpleName(element)) + .addModifiers(Modifier.PUBLIC) + .addJavadoc(JavadocUtils.getTaskJavadoc(context, element).javadoc().orElse(element.getName() + " enum.")); + if (element instanceof EnumElement enumElement) { + for (EnumConstantElement constant: enumElement.elements()) { + enumDefBuilder.addEnumConstant(constant.getName()); + } + } + EnumDef enumDef = enumDefBuilder.build(); + ClassTypeDef type = enumDef.asTypeDef(); + objects.add(new GeneratedModel(enumDef, element, convertEnumMethod(type, element), type)); + return type; + } + + /** + * Copy an existing POJO to the plugin generated sources. + * + * @param context The visitor context + * @param packageName The package name to copy to + * @param element The element to copy + * @param objects A mutable list of objects, where enum will be added to + * @return The type of copied POJO + */ + private static TypeDef copyPOJO( + VisitorContext context, String packageName, ClassElement element, List objects + ) { + TypeJavadoc javadoc = JavadocUtils.getTaskJavadoc(context, element); + ClassDefBuilder classDefBuilder = ClassDef.builder(packageName + "." + getSimpleName(element)) + .addModifiers(Modifier.PUBLIC) + .addJavadoc(javadoc.javadoc().orElse(element.getName() + " class.")) + .addSuperinterface(TypeDef.of(Serializable.class)); + List properties = new ArrayList<>(); + for (PropertyElement property: element.getBeanProperties()) { + String propertyDoc = javadoc.elements().containsKey(property.getName()) ? + javadoc.elements().get(property.getName()) : + property.getName() + " property."; + TypeDef type = getType(context, packageName, property.getType(), objects); + PropertyDef propertyDef = PropertyDef.builder(property.getName()) + .addModifiers(Modifier.PUBLIC) + .ofType(type) + .addJavadoc(propertyDoc) + .build(); + properties.add(propertyDef); + classDefBuilder.addProperty(propertyDef); + } + for (int i = 0; i < properties.size(); i++) { + classDefBuilder.addMethod(createWither( + properties, javadoc.elements().get(properties.get(i).getName()), i + )); + } + ClassDef classDef = classDefBuilder + .addAllFieldsConstructor(Modifier.PUBLIC) + .addConstructor(Collections.emptyList(), Modifier.PUBLIC) + .build(); + ClassTypeDef type = classDef.asTypeDef(); + objects.add(new GeneratedModel(classDef, element, convertPOJOMethod(type, element), type)); + return type; + } + + /** + * Converts a parameter value if required. + * Conversion is required if the value is a model, so a new type was generated for it + * instead of the original one. + * + * @param type The type + * @param name The name to use for local variable + * @param statements The modifiable statements to which a local variable may be added if needed + * @param paramExpression The current expression for param + * @return The new expression for param + */ + public static ExpressionDef convertParameterIfRequired( + ClassElement type, String name, List statements, ExpressionDef paramExpression + ) { + if (isModel(type)) { + ClassTypeDef requiredType = ClassTypeDef.of(type); + VariableDef.Local param = new VariableDef.Local(name, requiredType); + String simpleName = getSimpleName(type); + statements.add(param.defineAndAssign(new VariableDef.This() + .invoke(CONVERT_METHOD_PREFIX + simpleName, requiredType, paramExpression))); + return param; + } + Map typeArgs = type.getTypeArguments(); + if (type.isAssignable(Collection.class) && typeArgs.containsKey("E") && isModel(typeArgs.get("E"))) { + Local param = new VariableDef.Local(name, ClassTypeDef.of(type)); + return convertCollectionParameter(param, typeArgs.get("E"), param, statements, type.isAssignable(Set.class)); + } + return paramExpression; + } + + private static ExpressionDef convertCollectionParameter( + Local localVar, ClassElement arg, ExpressionDef paramExpression, List statements, boolean isSet + ) { + String simpleName = getSimpleName(arg); + statements.add(localVar.defineAndAssign(ExpressionDef.constant(null))); + List innerStatements = new ArrayList<>(); + innerStatements.add(localVar.assign( + isSet ? ClassTypeDef.of(HashSet.class).instantiate() + : ClassTypeDef.of(ArrayList.class).instantiate() + )); + Local i = new VariableDef.Local("i", TypeDef.primitive(int.class)); + innerStatements.add(i.defineAndAssign(ExpressionDef.constant(0))); + innerStatements.add(new StatementDef.While( + i.compare(OpType.LESS_THAN, paramExpression.invoke("size", TypeDef.primitive(int.class))), + StatementDef.multi( + localVar.invoke("add", TypeDef.VOID, + new VariableDef.This().invoke(CONVERT_METHOD_PREFIX + simpleName, TypeDef.of(arg), + paramExpression.invoke("get", TypeDef.OBJECT, i) + ) + ), + i.assign(i.math(MathBinaryOperation.OpType.ADDITION, ExpressionDef.constant(1))) + ) + )); + statements.add(new StatementDef.If(paramExpression.compare(OpType.NOT_EQUAL_TO, ExpressionDef.constant(null)), StatementDef.multi(innerStatements))); + return localVar; + } + + /** + * Create a wither method. + * + * @param properties The properties + * @param javadoc The javadoc for property + * @param index The property index + * @return The wither method + */ + private static MethodDef createWither(List properties, @Nullable String javadoc, int index) { + PropertyDef property = properties.get(index); + return MethodDef.builder("with" + NameUtils.capitalize(property.getName())) + .addJavadoc("Create a copy and set " + property.getName() + "." + + (javadoc != null ? "\n" + javadoc : "") + ) + .addParameter(ParameterDef.of(property.getName(), property.getType())) + .addModifiers(Modifier.PUBLIC) + .returns(TypeDef.THIS) + .build((t, params) -> { + List constructorValues = new ArrayList<>(); + for (int j = 0; j < properties.size(); j++) { + if (index == j) { + constructorValues.add(params.get(0)); + } else { + constructorValues.add(t.field( + properties.get(j).getName(), properties.get(j).getType() + )); + } + } + return new StatementDef.Return(TypeDef.THIS.instantiate(constructorValues)); + }); + } + + private static MethodDef convertEnumMethod(TypeDef type, ClassElement requiredType) { + String simpleName = getSimpleName(requiredType); + ClassTypeDef outputType = ClassTypeDef.of(requiredType); + return MethodDef.builder(CONVERT_METHOD_PREFIX + simpleName) + .returns(TypeDef.of(requiredType)) + .addParameter("value", type) + .build((t, params) -> new StatementDef.IfElse( + params.get(0).isNull(), + ExpressionDef.constant(null).returning(), + outputType.invokeStatic("valueOf", outputType, + params.get(0).invoke("name", TypeDef.STRING) + ).returning() + )); + } + + private static MethodDef convertPOJOMethod(TypeDef type, ClassElement requiredType) { + String simpleName = getSimpleName(requiredType); + return MethodDef.builder(CONVERT_METHOD_PREFIX + simpleName) + .returns(TypeDef.of(requiredType)) + .addParameter("value", type) + .build((t, params) -> { + List statements = new ArrayList<>(); + Map args = new HashMap<>(); + for (PropertyElement property: requiredType.getBeanProperties()) { + args.put(property.getName(), convertParameterIfRequired( + property.getType(), + NameUtils.capitalize(property.getName()) + "Param", + statements, + params.get(0).invoke("get" + NameUtils.capitalize(property.getName()), TypeDef.OBJECT) + )); + } + Local result = PluginUtils.instantiateType(requiredType, "result", args, statements); + statements.add(result.returning()); + + return new StatementDef.IfElse( + params.get(0).isNull(), + ExpressionDef.constant(null).returning(), + StatementDef.multi(statements) + ); + }); + } + + private static String getSimpleName(ClassElement element) { + String simpleName = element.getSimpleName(); + int index = simpleName.indexOf("$"); + if (index >= 0) { + simpleName = simpleName.substring(index + 1); + } + return simpleName; + } + + /** + * Whether it is considered a POJO. + * + * @param element The type + * @return Whether it is POJO + */ + public static boolean isPOJO(ClassElement element) { + return !element.isEnum() + && !element.isPrimitive() + && !element.getPackageName().equals("java.util") + && !element.getPackageName().equals("java.lang") + && !element.getPackageName().equals("java.io"); + } + + /** + * Whether the type is a model, in which case it will be copied. + * + * @param type The type + * @return Whether it is a model + */ + public static boolean isModel(ClassElement type) { + return type.isEnum() || isPOJO(type); + } + + /** + * A record for holding the generated model. + * + * @param model The generated model object def + * @param source The source of the model + * @param convertorMethod The method that converts model to source + * @param type The type of the mode + */ + public record GeneratedModel( + ObjectDef model, + ClassElement source, + MethodDef convertorMethod, + TypeDef type + ) { + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/PluginTaskConfigValidatingVisitor.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/PluginTaskConfigValidatingVisitor.java new file mode 100644 index 0000000..3d5fbbb --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/PluginTaskConfigValidatingVisitor.java @@ -0,0 +1,100 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.sourcegen.annotations.PluginTask; + +import java.util.HashSet; +import java.util.Set; + +/** + * The visitor that validates a PluginTaskConfig annotated type. + * It also creates a META-INF file with javadoc that will be used for plugin generation. + * + * @author Andriy Dmytruk + * @since 1.0.x + */ +@Internal +public final class PluginTaskConfigValidatingVisitor implements TypeElementVisitor { + + private final Set processed = new HashSet<>(); + + @Override + public @NonNull VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + + @Override + public void start(VisitorContext visitorContext) { + processed.clear(); + } + + @Override + public Set getSupportedAnnotationNames() { + return Set.of(PluginTask.class.getName()); + } + + @Override + public void visitClass(ClassElement element, VisitorContext context) { + if (processed.contains(element.getName())) { + return; + } + + // Verify that method is present + PluginUtils.getTaskExecutable(element); + + writeJavaDocForType(context, element); + } + + private void writeJavaDocForType(VisitorContext context, ClassElement element) { + writeJavaDocMetaInfFile(element, context); + + for (PropertyElement property: element.getBeanProperties()) { + ClassElement propertyType = property.getType(); + if (processed.contains(propertyType.getName())) { + continue; + } + if (!propertyType.isEnum() || !ModelUtils.isPOJO(propertyType)) { + continue; + } + processed.add(propertyType.getName()); + writeJavaDocForType(context, propertyType); + } + } + + private void writeJavaDocMetaInfFile(ClassElement element, VisitorContext context) { + context.info("Writing javadoc META-INF file for " + element.getName()); + String fileName = JavadocUtils.META_INF_FOLDER + element.getName() + JavadocUtils.META_INF_EXTENSION; + context.visitMetaInfFile(fileName, element) + .ifPresent(generatedFile -> { + try { + generatedFile.write(writer -> + writer.write(JavadocUtils.writeJavadocInfo(element)) + ); + } catch (Exception e) { + throw new ProcessingException(element, "Failed to generate '" + fileName + "': " + e.getMessage(), e); + } + }); + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/PluginUtils.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/PluginUtils.java new file mode 100644 index 0000000..c6f02be --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/PluginUtils.java @@ -0,0 +1,231 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.sourcegen.annotations.PluginTaskExecutable; +import io.micronaut.sourcegen.annotations.PluginTaskParameter; +import io.micronaut.sourcegen.annotations.PluginTaskParameter.PathSensitivity; +import io.micronaut.sourcegen.model.ClassTypeDef; +import io.micronaut.sourcegen.model.ExpressionDef; +import io.micronaut.sourcegen.model.StatementDef; +import io.micronaut.sourcegen.model.TypeDef; +import io.micronaut.sourcegen.model.VariableDef; +import io.micronaut.sourcegen.model.VariableDef.Local; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Common utility methods for plugin generation. + */ +@Internal +public class PluginUtils { + + /** + * Validate and get the method name of the task executable. + * + * @param source The source element annotated with {@link io.micronaut.sourcegen.annotations.PluginTask}. + * @return The method name + */ + public static MethodElement getTaskExecutable(ClassElement source) { + List executables = source.getMethods().stream() + .filter(m -> m.hasAnnotation(PluginTaskExecutable.class)) + .toList(); + + if (executables.size() != 1) { + throw new ProcessingException(source, "Expected exactly one method annotated with @PluginTaskExecutable but found " + executables.size()); + } + if (executables.get(0).getParameters().length != 0) { + throw new ProcessingException(source, "Expected @PluginTaskExecutable method to have no parameters"); + } + if (!executables.get(0).getReturnType().isVoid()) { + throw new ProcessingException(source, "Expected @PluginTaskExecutable to have void return type"); + } + return executables.get(0); + } + + /** + * Get configuration for a plugin parameter. + * + * @param sourceJavadoc The javadoc for the task type + * @param property The property representing the parameter + * @param type The type to use for parameter + * @return THe configuration + */ + public static @NonNull ParameterConfig getParameterConfig( + @NonNull JavadocUtils.TypeJavadoc sourceJavadoc, @NonNull PropertyElement property, @Nullable TypeDef type + ) { + AnnotationValue annotation = property.getAnnotation(PluginTaskParameter.class); + String javadoc = sourceJavadoc.elements().get(property.getName()); + if (javadoc == null) { + javadoc = "Configurable " + property.getName() + " parameter."; + } + if (type == null) { + type = TypeDef.of(property.getType()); + } + if (annotation == null) { + return new ParameterConfig(property, false, null, false, false, false, null, javadoc, type, PathSensitivity.ABSOLUTE); + } + return new ParameterConfig( + property, + annotation.booleanValue("required").orElse(false), + annotation.stringValue("defaultValue").orElse(null), + annotation.booleanValue("internal").orElse(false), + annotation.booleanValue("directory").orElse(false), + annotation.booleanValue("output").orElse(false), + annotation.stringValue("globalProperty").orElse(null), + javadoc, + type, + annotation.enumValue("pathSensitivity", PathSensitivity.class).orElse(PathSensitivity.ABSOLUTE) + ); + } + + /** + * Instantiate a type. + * + * @param element The class element + * @param name The name to use for the local variable + * @param arguments The arguments provided for type creation corresponding to properties + * @param statements A mutable statements list + * @return The local variable representing the instantiated type + */ + public static VariableDef.Local instantiateType( + ClassElement element, String name, Map arguments, List statements + ) { + ClassTypeDef taskType = ClassTypeDef.of(element); + Local local = new Local(name, taskType); + + MethodElement constructor = element.getPrimaryConstructor().orElse(null); + if (constructor == null) { + throw new ProcessingException(element, "No constructor found for " + element.getName()); + } + if (element.isRecord()) { + List argsSorted = element.getBeanProperties().stream() + .map(prop -> arguments.get(prop.getName())) + .toList(); + statements.add(new StatementDef.DefineAndAssign(local, + taskType.instantiate(constructor, argsSorted) + )); + } else { + Set fulfilledArgs = new HashSet<>(); + + constructor.getParameters(); + List constructorArgs = new ArrayList<>(); + for (ParameterElement param : constructor.getParameters()) { + fulfilledArgs.add(param.getName()); + constructorArgs.add(arguments.containsKey(param.getName()) + ? arguments.get(param.getName()) : ExpressionDef.constant(null)); + } + statements.add(local.defineAndAssign( + taskType.instantiate(constructor, constructorArgs))); + + for (PropertyElement property : element.getBeanProperties()) { + setProperty(property, fulfilledArgs, arguments, local, statements); + } + } + return local; + } + + /** + * Set a property. + * + * @param property The property + * @param fulfilledArgs The values that were fulfilled already + * @param arguments The argument values + * @param owner The owner to set property to + * @param statements The modifiable statements + */ + private static void setProperty( + PropertyElement property, Set fulfilledArgs, + Map arguments, Local owner, List statements + ) { + if (fulfilledArgs.contains(property.getName()) + || !arguments.containsKey(property.getName()) + ) { + return; + } + Optional writeMethod = property.getWriteMethod(); + Optional field = property.getField(); + if (writeMethod.isPresent()) { + statements.add(owner.invoke(writeMethod.get(), arguments.get(property.getName()))); + fulfilledArgs.add(property.getName()); + } else if (field.isPresent() + && (property.isPublic() || property.isPackagePrivate()) + ) { + statements.add(owner.field(field.get()).assign(arguments.get(property.getName()))); + fulfilledArgs.add(property.getName()); + } + } + + /** + * A common method for executing the main task executable. + * + * @param source The source annotated with {@link io.micronaut.sourcegen.annotations.PluginTask} + * @param methodName The name of the method annotated with {@link PluginTaskExecutable} + * @param arguments The prepared arguments for the task by name + * @return The statements to execute the task method + */ + public static StatementDef executeTaskMethod( + ClassElement source, String methodName, Map arguments + ) { + List statements = new ArrayList<>(); + Local task = instantiateType(source, "task", arguments, statements); + statements.add(task.invoke(methodName, TypeDef.VOID)); + return StatementDef.multi(statements); + } + + /** + * Configuration for a plugin parameter. + * + * @param source The source parameter + * @param required Whether it is required + * @param defaultValue The default value + * @param internal Whether it is internal + * @param directory Whether it is a directory + * @param output Whether it is an output + * @param globalProperty A global property + * @param javadoc The javadoc for property + * @param type The type to use for generated property + * @param pathSensitivity The path sensitivity + */ + public record ParameterConfig( + @NonNull PropertyElement source, + boolean required, + @Nullable String defaultValue, + boolean internal, + boolean directory, + boolean output, + @Nullable String globalProperty, + @NonNull String javadoc, + @NonNull TypeDef type, + @NonNull PathSensitivity pathSensitivity + ) { + } +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/GradlePluginGenerationTriggerAnnotationVisitor.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/GradlePluginGenerationTriggerAnnotationVisitor.java new file mode 100644 index 0000000..d2f6702 --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/GradlePluginGenerationTriggerAnnotationVisitor.java @@ -0,0 +1,131 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors.gradle; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.sourcegen.annotations.GenerateGradlePlugin; +import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.Type; +import io.micronaut.sourcegen.generator.SourceGenerator; +import io.micronaut.sourcegen.generator.SourceGenerators; +import io.micronaut.sourcegen.generator.visitors.ModelUtils.GeneratedModel; +import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradlePluginConfig; +import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradleTaskConfig; +import io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleExtensionBuilder; +import io.micronaut.sourcegen.generator.visitors.gradle.builder.GradlePluginBuilder; +import io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleSpecificationBuilder; +import io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleTaskBuilder; +import io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleTypeBuilder; +import io.micronaut.sourcegen.model.ObjectDef; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * The visitor for generating Gradle plugins. + * + * @author Andriy Dmytruk + * @since 1.0.x + */ +@Internal +public final class GradlePluginGenerationTriggerAnnotationVisitor implements TypeElementVisitor { + + private static final List BUILDERS = List.of( + new GradleTaskBuilder(), + new GradleExtensionBuilder(), + new GradleSpecificationBuilder(), + new GradlePluginBuilder() + ); + + private final Set generated = new HashSet<>(); + private final Set processed = new HashSet<>(); + + @Override + public @NonNull VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + + @Override + public void start(VisitorContext visitorContext) { + processed.clear(); + } + + @Override + public Set getSupportedAnnotationNames() { + return Set.of(GenerateGradlePlugin.class.getName()); + } + + @Override + public void visitClass(ClassElement element, VisitorContext context) { + context.info("Creating plugin classes"); + if (processed.contains(element.getName()) || !element.hasAnnotation(GenerateGradlePlugin.class)) { + return; + } + try { + List definitions = createDefinitions(context, element); + SourceGenerator sourceGenerator = SourceGenerators.findByLanguage(context.getLanguage()).orElse(null); + if (sourceGenerator == null) { + throw new ProcessingException(element, "Could not find SourceGenerator for language " + context.getLanguage()); + } + processed.add(element.getName()); + for (ObjectDef definition : definitions) { + if (generated.contains(definition.getName())) { + continue; + } + generated.add(definition.getName()); + sourceGenerator.write(definition, context, element); + } + } catch (ProcessingException e) { + throw e; + } catch (Exception e) { + SourceGenerators.handleFatalException(element, GenerateGradlePlugin.class, e, + (exception -> { + processed.remove(element.getName()); + throw exception; + }) + ); + } + } + + private List createDefinitions(VisitorContext context, ClassElement element) { + List definitions = new ArrayList<>(); + GradlePluginConfig pluginConfig = GradlePluginUtils.getPluginConfig(element, context); + for (GradleTaskConfig taskConfig : pluginConfig.tasks()) { + definitions.addAll(taskConfig.generatedModels().stream().map(GeneratedModel::model).toList()); + } + for (Type type : pluginConfig.types()) { + List typeDefinitions = null; + for (GradleTypeBuilder gradleTypeBuilder : BUILDERS) { + if (gradleTypeBuilder.getType().equals(type)) { + typeDefinitions = gradleTypeBuilder.build(pluginConfig); + } + } + if (typeDefinitions == null) { + throw new ProcessingException(element, "Building plugin sources of type " + type + " not supported!"); + } + definitions.addAll(typeDefinitions); + } + + return definitions; + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/GradlePluginUtils.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/GradlePluginUtils.java new file mode 100644 index 0000000..3e7e9fc --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/GradlePluginUtils.java @@ -0,0 +1,166 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors.gradle; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.sourcegen.annotations.GenerateGradlePlugin; +import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.GenerateGradleTask; +import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.Type; +import io.micronaut.sourcegen.generator.visitors.JavadocUtils; +import io.micronaut.sourcegen.generator.visitors.JavadocUtils.TypeJavadoc; +import io.micronaut.sourcegen.generator.visitors.ModelUtils; +import io.micronaut.sourcegen.generator.visitors.ModelUtils.GeneratedModel; +import io.micronaut.sourcegen.generator.visitors.PluginUtils; +import io.micronaut.sourcegen.generator.visitors.PluginUtils.ParameterConfig; +import io.micronaut.sourcegen.model.TypeDef; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Utility class for Gradle plugin generation. + */ +@Internal +public final class GradlePluginUtils { + + /** + * Get task configurations configured for a given element + * with {@link GenerateGradlePlugin} annotation. + * + * @param element The element + * @param context The visitor context + * @return The maven task config + */ + public static @NonNull GradlePluginConfig getPluginConfig( + @NonNull ClassElement element, + @NonNull VisitorContext context + ) { + AnnotationValue annotation = element.getAnnotation(GenerateGradlePlugin.class); + + List taskConfigs = new ArrayList<>(); + for (AnnotationValue taskAnn: + annotation.getAnnotations("tasks", GenerateGradleTask.class) + ) { + taskConfigs.add(getTaskConfig(element, taskAnn, context)); + } + + return new GradlePluginConfig( + taskConfigs, + element.getPackageName(), + annotation.stringValue("namePrefix").orElse(element.getSimpleName()), + annotation.stringValue("taskGroup").orElse(null), + annotation.booleanValue("micronautPlugin").orElse(true), + annotation.stringValue("dependency").orElse(null), + Arrays.stream(annotation.getRequiredValue("types", Type[].class)).toList() + ); + } + + private static @NonNull GradleTaskConfig getTaskConfig( + @NonNull ClassElement element, + @NonNull AnnotationValue annotation, + @NonNull VisitorContext context + ) { + ClassElement source = annotation.stringValue("source") + .flatMap(context::getClassElement).orElse(null); + if (source == null) { + throw new ProcessingException(element, "Could not load source type defined in @GenerateGradleTask: " + + annotation.stringValue("source")); + } + + List generatedModels = new ArrayList<>(); + TypeJavadoc javadoc = JavadocUtils.getTaskJavadoc(context, source); + List parameters = new ArrayList<>(); + for (PropertyElement property: source.getBeanProperties()) { + TypeDef type = ModelUtils.getType(context, element.getPackageName() + ".model", + property.getType(), generatedModels); + parameters.add(PluginUtils.getParameterConfig(javadoc, property, type)); + } + + String namePrefix = annotation.stringValue("namePrefix").orElse(source.getSimpleName()); + String methodName = PluginUtils.getTaskExecutable(source).getName(); + String methodJavadoc = javadoc.elements().get(methodName + "()"); + if (methodJavadoc == null) { + methodJavadoc = "Main execution of " + namePrefix + " task."; + } + return new GradleTaskConfig( + source, + parameters, + methodName, + namePrefix, + annotation.stringValue("extensionMethodName").orElse(methodName), + javadoc.javadoc().orElse(namePrefix + " Gradle task."), + methodJavadoc, + generatedModels, + annotation.booleanValue("cacheable").orElse(true) + ); + } + + /** + * Configuration for a gradle plugin. + * + * @param tasks The task configuration + * @param packageName The package name + * @param namePrefix The type name prefix + * @param taskGroup The gradle group to use + * @param micronautPlugin Whether to extend micronaut plugin + * @param dependency The dependency + * @param types The types to generate + */ + public record GradlePluginConfig( + List tasks, + String packageName, + String namePrefix, + String taskGroup, + boolean micronautPlugin, + String dependency, + List types + ) { + } + + /** + * Configuration for a gradle task. + * + * @param source The source element + * @param parameters The parameters + * @param methodName The run method name + * @param namePrefix The prefix to use for classnames + * @param extensionMethodName The method name for gradle extension + * @param methodJavadoc The javadoc for executable method + * @param taskJavadoc The javadoc for the whole task + * @param generatedModels Additional generated models + * @param cacheable Whether the task should be cacheable + */ + public record GradleTaskConfig ( + @NonNull ClassElement source, + @NonNull List parameters, + @NonNull String methodName, + @NonNull String namePrefix, + @NonNull String extensionMethodName, + @NonNull String taskJavadoc, + @NonNull String methodJavadoc, + @NonNull List generatedModels, + boolean cacheable + ) { + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleExtensionBuilder.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleExtensionBuilder.java new file mode 100644 index 0000000..ce24c3d --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleExtensionBuilder.java @@ -0,0 +1,286 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors.gradle.builder; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.Type; +import io.micronaut.sourcegen.generator.visitors.PluginUtils.ParameterConfig; +import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradlePluginConfig; +import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradleTaskConfig; +import io.micronaut.sourcegen.model.ClassDef; +import io.micronaut.sourcegen.model.ClassDef.ClassDefBuilder; +import io.micronaut.sourcegen.model.ClassTypeDef; +import io.micronaut.sourcegen.model.ExpressionDef; +import io.micronaut.sourcegen.model.FieldDef; +import io.micronaut.sourcegen.model.InterfaceDef; +import io.micronaut.sourcegen.model.InterfaceDef.InterfaceDefBuilder; +import io.micronaut.sourcegen.model.MethodDef; +import io.micronaut.sourcegen.model.ObjectDef; +import io.micronaut.sourcegen.model.ParameterDef; +import io.micronaut.sourcegen.model.StatementDef; +import io.micronaut.sourcegen.model.TypeDef; +import io.micronaut.sourcegen.model.VariableDef; +import io.micronaut.sourcegen.model.VariableDef.Local; +import io.micronaut.sourcegen.model.VariableDef.MethodParameter; + +import javax.lang.model.element.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleTaskBuilder.TASK_SUFFIX; +import static io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleTaskBuilder.createGradleProperty; + +/** + * A builder for {@link Type#GRADLE_EXTENSION}. + * Creates a Gradle extension for calling a gradle task with the specification. + */ +@Internal +public class GradleExtensionBuilder implements GradleTypeBuilder { + + public static final String EXTENSION_NAME_SUFFIX = "Extension"; + public static final String DEFAULT_EXTENSION_NAME_PREFIX = "Default"; + public static final String TASK_CONFIGURATOR_SUFFIX = "TaskConfigurator"; + + private static final String PROJECT_FIELD = "project"; + private static final String CLASSPATH_FIELD = "classpath"; + private static final TypeDef PROJECT_TYPE = TypeDef.of("org.gradle.api.Project"); + private static final TypeDef CONFIGURATION_TYPE = TypeDef.of("org.gradle.api.artifacts.Configuration"); + private static final ClassTypeDef ACTION_TYPE = ClassTypeDef.of("org.gradle.api.Action"); + + @Override + public Type getType() { + return Type.GRADLE_EXTENSION; + } + + @Override + @NonNull + public List build(GradlePluginConfig pluginConfig) { + return List.of( + buildExtensionInterface(pluginConfig), + buildDefaultExtension(pluginConfig) + ); + } + + private ObjectDef buildExtensionInterface(GradlePluginConfig pluginConfig) { + InterfaceDefBuilder builder = InterfaceDef.builder(pluginConfig.packageName() + "." + pluginConfig.namePrefix() + EXTENSION_NAME_SUFFIX) + .addModifiers(Modifier.PUBLIC) + .addJavadoc("Configures the " + pluginConfig.namePrefix() + " execution."); + + for (GradleTaskConfig taskConfig: pluginConfig.tasks()) { + ClassTypeDef specificationType = ClassTypeDef.of(pluginConfig.packageName() + + "." + taskConfig.namePrefix() + GradleSpecificationBuilder.SPECIFICATION_NAME_SUFFIX); + ClassTypeDef actionType = TypeDef.parameterized( + ACTION_TYPE, TypeDef.wildcardSupertypeOf(specificationType)); + + builder.addMethod(MethodDef.builder(taskConfig.extensionMethodName()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addParameter("name", String.class) + .addParameter(ParameterDef.builder("action", actionType).build()) + .addJavadoc("Create a task for " + taskConfig.extensionMethodName() + "." + + "\n" + taskConfig.methodJavadoc() + + "\n@param name The unique identifier used to derive task names" + + "\n@param spec The configurable specification" + ) + .build() + ); + } + return builder.build(); + } + + private ObjectDef buildDefaultExtension(GradlePluginConfig pluginConfig) { + TypeDef interfaceType = TypeDef.of(pluginConfig.packageName() + "." + pluginConfig.namePrefix() + EXTENSION_NAME_SUFFIX); + + ClassDefBuilder builder = ClassDef.builder(pluginConfig.packageName() + + "." + DEFAULT_EXTENSION_NAME_PREFIX + pluginConfig.namePrefix() + EXTENSION_NAME_SUFFIX) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addSuperinterface(interfaceType) + .addField( + FieldDef.builder("names", TypeDef.parameterized(Set.class, String.class)) + .addModifiers(Modifier.PROTECTED, Modifier.FINAL) + .initializer(ClassTypeDef.of(HashSet.class).instantiate()) + .build() + ) + .addField(FieldDef.builder(PROJECT_FIELD, PROJECT_TYPE) + .addModifiers(Modifier.PROTECTED, Modifier.FINAL).build()) + .addField(FieldDef.builder(CLASSPATH_FIELD, CONFIGURATION_TYPE) + .addModifiers(Modifier.PROTECTED, Modifier.FINAL).build()); + + builder.addMethod(MethodDef.builder(MethodDef.CONSTRUCTOR) + .addModifiers(Modifier.PUBLIC) + .addAnnotation("javax.inject.Inject") + .addParameter(PROJECT_FIELD, PROJECT_TYPE) + .addParameter(CLASSPATH_FIELD, CONFIGURATION_TYPE) + .build((t, params) -> + StatementDef.multi( + t.field(PROJECT_FIELD, PROJECT_TYPE).assign(params.get(0)), + t.field(CLASSPATH_FIELD, CONFIGURATION_TYPE).assign(params.get(1)) + ) + )); + + for (GradleTaskConfig taskConfig: pluginConfig.tasks()) { + ClassTypeDef specificationType = ClassTypeDef.of(pluginConfig.packageName() + + "." + taskConfig.namePrefix() + GradleSpecificationBuilder.SPECIFICATION_NAME_SUFFIX); + ClassTypeDef actionType = TypeDef.parameterized( + ACTION_TYPE, TypeDef.wildcardSupertypeOf(specificationType)); + + builder.addMethod(MethodDef.builder(taskConfig.extensionMethodName()) + .overrides() + .addModifiers(Modifier.PUBLIC) + .addParameter("name", String.class) + .addParameter(ParameterDef.builder("action", actionType).build()) + .build((t, params) -> buildExtensionMethod(t, params, pluginConfig, taskConfig, specificationType)) + ); + builder.addMethod(buildCreateTaskMethod(pluginConfig, taskConfig)); + builder.addMethod(MethodDef.builder("configureSpec") + .addModifiers(Modifier.PROTECTED) + .addParameter("spec", specificationType) + .build((t, params) -> buildConfigureSpecMethod(taskConfig, params)) + ); + + builder.addInnerType(buildTaskConfigurator(pluginConfig, taskConfig, specificationType)); + } + + return builder.build(); + } + + private ClassDef buildTaskConfigurator( + GradlePluginConfig pluginConfig, GradleTaskConfig taskConfig, TypeDef specificationType + ) { + ClassTypeDef taskType = ClassTypeDef.of(pluginConfig.packageName() + "." + taskConfig.namePrefix() + TASK_SUFFIX); + FieldDef specField = FieldDef.builder("spec", specificationType).build(); + FieldDef classpathField = FieldDef.builder(CLASSPATH_FIELD, CONFIGURATION_TYPE).build(); + + MethodDef execute = MethodDef.builder("execute") + .addParameter(taskType) + .overrides() + .addModifiers(Modifier.PUBLIC) + .build((t, params) -> { + List statements = new ArrayList<>(); + MethodParameter task = params.get(0); + if (pluginConfig.taskGroup() != null) { + statements.add(task.invoke("setGroup", TypeDef.VOID, ExpressionDef.constant(pluginConfig.taskGroup()))); + } + statements.add(task.invoke("getClasspath", ClassTypeDef.of("org.gradle.api.file.ConfigurableFileLocation")) + .invoke("from", TypeDef.VOID, t.field(classpathField)) + ); + statements.add(task.invoke("setDescription", TypeDef.VOID, + ExpressionDef.constant("Configure the " + taskConfig.extensionMethodName()))); + for (ParameterConfig parameter: taskConfig.parameters()) { + String getterName = "get" + NameUtils.capitalize(parameter.source().getName()); + TypeDef getterType = createGradleProperty(parameter); + if (!parameter.internal()) { + StatementDef convention = task + .invoke(getterName, getterType) + .invoke("convention", getterType, t.field(specField).invoke(getterName, getterType)); + statements.add(convention); + } + } + return StatementDef.multi(statements); + }); + return ClassDef.builder(taskConfig.namePrefix() + TASK_CONFIGURATOR_SUFFIX) + .addModifiers(Modifier.STATIC, Modifier.PROTECTED) + .addSuperinterface(TypeDef.parameterized(ACTION_TYPE, taskType)) + .addField(specField) + .addField(classpathField) + .addAllFieldsConstructor() + .addMethod(execute) + .build(); + } + + private MethodDef buildCreateTaskMethod(GradlePluginConfig pluginConfig, GradleTaskConfig taskConfig) { + ClassTypeDef taskType = ClassTypeDef.of(pluginConfig.packageName() + "." + taskConfig.namePrefix() + TASK_SUFFIX); + TypeDef taskProviderType = TypeDef.parameterized(ClassTypeDef.of("org.gradle.api.tasks.TaskProvider"), TypeDef.wildcardSubtypeOf(taskType)); + TypeDef taskContainerType = TypeDef.of("org.gradle.api.tasks.TaskContainer"); + TypeDef pluginConfiguratorType = TypeDef.parameterized(ACTION_TYPE, taskType); + + return MethodDef.builder("create" + taskConfig.namePrefix() + "Task") + .returns(taskProviderType) + .addParameter("name", String.class) + .addParameter("configurator", pluginConfiguratorType) + .build((t, params) -> + t.field(PROJECT_FIELD, PROJECT_TYPE) + .invoke("getTasks", taskContainerType) + .invoke("register", taskProviderType, + params.get(0), + taskType.getStaticField("class", TypeDef.CLASS), + params.get(1) + ).returning() + ); + } + + private StatementDef buildConfigureSpecMethod(GradleTaskConfig taskConfig, List params) { + List statements = new ArrayList<>(); + for (ParameterConfig parameter: taskConfig.parameters()) { + String getterName = "get" + NameUtils.capitalize(parameter.source().getName()); + TypeDef getterType = createGradleProperty(parameter); + if (parameter.defaultValue() != null && !parameter.internal()) { + TypeDef type = parameter.type(); + StatementDef convention = params.get(0) + .invoke(getterName, getterType) + .invoke("convention", getterType, GradleTaskBuilder.createDefault(type, parameter.defaultValue())); + statements.add(convention); + } + } + return StatementDef.multi(statements); + } + + private StatementDef buildExtensionMethod( + VariableDef t, List params, GradlePluginConfig pluginConfig, GradleTaskConfig taskConfig, ClassTypeDef specificationType + ) { + StatementDef ifStatement = new StatementDef.If( + t.field("names", TypeDef.of(String.class)).invoke("add", TypeDef.of(boolean.class), params.get(0)).isFalse(), + new StatementDef.Throw(ClassTypeDef.of("org.gradle.api.GradleException") + .instantiate(TypeDef.STRING.invokeStatic("format", TypeDef.STRING, + ExpressionDef.constant("An " + taskConfig.extensionMethodName() + " definition with name '%s' was already created"), + params.get(0)) + ) + ) + ); + TypeDef objectFactoryType = TypeDef.of("org.gradle.api.type.ObjectFactory"); + Local spec = new Local("spec", specificationType); + StatementDef specCreation = new StatementDef.DefineAndAssign( + spec, + t.field(PROJECT_FIELD, PROJECT_TYPE) + .invoke("getObjects", objectFactoryType) + .invoke("newInstance", specificationType, specificationType.getStaticField("class", TypeDef.CLASS)) + ); + StatementDef configureSpec = t.invoke("configureSpec", TypeDef.VOID, spec); + StatementDef actionCall = params.get(1).invoke("execute", TypeDef.VOID, spec); + + ClassTypeDef taskType = ClassTypeDef.of(pluginConfig.packageName() + "." + taskConfig.namePrefix() + TASK_SUFFIX); + TypeDef taskProviderType = TypeDef.parameterized(ClassTypeDef.of("org.gradle.api.tasks.TaskProvider"), TypeDef.wildcardSubtypeOf(taskType)); + ExpressionDef pluginConfigurator = ClassTypeDef.of(taskConfig.namePrefix() + TASK_CONFIGURATOR_SUFFIX) + .instantiate(spec, t.field(CLASSPATH_FIELD, CONFIGURATION_TYPE)); + Local task = new Local("task", taskProviderType); + StatementDef taskCreation = new StatementDef.DefineAndAssign( + task, + t.invoke("create" + taskConfig.namePrefix() + "Task", taskProviderType, params.get(0), pluginConfigurator) + ); + // TODO source sets + return StatementDef.multi( + ifStatement, + specCreation, + configureSpec, + actionCall, + taskCreation + ); + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradlePluginBuilder.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradlePluginBuilder.java new file mode 100644 index 0000000..fe3c9f6 --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradlePluginBuilder.java @@ -0,0 +1,149 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors.gradle.builder; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.Type; +import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradlePluginConfig; +import io.micronaut.sourcegen.model.ClassDef; +import io.micronaut.sourcegen.model.ClassDef.ClassDefBuilder; +import io.micronaut.sourcegen.model.ClassTypeDef; +import io.micronaut.sourcegen.model.ExpressionDef; +import io.micronaut.sourcegen.model.FieldDef; +import io.micronaut.sourcegen.model.MethodDef; +import io.micronaut.sourcegen.model.ObjectDef; +import io.micronaut.sourcegen.model.StatementDef; +import io.micronaut.sourcegen.model.StatementDef.DefineAndAssign; +import io.micronaut.sourcegen.model.TypeDef; +import io.micronaut.sourcegen.model.VariableDef.Local; + +import javax.lang.model.element.Modifier; +import java.util.ArrayList; +import java.util.List; + +import static io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleExtensionBuilder.DEFAULT_EXTENSION_NAME_PREFIX; +import static io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleExtensionBuilder.EXTENSION_NAME_SUFFIX; + +/** + * A builder for {@link Type#GRADLE_PLUGIN}. + * Creates a plugin that configures an extension and task. + */ +@Internal +public class GradlePluginBuilder implements GradleTypeBuilder { + + public static final String PLUGIN_SUFFIX = "Plugin"; + + private static final String MICRONAUT_BASE_PLUGIN = "io.micronaut.gradle.MicronautBasePlugin"; + private static final String MICRONAUT_PLUGINS_HELPER = "io.micronaut.gradle.PluginsHelper"; + private static final String CREATE_METHOD = "create"; + private static final ClassTypeDef PROJECT_TYPE = ClassTypeDef.of("org.gradle.api.Project"); + private static final ClassTypeDef CONFIGURATION_TYPE = ClassTypeDef.of("org.gradle.api.artifacts.Configuration"); + private static final FieldDef CLASS_STATIC_FIELD = FieldDef.builder("class", TypeDef.CLASS).build(); + + @Override + public Type getType() { + return Type.GRADLE_PLUGIN; + } + + @Override + @NonNull + public List build(GradlePluginConfig pluginConfig) { + String pluginType = pluginConfig.packageName() + "." + pluginConfig.namePrefix() + PLUGIN_SUFFIX; + ClassDefBuilder builder = ClassDef.builder(pluginType) + .addModifiers(Modifier.PUBLIC) + .addSuperinterface(TypeDef.parameterized( + ClassTypeDef.of("org.gradle.api.Plugin"), + PROJECT_TYPE + )); + builder.addMethod(createExtensionMethod(pluginConfig)); + builder.addMethod(createApplyMethod(pluginConfig)); + return List.of(builder.build()); + } + + private MethodDef createApplyMethod(GradlePluginConfig pluginConfig) { + ClassTypeDef extensionType = ClassTypeDef.of(pluginConfig.packageName() + "." + + pluginConfig.namePrefix() + EXTENSION_NAME_SUFFIX); + + return MethodDef.builder("apply") + .addModifiers(Modifier.PUBLIC) + .addParameter("project", ClassTypeDef.of("org.gradle.api.Project")) + .build((t, params) -> { + List statements = new ArrayList<>(); + if (pluginConfig.micronautPlugin()) { + params.get(0) + .invoke("getPluginManager", ClassTypeDef.of("org.gradle.api.plugins.PluginManager")) + .invoke("apply", TypeDef.VOID, ClassTypeDef.of(MICRONAUT_BASE_PLUGIN).getStaticField(CLASS_STATIC_FIELD)); + } + ExpressionDef configurations = params.get(0).invoke("getConfigurations", TypeDef.of("org.gradle.api.artifacts.ConfigurationContainer")); + ExpressionDef dependencyHandler = params.get(0).invoke("getDependencies", TypeDef.of("org.gradle.api.artifacts.dsl.DependencyHandler")); + TypeDef dependencyType = TypeDef.of("org.gradle.api.artifacts.Dependency"); + + Local dependencies = new Local("dependencies", CONFIGURATION_TYPE); + statements.add(new DefineAndAssign( + dependencies, + configurations.invoke(CREATE_METHOD, CONFIGURATION_TYPE, ExpressionDef.constant(pluginConfig.namePrefix() + "Configuration")) + )); + statements.add(dependencies.invoke("setCanBeResolved", TypeDef.VOID, ExpressionDef.constant(false))); + statements.add(dependencies.invoke("setCanBeConsumed", TypeDef.VOID, ExpressionDef.constant(false))); + statements.add(dependencies.invoke("setDescription", TypeDef.VOID, ExpressionDef.constant("The " + pluginConfig.namePrefix() + " worker dependencies"))); + if (pluginConfig.dependency() != null) { + statements.add(dependencies.invoke("getDependencies", TypeDef.of("org.gradle.api.artifacts.DependencySet")) + .invoke("add", TypeDef.VOID, dependencyHandler.invoke(CREATE_METHOD, dependencyType, ExpressionDef.constant(pluginConfig.dependency())))); + } + + Local classpath = new Local("classpath", CONFIGURATION_TYPE); + statements.add(new DefineAndAssign( + classpath, + configurations.invoke(CREATE_METHOD, CONFIGURATION_TYPE, ExpressionDef.constant(pluginConfig.namePrefix() + "Classpath")) + )); + statements.add(classpath.invoke("setCanBeResolved", TypeDef.VOID, ExpressionDef.constant(true))); + statements.add(classpath.invoke("setCanBeConsumed", TypeDef.VOID, ExpressionDef.constant(false))); + statements.add(classpath.invoke("setDescription", TypeDef.VOID, ExpressionDef.constant("The " + pluginConfig.namePrefix() + " worker classpath"))); + statements.add(classpath.invoke("extendsFrom", TypeDef.VOID, dependencies)); + + statements.add(t.invoke("createExtension", extensionType, params.get(0), classpath)); + return StatementDef.multi(statements); + }); + } + + private MethodDef createExtensionMethod(GradlePluginConfig pluginConfig) { + ClassTypeDef extensionType = ClassTypeDef.of(pluginConfig.packageName() + "." + pluginConfig.namePrefix() + EXTENSION_NAME_SUFFIX); + ClassTypeDef defaultExtensionType = ClassTypeDef.of(pluginConfig.packageName() + "." + DEFAULT_EXTENSION_NAME_PREFIX + pluginConfig.namePrefix() + EXTENSION_NAME_SUFFIX); + + return MethodDef.builder("createExtension") + .addModifiers(Modifier.PROTECTED) + .returns(extensionType) + .addParameter("project", PROJECT_TYPE) + .addParameter("classpath", CONFIGURATION_TYPE) + .build((t, params) -> { + ExpressionDef root = params.get(0); + if (pluginConfig.micronautPlugin()) { + root = ClassTypeDef.of(MICRONAUT_PLUGINS_HELPER) + .invokeStatic("findMicronautExtension", TypeDef.of("io.micronaut.gradle.MicronautExtension"), params.get(0)); + } + ExpressionDef extensions = root.invoke("getExtensions", ClassTypeDef.of("org.gradle.api.plugins.ExtensionContainer")); + return new StatementDef.Return(extensions.invoke(CREATE_METHOD, extensionType, + extensionType.getStaticField(CLASS_STATIC_FIELD), + ExpressionDef.constant(pluginConfig.namePrefix()), + defaultExtensionType.getStaticField(CLASS_STATIC_FIELD), + params.get(0), + params.get(1) + )); + }); + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleSpecificationBuilder.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleSpecificationBuilder.java new file mode 100644 index 0000000..35a5a6e --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleSpecificationBuilder.java @@ -0,0 +1,80 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors.gradle.builder; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.Type; +import io.micronaut.sourcegen.generator.visitors.PluginUtils.ParameterConfig; +import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradlePluginConfig; +import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradleTaskConfig; +import io.micronaut.sourcegen.model.InterfaceDef; +import io.micronaut.sourcegen.model.InterfaceDef.InterfaceDefBuilder; +import io.micronaut.sourcegen.model.MethodDef; +import io.micronaut.sourcegen.model.MethodDef.MethodDefBuilder; +import io.micronaut.sourcegen.model.ObjectDef; + +import javax.lang.model.element.Modifier; +import java.util.ArrayList; +import java.util.List; + +import static io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleTaskBuilder.createGradleProperty; + +/** + * A builder for {@link Type#GRADLE_SPECIFICATION}. + * Creates a Gradle specification for configuring a gradle task. + */ +@Internal +public class GradleSpecificationBuilder implements GradleTypeBuilder { + + public static final String SPECIFICATION_NAME_SUFFIX = "Spec"; + + @Override + public Type getType() { + return Type.GRADLE_SPECIFICATION; + } + + @Override + @NonNull + public List build(GradlePluginConfig pluginConfig) { + List objects = new ArrayList<>(); + for (GradleTaskConfig taskConfig: pluginConfig.tasks()) { + objects.add(buildForTask(pluginConfig.packageName(), taskConfig)); + } + return objects; + } + + private ObjectDef buildForTask(String packageName, GradleTaskConfig taskConfig) { + InterfaceDefBuilder builder = InterfaceDef.builder(packageName + "." + taskConfig.namePrefix() + SPECIFICATION_NAME_SUFFIX) + .addModifiers(Modifier.PUBLIC) + .addJavadoc("Specification that is used for configuring " + taskConfig.namePrefix() + " task.\n" + + taskConfig.taskJavadoc()); + for (ParameterConfig parameter: taskConfig.parameters()) { + if (parameter.internal()) { + continue; + } + MethodDefBuilder propBuilder = MethodDef + .builder("get" + NameUtils.capitalize(parameter.source().getName())) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addJavadoc(parameter.javadoc()) + .returns(createGradleProperty(parameter)); + builder.addMethod(propBuilder.build()); + } + return builder.build(); + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleTaskBuilder.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleTaskBuilder.java new file mode 100644 index 0000000..ede0bea --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleTaskBuilder.java @@ -0,0 +1,360 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors.gradle.builder; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.core.reflect.ClassUtils; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.sourcegen.annotations.GenerateGradlePlugin; +import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.Type; +import io.micronaut.sourcegen.generator.visitors.ModelUtils; +import io.micronaut.sourcegen.generator.visitors.ModelUtils.GeneratedModel; +import io.micronaut.sourcegen.generator.visitors.PluginUtils; +import io.micronaut.sourcegen.generator.visitors.PluginUtils.ParameterConfig; +import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradlePluginConfig; +import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradleTaskConfig; +import io.micronaut.sourcegen.model.AnnotationDef; +import io.micronaut.sourcegen.model.ClassDef; +import io.micronaut.sourcegen.model.ClassDef.ClassDefBuilder; +import io.micronaut.sourcegen.model.ClassTypeDef; +import io.micronaut.sourcegen.model.ClassTypeDef.ClassDefType; +import io.micronaut.sourcegen.model.ClassTypeDef.ClassElementType; +import io.micronaut.sourcegen.model.EnumDef; +import io.micronaut.sourcegen.model.ExpressionDef; +import io.micronaut.sourcegen.model.ExpressionDef.Constant; +import io.micronaut.sourcegen.model.FieldDef; +import io.micronaut.sourcegen.model.InterfaceDef; +import io.micronaut.sourcegen.model.InterfaceDef.InterfaceDefBuilder; +import io.micronaut.sourcegen.model.MethodDef; +import io.micronaut.sourcegen.model.MethodDef.MethodDefBuilder; +import io.micronaut.sourcegen.model.ObjectDef; +import io.micronaut.sourcegen.model.ParameterDef; +import io.micronaut.sourcegen.model.StatementDef; +import io.micronaut.sourcegen.model.TypeDef; +import io.micronaut.sourcegen.model.VariableDef; + +import javax.lang.model.element.Modifier; +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A builder for {@link GenerateGradlePlugin.Type#GRADLE_TASK}. + * Creates a task, work action and work action parameters given a plugin task configuration. + */ +@Internal +public class GradleTaskBuilder implements GradleTypeBuilder { + + public static final String TASK_SUFFIX = "Task"; + public static final String WORK_ACTION_SUFFIX = "WorkAction"; + public static final String WORK_ACTION_PARAMETERS_SUFFIX = "WorkActionParameters"; + + private static final String GET_CLASSPATH_METHOD = "getClasspath"; + private static final String EXECUTE_METHOD = "execute"; + + @Override + public Type getType() { + return Type.GRADLE_TASK; + } + + @Override + @NonNull + public List build(GradlePluginConfig pluginConfig) { + List objects = new ArrayList<>(); + for (GradleTaskConfig taskConfig: pluginConfig.tasks()) { + objects.addAll(buildTask(pluginConfig.packageName(), taskConfig)); + } + return objects; + } + + private List buildTask(String packageName, GradleTaskConfig taskConfig) { + String taskType = packageName + "." + taskConfig.namePrefix() + TASK_SUFFIX; + ClassDefBuilder builder = ClassDef.builder(taskType) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .superclass(ClassTypeDef.of("org.gradle.api.DefaultTask")) + .addJavadoc(taskConfig.taskJavadoc()); + if (taskConfig.cacheable()) { + builder.addAnnotation("org.gradle.api.tasks.CacheableTask"); + } + builder.addInnerType(createWorkAction(taskConfig)); + builder.addInnerType(createWorkActionParameters(taskConfig)); + builder.addInnerType(createWorkActionParameterConfigurator(TypeDef.of(taskType), taskConfig)); + builder.addInnerType(createClasspathConfigurator(TypeDef.of(taskType), taskConfig)); + + for (ParameterConfig parameter: taskConfig.parameters()) { + builder.addMethod(createParameterGetter(parameter)); + } + + TypeDef classpathType = TypeDef.of("org.gradle.api.file.ConfigurableFileCollection"); + builder.addMethod(MethodDef.builder(GET_CLASSPATH_METHOD) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(classpathType) + .addAnnotation("org.gradle.api.tasks.Classpath") + .build() + ); + + TypeDef workerExecutorType = TypeDef.of("org.gradle.workers.WorkerExecutor"); + builder.addMethod(MethodDef.builder("getWorkerExecutor") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(workerExecutorType) + .addAnnotation("javax.inject.Inject") + .build() + ); + + builder.addMethod(createExecuteMethod(taskConfig, workerExecutorType)); + return List.of(builder.build()); + } + + private MethodDef createParameterGetter(ParameterConfig parameter) { + MethodDefBuilder propBuilder = MethodDef + .builder("get" + NameUtils.capitalize(parameter.source().getName())) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addJavadoc(parameter.javadoc()) + .returns(createGradleProperty(parameter)); + if (parameter.output()) { + if (parameter.source().getType().isAssignable(File.class)) { + if (parameter.directory()) { + propBuilder.addAnnotation(AnnotationDef.builder(ClassTypeDef.of("org.gradle.api.tasks.OutputDirectory")).build()); + } else { + propBuilder.addAnnotation(AnnotationDef.builder(ClassTypeDef.of("org.gradle.api.tasks.OutputFile")).build()); + } + } + } else { + propBuilder.addAnnotation("org.gradle.api.tasks.Input"); + if (parameter.source().getType().isAssignable(File.class)) { + if (parameter.directory()) { + propBuilder.addAnnotation(AnnotationDef.builder(ClassTypeDef.of("org.gradle.api.tasks.InputDirectory")).build()); + } else { + propBuilder.addAnnotation(AnnotationDef.builder(ClassTypeDef.of("org.gradle.api.tasks.InputFile")).build()); + } + ClassTypeDef pathSensitivityType = ClassTypeDef.of("org.gradle.api.tasks.PathSensitivity"); + ClassTypeDef pathSensitiveType = ClassTypeDef.of("org.gradle.api.tasks.PathSensitive"); + propBuilder.addAnnotation(AnnotationDef.builder(pathSensitiveType) + .addMember("value", pathSensitivityType.getStaticField(parameter.pathSensitivity().name(), pathSensitivityType)) + .build() + ); + } + } + + if (!parameter.required()) { + propBuilder.addAnnotation("org.gradle.api.tasks.Optional"); + } + return propBuilder.build(); + } + + private ClassDef createWorkActionParameterConfigurator(TypeDef taskType, GradleTaskConfig taskConfig) { + TypeDef parametersType = TypeDef.of(taskConfig.namePrefix() + WORK_ACTION_PARAMETERS_SUFFIX); + FieldDef taskField = FieldDef.builder("task").ofType(taskType).build(); + return ClassDef.builder(taskConfig.namePrefix() + "WorkActionParameterConfigurator") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addSuperinterface(TypeDef.parameterized( + ClassTypeDef.of("org.gradle.api.Action"), + parametersType + )) + .addField(taskField) + .addAllFieldsConstructor(Modifier.PUBLIC) + .addMethod(MethodDef.builder(EXECUTE_METHOD) + .addModifiers(Modifier.PUBLIC) + .returns(TypeDef.VOID) + .overrides() + .addParameter(ParameterDef.of("params", parametersType)) + .build((t, params) -> { + List statements = new ArrayList<>(); + for (ParameterConfig parameter: taskConfig.parameters()) { + String getterName = "get" + NameUtils.capitalize(parameter.source().getName()); + TypeDef getterType = createGradleProperty(parameter); + ExpressionDef def = t.field(taskField).invoke(getterName, getterType); + if (!parameter.required()) { + if (parameter.defaultValue() != null) { + TypeDef type = parameter.type(); + def = def.invoke( + "orElse", + type, + createDefault(type, parameter.defaultValue()) + ); + } else { + def = def.invoke("getOrNull", parameter.type()); + } + } + statements.add(params.get(0) + .invoke(getterName, getterType) + .invoke("set", TypeDef.VOID, def) + ); + } + return StatementDef.multi(statements); + }) + ) + .build(); + } + + static ExpressionDef createDefault(TypeDef type, String value) { + if (type instanceof ClassElementType classElementType) { + return ExpressionDef.constant(classElementType.classElement(), type, value); + } else if (type instanceof TypeDef.Primitive primitiveType) { + return ClassUtils.getPrimitiveType(primitiveType.name()).flatMap(t -> + ConversionService.SHARED.convert(value, t) + ).map(o -> new Constant(type, o)).orElse(null); + } else if (type instanceof ClassDefType classDefType && classDefType.objectDef() instanceof EnumDef) { + return classDefType.getStaticField(value, type); + } + throw new UnsupportedOperationException("Cannot create default value of type " + type); + } + + private ClassDef createClasspathConfigurator(TypeDef taskType, GradleTaskConfig taskConfig) { + FieldDef taskField = FieldDef.builder("task").ofType(taskType).build(); + TypeDef specType = TypeDef.of("org.gradle.workers.ClassLoaderWorkerSpec"); + TypeDef classpathType = TypeDef.of("org.gradle.api.file.ConfigurableFileCollection"); + return ClassDef.builder(taskConfig.namePrefix() + "ClasspathConfigurator") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addSuperinterface(TypeDef.parameterized( + ClassTypeDef.of("org.gradle.api.Action"), + specType + )) + .addField(taskField) + .addAllFieldsConstructor(Modifier.PUBLIC) + .addMethod(MethodDef.builder(EXECUTE_METHOD) + .addModifiers(Modifier.PUBLIC) + .returns(TypeDef.VOID) + .overrides() + .addParameter(ParameterDef.of("spec", specType)) + .build((t, params) -> + params.get(0) + .invoke(GET_CLASSPATH_METHOD, classpathType) + .invoke("from", TypeDef.VOID, t.field(taskField).invoke(GET_CLASSPATH_METHOD, classpathType)) + ) + ) + .build(); + } + + private InterfaceDef createWorkActionParameters(GradleTaskConfig taskConfig) { + InterfaceDefBuilder builder = InterfaceDef.builder(taskConfig.namePrefix() + WORK_ACTION_PARAMETERS_SUFFIX) + .addModifiers(Modifier.PUBLIC) + .addSuperinterface(ClassTypeDef.of("org.gradle.workers.WorkParameters")); + for (ParameterConfig parameter: taskConfig.parameters()) { + MethodDefBuilder propBuilder = MethodDef + .builder("get" + NameUtils.capitalize(parameter.source().getName())) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(createGradleProperty(parameter)); + builder.addMethod(propBuilder.build()); + } + return builder.build(); + } + + private ClassDef createWorkAction(GradleTaskConfig taskConfig) { + ClassTypeDef parametersType = ClassTypeDef.of(taskConfig.namePrefix() + WORK_ACTION_PARAMETERS_SUFFIX); + MethodDef executeMethod = MethodDef + .builder(EXECUTE_METHOD) + .returns(TypeDef.VOID) + .addModifiers(Modifier.PUBLIC) + .overrides() + .build((t, params) -> runTask(taskConfig, t, parametersType)); + return ClassDef.builder(taskConfig.namePrefix() + WORK_ACTION_SUFFIX) + .addSuperinterface(TypeDef.parameterized( + ClassTypeDef.of("org.gradle.workers.WorkAction"), + parametersType + )) + .addMethods(taskConfig.generatedModels().stream().map(GeneratedModel::convertorMethod).toList()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT, Modifier.STATIC) + .addMethod(executeMethod) + .build(); + } + + private MethodDef createExecuteMethod(GradleTaskConfig taskConfig, TypeDef workerExecutorType) { + return MethodDef.builder(EXECUTE_METHOD) + .returns(TypeDef.VOID) + .addModifiers(Modifier.PUBLIC) + .addAnnotation("org.gradle.api.tasks.TaskAction") + .addJavadoc(taskConfig.methodJavadoc()) + .build((t, params) -> + t.invoke("getWorkerExecutor", workerExecutorType) + .invoke("classLoaderIsolation", + workerExecutorType, + ClassTypeDef.of(taskConfig.namePrefix() + "ClasspathConfigurator").instantiate(t) + ) + .invoke("submit", TypeDef.VOID, + ClassTypeDef.of(taskConfig.namePrefix() + WORK_ACTION_SUFFIX).getStaticField("class", TypeDef.CLASS), + ClassTypeDef.of(taskConfig.namePrefix() + "WorkActionParameterConfigurator").instantiate(t) + ) + ); + } + + private StatementDef runTask(GradleTaskConfig taskConfig, VariableDef.This t, ClassTypeDef parametersType) { + List statements = new ArrayList<>(); + Map params = new HashMap<>(); + statements.add(t.invoke("getParameters", parametersType).newLocal("parameters")); + + for (ParameterConfig parameter: taskConfig.parameters()) { + ExpressionDef expression = new VariableDef.Local("parameters", parametersType) + .invoke("get" + NameUtils.capitalize(parameter.source().getName()), createGradleProperty(parameter)); + if (!parameter.required() && parameter.defaultValue() == null) { + expression = expression.invoke("getOrNull", parameter.type()); + } else { + expression = expression.invoke("get", parameter.type()); + } + if (parameter.source().getType().isAssignable(File.class)) { + expression = expression.invoke("getAsFile", TypeDef.of(File.class)); + } + params.put( + parameter.source().getName(), + ModelUtils.convertParameterIfRequired( + parameter.source().getType(), parameter.source().getName() + "Param", statements, expression + ) + ); + } + statements.add(PluginUtils.executeTaskMethod(taskConfig.source(), taskConfig.methodName(), params)); + return StatementDef.multi(statements); + } + + static TypeDef createGradleProperty(ParameterConfig parameter) { + ClassElement type = parameter.source().getType(); + if (type.isAssignable(File.class)) { + if (parameter.directory()) { + return ClassTypeDef.of("org.gradle.api.file.DirectoryProperty"); + } + return ClassTypeDef.of("org.gradle.api.file.RegularFileProperty"); + } + if (parameter.type() instanceof ClassTypeDef.Parameterized parameterized) { + if (type.isAssignable(Map.class)) { + return TypeDef.parameterized( + ClassTypeDef.of("org.gradle.api.provider.MapProperty"), + parameterized.typeArguments().get(0), + parameterized.typeArguments().get(1) + ); + } else if (type.isAssignable(List.class)) { + return TypeDef.parameterized( + ClassTypeDef.of("org.gradle.api.provider.ListProperty"), + parameterized.typeArguments().get(0) + ); + } else if (type.isAssignable(Set.class)) { + return TypeDef.parameterized( + ClassTypeDef.of("org.gradle.api.provider.SetProperty"), + parameterized.typeArguments().get(0) + ); + } + } + return TypeDef.parameterized( + ClassTypeDef.of("org.gradle.api.provider.Property"), + parameter.type() + ); + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleTypeBuilder.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleTypeBuilder.java new file mode 100644 index 0000000..c409a99 --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleTypeBuilder.java @@ -0,0 +1,47 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors.gradle.builder; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.sourcegen.annotations.GenerateGradlePlugin; +import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils; +import io.micronaut.sourcegen.model.ObjectDef; + +import java.util.List; + +/** + * An interface for a Gradle plugin builder type. + */ +@Internal +public interface GradleTypeBuilder { + + /** + * Get the gradle type it can generate. + * + * @return The type + */ + @NonNull GenerateGradlePlugin.Type getType(); + + /** + * Generate the gradle type. + * + * @param pluginConfig The configuration + * @return The generated objects for the type + */ + @NonNull List build(@NonNull GradlePluginUtils.GradlePluginConfig pluginConfig); + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenMojoBuilder.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenMojoBuilder.java new file mode 100644 index 0000000..ec585a9 --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenMojoBuilder.java @@ -0,0 +1,150 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors.maven; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.sourcegen.generator.visitors.ModelUtils; +import io.micronaut.sourcegen.generator.visitors.ModelUtils.GeneratedModel; +import io.micronaut.sourcegen.generator.visitors.PluginUtils; +import io.micronaut.sourcegen.generator.visitors.maven.MavenPluginUtils.MavenTaskConfig; +import io.micronaut.sourcegen.generator.visitors.PluginUtils.ParameterConfig; +import io.micronaut.sourcegen.model.AnnotationDef; +import io.micronaut.sourcegen.model.AnnotationDef.AnnotationDefBuilder; +import io.micronaut.sourcegen.model.ClassDef; +import io.micronaut.sourcegen.model.ClassDef.ClassDefBuilder; +import io.micronaut.sourcegen.model.ClassTypeDef; +import io.micronaut.sourcegen.model.ExpressionDef; +import io.micronaut.sourcegen.model.FieldDef; +import io.micronaut.sourcegen.model.MethodDef; +import io.micronaut.sourcegen.model.StatementDef; +import io.micronaut.sourcegen.model.TypeDef; +import io.micronaut.sourcegen.model.VariableDef; + +import javax.lang.model.element.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A builder for Maven Mojos. + */ +@Internal +public class MavenMojoBuilder { + + public static final String MOJO_SUFFIX = "Mojo"; + + /** + * Method for building the Maven mojo. + * + * @param taskConfig The config + * @return The class + */ + public ClassDef build(MavenTaskConfig taskConfig) { + String mojoName = taskConfig.packageName() + "." + taskConfig.namePrefix() + MOJO_SUFFIX; + ClassDefBuilder builder = ClassDef.builder(mojoName) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT); + if (taskConfig.micronautPlugin()) { + builder.superclass(ClassTypeDef.of("io.micronaut.maven.AbstractMicronautMojo")); + } else { + builder.superclass(ClassTypeDef.of("org.apache.maven.plugin.AbstractMojo")); + } + + for (ParameterConfig parameter : taskConfig.parameters()) { + addParameter(taskConfig, parameter, builder); + } + + builder.addMethod(MethodDef.builder("isEnabled") + .addModifiers(Modifier.PROTECTED, Modifier.ABSTRACT) + .returns(TypeDef.of(boolean.class)) + .addJavadoc("Determines if this mojo must be executed.\n@return true if the mojo is enabled") + .build() + ); + builder.addMethods(taskConfig.generatedModels().stream().map(GeneratedModel::convertorMethod).toList()); + builder.addMethod(createExecuteMethod(taskConfig)); + builder.addJavadoc(taskConfig.taskJavadoc()); + + return builder.build(); + } + + private void addParameter(MavenTaskConfig taskConfig, ParameterConfig parameter, ClassDefBuilder builder) { + if (parameter.internal() || parameter.output()) { + builder.addMethod(MethodDef + .builder("get" + NameUtils.capitalize(parameter.source().getName())) + .returns(parameter.type()) + .addModifiers(Modifier.PROTECTED, Modifier.ABSTRACT) + .addJavadoc(parameter.javadoc()) + .build() + ); + } else { + AnnotationDefBuilder ann = AnnotationDef.builder(ClassTypeDef.of("org.apache.maven.plugins.annotations.Parameter")); + if (parameter.defaultValue() != null) { + ann.addMember("defaultValue", parameter.defaultValue()); + } + if (parameter.required()) { + ann.addMember("required", true); + } + if (parameter.globalProperty() != null) { + ann.addMember("property", taskConfig.mavenPropertyPrefix() + + "." + MavenPluginUtils.toDotSeparated(parameter.globalProperty())); + } + FieldDef field = FieldDef.builder(parameter.source().getName()) + .ofType(parameter.type()) + .addModifiers(Modifier.PROTECTED) + .addAnnotation(ann.build()) + .addJavadoc(parameter.javadoc()) + .build(); + builder.addField(field); + } + } + + private MethodDef createExecuteMethod(MavenTaskConfig taskConfig) { + return MethodDef.builder("execute") + .overrides() + .addModifiers(Modifier.PUBLIC) + .addJavadoc(taskConfig.methodJavadoc()) + .build((t, params) -> t.invoke("isEnabled", TypeDef.of(boolean.class)) + .ifFalse( + t.invoke("getLog", ClassTypeDef.of("org.apache.maven.plugin.logging.Log")) + .invoke("debug", TypeDef.VOID, ExpressionDef.constant(taskConfig.namePrefix() + MOJO_SUFFIX + " is disabled")), + runTask(taskConfig, t) + )); + } + + private StatementDef runTask(MavenTaskConfig taskConfig, VariableDef.This t) { + Map params = new HashMap<>(); + List statements = new ArrayList<>(); + for (ParameterConfig parameter: taskConfig.parameters()) { + ExpressionDef expression; + if (parameter.internal() || parameter.output()) { + String getter = "get" + NameUtils.capitalize(parameter.source().getName()); + expression = t.invoke(getter, parameter.type()); + } else { + expression = t.field(parameter.source().getName(), parameter.type()); + } + params.put( + parameter.source().getName(), + ModelUtils.convertParameterIfRequired( + parameter.source().getType(), parameter.source().getName() + "Param", statements, expression + ) + ); + } + statements.add(PluginUtils.executeTaskMethod(taskConfig.source(), taskConfig.methodName(), params)); + return StatementDef.multi(statements); + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenMojoGenerationTriggerAnnotationVisitor.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenMojoGenerationTriggerAnnotationVisitor.java new file mode 100644 index 0000000..da57708 --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenMojoGenerationTriggerAnnotationVisitor.java @@ -0,0 +1,109 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors.maven; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.sourcegen.annotations.GenerateMavenMojo; +import io.micronaut.sourcegen.generator.SourceGenerator; +import io.micronaut.sourcegen.generator.SourceGenerators; +import io.micronaut.sourcegen.generator.visitors.ModelUtils.GeneratedModel; +import io.micronaut.sourcegen.generator.visitors.maven.MavenPluginUtils.MavenTaskConfig; +import io.micronaut.sourcegen.model.ObjectDef; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Visitor for generating maven mojos. + * + * @author Andriy Dmytruk + * @since 1.0.x + */ +@Internal +public final class MavenMojoGenerationTriggerAnnotationVisitor implements TypeElementVisitor { + + private final Set processed = new HashSet<>(); + private final Set generated = new HashSet<>(); + + @Override + public @NonNull VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + + @Override + public void start(VisitorContext visitorContext) { + processed.clear(); + } + + @Override + public Set getSupportedAnnotationNames() { + return Set.of( + GenerateMavenMojo.class.getName(), + GenerateMavenMojo.class.getName() + "$List" + ); + } + + @Override + public void visitClass(ClassElement element, VisitorContext context) { + if (!element.hasAnnotation(GenerateMavenMojo.List.class) || processed.contains(element.getName())) { + return; + } + context.info("Creating plugin classes"); + + try { + List definitions = createDefinitions(context, element); + SourceGenerator sourceGenerator = SourceGenerators.findByLanguage(context.getLanguage()).orElse(null); + if (sourceGenerator == null) { + throw new ProcessingException(element, "Could not find SourceGenerator for language " + context.getLanguage()); + } + processed.add(element.getName()); + for (ObjectDef definition : definitions) { + if (generated.contains(definition.getName())) { + continue; + } + generated.add(definition.getName()); + sourceGenerator.write(definition, context, element); + } + } catch (ProcessingException e) { + throw e; + } catch (Exception e) { + SourceGenerators.handleFatalException(element, GenerateMavenMojo.class, e, + (exception -> { + processed.remove(element.getName()); + throw exception; + }) + ); + } + } + + private List createDefinitions(VisitorContext context, ClassElement element) { + List definitions = new ArrayList<>(); + List taskConfigs = MavenPluginUtils.getTaskConfigs(element, context); + for (MavenTaskConfig taskConfig : taskConfigs) { + definitions.addAll(taskConfig.generatedModels().stream().map(GeneratedModel::model).toList()); + definitions.add(new MavenMojoBuilder().build(taskConfig)); + } + return definitions; + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenPluginUtils.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenPluginUtils.java new file mode 100644 index 0000000..0a5930d --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenPluginUtils.java @@ -0,0 +1,151 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.generator.visitors.maven; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.sourcegen.annotations.GenerateMavenMojo; +import io.micronaut.sourcegen.generator.visitors.JavadocUtils; +import io.micronaut.sourcegen.generator.visitors.JavadocUtils.TypeJavadoc; +import io.micronaut.sourcegen.generator.visitors.ModelUtils; +import io.micronaut.sourcegen.generator.visitors.ModelUtils.GeneratedModel; +import io.micronaut.sourcegen.generator.visitors.PluginUtils; +import io.micronaut.sourcegen.generator.visitors.PluginUtils.ParameterConfig; +import io.micronaut.sourcegen.model.TypeDef; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utils class for Maven plugin generation. + */ +@Internal +public final class MavenPluginUtils { + + /** + * Get task configurations configured for a given element + * with {@link GenerateMavenMojo} annotations. + * + * @param element The element + * @param context The visitor context + * @return The maven task config + */ + public static @NonNull List getTaskConfigs( + @NonNull ClassElement element, @NonNull VisitorContext context + ) { + List> annotations = + element.getAnnotationValuesByType(GenerateMavenMojo.class); + return annotations.stream().map(a -> getTaskConfig(element, a, context)).toList(); + } + + /** + * Convert to dot-separated string. + * + * @param camelCase Camel case name + * @return Dot separated name + */ + public static String toDotSeparated(String camelCase) { + StringBuilder result = new StringBuilder(); + boolean isStartOfWord = true; + for (int i = 0; i < camelCase.length(); i++) { + if (Character.isUpperCase(camelCase.charAt(i))) { + if (!isStartOfWord) { + result.append("."); + } + result.append(Character.toLowerCase(camelCase.charAt(i))); + isStartOfWord = true; + } else { + result.append(camelCase.charAt(i)); + isStartOfWord = camelCase.charAt(i) == '.'; + } + } + return result.toString(); + } + + private static @NonNull MavenTaskConfig getTaskConfig( + @NonNull ClassElement element, @NonNull AnnotationValue annotation, @NonNull VisitorContext context + ) { + ClassElement source = annotation.stringValue("source") + .flatMap(context::getClassElement).orElse(null); + if (source == null) { + throw new ProcessingException(element, "Could not load source type defined in @GenerateMavenMojo: " + + annotation.stringValue("source")); + } + + List generatedModels = new ArrayList<>(); + TypeJavadoc javadoc = JavadocUtils.getTaskJavadoc(context, source); + List parameters = new ArrayList<>(); + for (PropertyElement property: source.getBeanProperties()) { + TypeDef type = ModelUtils.getType(context, element.getPackageName() + ".model", + property.getType(), generatedModels); + parameters.add(PluginUtils.getParameterConfig(javadoc, property, type)); + } + + String namePrefix = annotation.stringValue("namePrefix").orElse(element.getSimpleName()); + String methodName = PluginUtils.getTaskExecutable(source).getName(); + String methodJavadoc = javadoc.elements().get(methodName + "()"); + if (methodJavadoc == null) { + methodJavadoc = "Main execution of " + namePrefix + " Mojo."; + } + return new MavenTaskConfig( + source, + parameters, + methodName, + element.getPackageName(), + namePrefix, + annotation.booleanValue("micronautPlugin").orElse(true), + annotation.stringValue("mavenPropertyPrefix").orElse(toDotSeparated(namePrefix)), + javadoc.javadoc().orElse(namePrefix + " Maven Mojo."), + methodJavadoc, + generatedModels + ); + } + + /** + * Configuration for a gradle task type. + * + * @param source The configuration source + * @param parameters The parameters + * @param methodName The run method name + * @param packageName The package name + * @param namePrefix The type name prefix + * @param micronautPlugin Whether to extend micronaut plugin + * @param mavenPropertyPrefix The prefix for maven properties + * @param taskJavadoc The javadoc for the whole task + * @param methodJavadoc The javadoc for the executable method + * @param generatedModels Additional generated models + */ + public record MavenTaskConfig( + ClassElement source, + List parameters, + @NonNull String methodName, + @NonNull String packageName, + @NonNull String namePrefix, + boolean micronautPlugin, + @Nullable String mavenPropertyPrefix, + @NonNull String taskJavadoc, + @NonNull String methodJavadoc, + @NonNull List generatedModels + ) { + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/micronaut-build-plugin-sourcegen-generator/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor new file mode 100644 index 0000000..1f573ee --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -0,0 +1,3 @@ +io.micronaut.sourcegen.generator.visitors.PluginTaskConfigValidatingVisitor +io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginGenerationTriggerAnnotationVisitor +io.micronaut.sourcegen.generator.visitors.maven.MavenMojoGenerationTriggerAnnotationVisitor diff --git a/micronaut-build-plugin-sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/AbstractGenerationSpec.groovy b/micronaut-build-plugin-sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/AbstractGenerationSpec.groovy new file mode 100644 index 0000000..c6af444 --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/AbstractGenerationSpec.groovy @@ -0,0 +1,47 @@ +package io.micronaut.sourcegen.generator.visitors + + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder +import org.intellij.lang.annotations.Language + +import javax.tools.JavaFileObject +import javax.tools.SimpleJavaFileObject +import java.util.stream.Collectors + +abstract class AbstractGenerationSpec extends AbstractTypeElementSpec { + + Map generateSources(String className, @Language("java") String cls) { + AbstractAnnotationMetadataBuilder.clearMutated() + try (def parser = newJavaParser()) { + var files = parser.generate(className, cls) + Map result = new HashMap<>() + for (JavaFileObject file: files) { + String name = (file as SimpleJavaFileObject).toUri().toString() + if (name.startsWith("mem:///SOURCE_OUTPUT/")) { + name = name.substring("mem:///SOURCE_OUTPUT/".length()).replace("/", ".") + if (name.endsWith(".java")) { + name = name.substring(0, name.length() - ".java".length()) + } + result.put(name, file) + } + } + return result; + } + } + + String stripImports(CharSequence value) { + String[] lines = value.toString().split("\n") + int startI = 0 + while (lines[startI].isEmpty() || lines[startI].startsWith("package") + || lines[startI].startsWith("import")) { + ++startI + } + int endI = lines.length; + while (lines[endI - 1].isEmpty()) { + --endI + } + return Arrays.stream(lines, startI, endI).collect(Collectors.joining("\n")) + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/GradlePluginGenerationSpec.groovy b/micronaut-build-plugin-sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/GradlePluginGenerationSpec.groovy new file mode 100644 index 0000000..189debc --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/GradlePluginGenerationSpec.groovy @@ -0,0 +1,209 @@ +package io.micronaut.sourcegen.generator.visitors + +class GradlePluginGenerationSpec extends AbstractGenerationSpec { + + void "test simple gradle plugin generation"() { + when: + var files = generateSources("test.Wolf", """ + package test; + import io.micronaut.sourcegen.annotations.*; + + @GenerateGradlePlugin( + micronautPlugin = false, + tasks = @GenerateGradlePlugin.GenerateGradleTask( + source = "test.Wolf" + ) + ) + @PluginTask + public record Wolf( + @PluginTaskParameter(required = true) + String slogan, + @PluginTaskParameter(defaultValue = "1") + Integer age + ) { + + @PluginTaskExecutable + public void awooo() { + } + + } + """) + + then: + var taskContent = stripImports(files.get("test.WolfTask").getCharContent(false)) + taskContent == """/** + * Wolf Gradle task. + */ +@CacheableTask +public abstract class WolfTask extends DefaultTask { + /** + * Configurable slogan parameter. + */ + @Input + public abstract Property getSlogan(); + + /** + * Configurable age parameter. + */ + @Input + @Optional + public abstract Property getAge(); + + @Classpath + public abstract ConfigurableFileCollection getClasspath(); + + @Inject + public abstract WorkerExecutor getWorkerExecutor(); + + /** + * Main execution of Wolf task. + */ + @TaskAction + public void execute() { + this.getWorkerExecutor().classLoaderIsolation(new WolfClasspathConfigurator(this)).submit(WolfWorkAction.class, new WolfWorkActionParameterConfigurator(this)); + } + + public abstract static class WolfWorkAction implements WorkAction { + public void execute() { + WolfWorkActionParameters parameters = this.getParameters(); + Wolf task = new test.Wolf(parameters.getSlogan().get(), parameters.getAge().get()); + task.awooo(); + } + } + + public interface WolfWorkActionParameters extends WorkParameters { + Property getSlogan(); + + Property getAge(); + } + + public static class WolfWorkActionParameterConfigurator implements Action { + WolfTask task; + + public WolfWorkActionParameterConfigurator(WolfTask task) { + this.task = task; + } + + public void execute(WolfWorkActionParameters params) { + params.getSlogan().set(this.task.getSlogan()); + params.getAge().set(this.task.getAge().orElse(1)); + } + } + + public static class WolfClasspathConfigurator implements Action { + WolfTask task; + + public WolfClasspathConfigurator(WolfTask task) { + this.task = task; + } + + public void execute(ClassLoaderWorkerSpec spec) { + spec.getClasspath().from(this.task.getClasspath()); + } + } +}""" + + var specContent = stripImports(files.get("test.WolfSpec").getCharContent(false)) + specContent == """/** + * Specification that is used for configuring Wolf task. + * Wolf Gradle task. + */ +public interface WolfSpec { + /** + * Configurable slogan parameter. + */ + Property getSlogan(); + + /** + * Configurable age parameter. + */ + Property getAge(); +}""" + + var extensionContent = stripImports(files.get("test.WolfExtension").getCharContent(false)) + extensionContent == """/** + * Configures the Wolf execution. + */ +public interface WolfExtension { + /** + * Create a task for awooo. + * Main execution of Wolf task. + * @param name The unique identifier used to derive task names + * @param spec The configurable specification + */ + void awooo(String name, Action action); +}""" + + var defaultExtensionContent = stripImports(files.get("test.DefaultWolfExtension").getCharContent(false)) + defaultExtensionContent == """public abstract class DefaultWolfExtension implements WolfExtension { + protected final Set names = new java.util.HashSet(); + + protected final Project project; + + protected final Configuration classpath; + + @Inject + public DefaultWolfExtension(Project project, Configuration classpath) { + this.project = project; + this.classpath = classpath; + } + + public void awooo(String name, Action action) { + if (!this.names.add(name)) { + throw new org.gradle.api.GradleException(String.format("An awooo definition with name '%s' was already created", name)); + } + WolfSpec spec = this.project.getObjects().newInstance(WolfSpec.class); + this.configureSpec(spec); + action.execute(spec); + TaskProvider task = this.createWolfTask(name, new WolfTaskConfigurator(spec, this.classpath)); + } + + TaskProvider createWolfTask(String name, Action configurator) { + return this.project.getTasks().register(name, WolfTask.class, configurator); + } + + protected void configureSpec(WolfSpec spec) { + spec.getAge().convention(1); + } + + protected static class WolfTaskConfigurator implements Action { + WolfSpec spec; + + Configuration classpath; + + WolfTaskConfigurator(WolfSpec spec, Configuration classpath) { + this.spec = spec; + this.classpath = classpath; + } + + public void execute(WolfTask arg1) { + arg1.getClasspath().from(this.classpath); + arg1.setDescription("Configure the awooo"); + arg1.getSlogan().convention(this.spec.getSlogan()); + arg1.getAge().convention(this.spec.getAge()); + } + } +}""" + + var pluginContent = stripImports(files.get("test.WolfPlugin").getCharContent(false)) + pluginContent == """public class WolfPlugin implements Plugin { + protected WolfExtension createExtension(Project project, Configuration classpath) { + return project.getExtensions().create(WolfExtension.class, "Wolf", DefaultWolfExtension.class, project, classpath); + } + + public void apply(Project project) { + Configuration dependencies = project.getConfigurations().create("WolfConfiguration"); + dependencies.setCanBeResolved(false); + dependencies.setCanBeConsumed(false); + dependencies.setDescription("The Wolf worker dependencies"); + Configuration classpath = project.getConfigurations().create("WolfClasspath"); + classpath.setCanBeResolved(true); + classpath.setCanBeConsumed(false); + classpath.setDescription("The Wolf worker classpath"); + classpath.extendsFrom(dependencies); + this.createExtension(project, classpath); + } +}""" + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/MavenPluginGenerationSpec.groovy b/micronaut-build-plugin-sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/MavenPluginGenerationSpec.groovy new file mode 100644 index 0000000..186f9ff --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/MavenPluginGenerationSpec.groovy @@ -0,0 +1,72 @@ +package io.micronaut.sourcegen.generator.visitors + +class MavenPluginGenerationSpec extends AbstractGenerationSpec { + + void "test simple maven plugin generation"() { + when: + var files = generateSources("test.Wolf", """ + package test; + import io.micronaut.sourcegen.annotations.*; + + @GenerateMavenMojo( + micronautPlugin = false, + source = "test.Wolf" + ) + @PluginTask + public record Wolf( + @PluginTaskParameter(required = true) + String slogan, + @PluginTaskParameter(defaultValue = "1") + Integer age + ) { + + @PluginTaskExecutable + public void awooo() { + } + + } + """) + + then: + var mojoContent = stripImports(files.get("test.WolfMojo").getCharContent(false)) + mojoContent == """/** + * Wolf Maven Mojo. + */ +public abstract class WolfMojo extends AbstractMojo { + /** + * Configurable slogan parameter. + */ + @Parameter( + required = true + ) + protected String slogan; + + /** + * Configurable age parameter. + */ + @Parameter( + defaultValue = "1" + ) + protected Integer age; + + /** + * Determines if this mojo must be executed. + * @return true if the mojo is enabled + */ + protected abstract boolean isEnabled(); + + /** + * Main execution of Wolf Mojo. + */ + public void execute() { + if (!this.isEnabled()) { + this.getLog().debug("WolfMojo is disabled"); + } else { + Wolf task = new test.Wolf(this.slogan, this.age); + task.awooo(); + } + } +}""" + } + +} diff --git a/micronaut-build-plugin-sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/ModelGenerationSpec.groovy b/micronaut-build-plugin-sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/ModelGenerationSpec.groovy new file mode 100644 index 0000000..7aa840a --- /dev/null +++ b/micronaut-build-plugin-sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/ModelGenerationSpec.groovy @@ -0,0 +1,234 @@ +package io.micronaut.sourcegen.generator.visitors + +class ModelGenerationSpec extends AbstractGenerationSpec { + + void "test generate with an enum model"() { + when: + var files = generateSources("test.Jaguar", """ + package test; + import io.micronaut.sourcegen.annotations.*; + + @GenerateGradlePlugin( + micronautPlugin = false, + tasks = @GenerateGradlePlugin.GenerateGradleTask( + source = "test.Jaguar" + ) + ) + @PluginTask + public record Jaguar( + @PluginTaskParameter(defaultValue = "GOLDEN") + Color color + ) { + + @PluginTaskExecutable + public void meow() { + } + + } + + /** + * An enum representing Jaguar's color. + */ + enum Color { + MELANISTIC, + ERYTHRISM, + GOLDEN, + WHITE + } + """) + + then: + var enumContent = stripImports(files.get("test.model.Color").getCharContent(false)) + enumContent == """/** + * An enum representing Jaguar's color. + */ +public enum Color { + + MELANISTIC, + ERYTHRISM, + GOLDEN, + WHITE +}""" + + var taskContent = stripImports(files.get("test.JaguarTask").getCharContent(false)) + taskContent.contains("""public abstract static class JaguarWorkAction implements WorkAction { + Color convertColor(test.model.Color value) { + if (value == (test.model.Color) (null)) { + return null; + } else { + return Color.valueOf(value.name()); + } + } + + public void execute() { + JaguarWorkActionParameters parameters = this.getParameters(); + Color colorParam = this.convertColor(parameters.getColor().get()); + Jaguar task = new test.Jaguar(colorParam); + task.meow(); + } + } +""") + } + + void "test generate with a record model"() { + when: + var files = generateSources("test.Jaguar", """ + package test; + import io.micronaut.sourcegen.annotations.*; + + @GenerateGradlePlugin( + micronautPlugin = false, + tasks = @GenerateGradlePlugin.GenerateGradleTask( + source = "test.Jaguar" + ) + ) + @PluginTask + public record Jaguar( + @PluginTaskParameter(required = true) + Tail tail + ) { + + @PluginTaskExecutable + public void meow() { + } + + } + + /** + * A record representing Jaguar's tail. + * + * @param description Detailed tail description + * @param length The length of the tail + * @param color The color + */ + record Tail( + String description, + float length, + Color color + ) { + } + + /** + * An enum representing Jaguar's color. + */ + enum Color { + MELANISTIC, + ERYTHRISM, + GOLDEN, + WHITE + } + """) + + then: + var recordContent = stripImports(files.get("test.model.Tail").getCharContent(false)) + recordContent == """/** + * A record representing Jaguar's tail. + */ +public class Tail implements Serializable { + /** + * Detailed tail description. + */ + private String description; + + /** + * The length of the tail. + */ + private float length; + + /** + * The color. + */ + private Color color; + + public Tail(String description, float length, Color color) { + this.description = description; + this.length = length; + this.color = color; + } + + public Tail() { + } + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + public float getLength() { + return this.length; + } + + public void setLength(float length) { + this.length = length; + } + + public Color getColor() { + return this.color; + } + + public void setColor(Color color) { + this.color = color; + } + + /** + * Create a copy and set description. + * Detailed tail description. + */ + public Tail withDescription(String description) { + return new test.model.Tail(description, this.length, this.color); + } + + /** + * Create a copy and set length. + * The length of the tail. + */ + public Tail withLength(float length) { + return new test.model.Tail(this.description, length, this.color); + } + + /** + * Create a copy and set color. + * The color. + */ + public Tail withColor(Color color) { + return new test.model.Tail(this.description, this.length, color); + } +}""" + + var enumContent = stripImports(files.get("test.model.Color").getCharContent(false)) + enumContent != null + + var taskContent = stripImports(files.get("test.JaguarTask").getCharContent(false)) + taskContent.contains("""public abstract static class JaguarWorkAction implements WorkAction { + Color convertColor(test.model.Color value) { + if (value == (test.model.Color) (null)) { + return null; + } else { + return Color.valueOf(value.name()); + } + } + + Tail convertTail(test.model.Tail value) { + if (value == (test.model.Tail) (null)) { + return null; + } else { + Color ColorParam = this.convertColor(value.getColor()); + Tail result = new test.Tail(value.getDescription(), value.getLength(), ColorParam); + return result; + } + } + + public void execute() { + JaguarWorkActionParameters parameters = this.getParameters(); + Tail tailParam = this.convertTail(parameters.getTail().get()); + Jaguar task = new test.Jaguar(tailParam); + task.meow(); + } + } +""") + } + +} diff --git a/micronaut-build-plugin-sourcegen/build.gradle b/micronaut-build-plugin-sourcegen/build.gradle deleted file mode 100644 index 18d4e02..0000000 --- a/micronaut-build-plugin-sourcegen/build.gradle +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id 'io.micronaut.build.internal.build-plugin-sourcegen-module' -} diff --git a/settings.gradle b/settings.gradle index f4361fd..aadb14b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,16 +6,21 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '7.3.1' + id 'io.micronaut.build.shared.settings' version '7.3.2' } -rootProject.name = 'project-template-parent' +rootProject.name = 'micronaut-build-plugin-sourcegen-parent' -include 'project-template' -include 'project-template-bom' +include 'micronaut-build-plugin-sourcegen-bom' +include 'micronaut-build-plugin-sourcegen-annotations' +include 'micronaut-build-plugin-sourcegen-generator' +include 'test-suite-common-java' +include 'test-suite-gradle-java' +include 'test-suite-maven-java' enableFeaturePreview 'TYPESAFE_PROJECT_ACCESSORS' micronautBuild { importMicronautCatalog() + importMicronautCatalog("micronaut-sourcegen") } diff --git a/src/main/docs/guide/gettingStarted.adoc b/src/main/docs/guide/gettingStarted.adoc new file mode 100644 index 0000000..7580924 --- /dev/null +++ b/src/main/docs/guide/gettingStarted.adoc @@ -0,0 +1,8 @@ +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. + diff --git a/src/main/docs/guide/gettingStarted/commonModule.adoc b/src/main/docs/guide/gettingStarted/commonModule.adoc new file mode 100644 index 0000000..9e60bbe --- /dev/null +++ b/src/main/docs/guide/gettingStarted/commonModule.adoc @@ -0,0 +1,17 @@ +The common module should have the following dependencies: + +dependency:micronaut-build-plugin-sourcegen-annotations[groupId="io.micronaut.build.plugin.sourcegen"] + +dependency:micronaut-build-plugin-sourcegen-generator[scope="annotationProcessor", groupId="io.micronaut.build.plugin.sourcegen"] + +Use the link:{api}/io/micronaut/sourcegen/annotations/PluginTask.html[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. + +snippet::io.micronaut.sourcegen.example.plugin.GenerateSimpleRecordTask[project-base="test-suite-common", tags="content", source="main"] + +<1> Define the task. The task can be a record or a Java class. +<2> Use the link:{api}/io/micronaut/sourcegen/annotations/PluginTaskParameter.html[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 = true` for outputs a the task. +<5> Use the link:{api}/io/micronaut/sourcegen/annotations/PluginTaskExecutable.html[PluginTaskExecutable] to define the executable for task. The executable will use the parameters defined for the task. + +See documentation for link:{api}/io/micronaut/sourcegen/annotations/PluginTask.html[PluginTask], link:{api}/io/micronaut/sourcegen/annotations/PluginTaskParameter.html[PluginTaskParameter] and link:{api}/io/micronaut/sourcegen/annotations/PluginTaskExecutable.html[PluginTaskExecutable] to view all the configurable properties. diff --git a/src/main/docs/guide/gettingStarted/gradleModule.adoc b/src/main/docs/guide/gettingStarted/gradleModule.adoc new file mode 100644 index 0000000..0e4f9e2 --- /dev/null +++ b/src/main/docs/guide/gettingStarted/gradleModule.adoc @@ -0,0 +1,38 @@ +The Gradle module should have the same dependencies and also the dependency on the common module. + +dependency:micronaut-build-plugin-sourcegen-annotations[groupId="io.micronaut.build.plugin.sourcegen"] + +dependency:test-plugin-common[scope="compileOnly", groupId="io.micronaut.test"] + +dependency:micronaut-build-plugin-sourcegen-generator[scope="annotationProcessor", groupId="io.micronaut.build.plugin.sourcegen"] + +dependency:test-plugin-common[scope="annotationProcessor", groupId="io.micronaut.test"] + +NOTE: Adding the common plugin to the annotation processor paths is currently required to retrieve javadoc. + +Use the link:{api}/io/micronaut/sourcegen/annotations/GenerateGradlePlugin.html[GenerateGradlePlugin] annotation to trigger generation of Gradle Plugin sources. + +snippet::io.micronaut.sourcegen.example.plugin.gradle.GeneratePluginTrigger[project-base="test-suite-gradle", tags="content", source="main"] + +<1> Specify the name prefix for all generated sources. `TestPlugin` and `TestExtension` will be generated based on this. +<2> Use the link:{api}/io/micronaut/sourcegen/annotations/GenerateGradlePlugin/GenerateGradleTask.html[GenerateGradleTask] annotation to define generation of a task. Specify the task from common module annotated with link:{api}/io/micronaut/sourcegen/annotations/PluginTask.html[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. + +See documentation for link:{api}/io/micronaut/sourcegen/annotations/GenerateGradleTask.html[GenerateGradleTask] to view all the configurable properties. + +=== Plugin Customization + +Plugin and extension can be extended to add custom Gradle-specific behavior. + +snippet::io.micronaut.sourcegen.example.plugin.gradle.TestExtensionImpl[project-base="test-suite-gradle", tags="begin,generateRecordWithName,createGenerateSimpleRecordTask,withJavaSourceSets,end", source="main"] + +<1> Extend the generated `TestExtension` class. +<2> Create a utility extension method that users could call instead. +<3> Add the generated file to sources. diff --git a/src/main/docs/guide/gettingStarted/mavenModule.adoc b/src/main/docs/guide/gettingStarted/mavenModule.adoc new file mode 100644 index 0000000..315eff4 --- /dev/null +++ b/src/main/docs/guide/gettingStarted/mavenModule.adoc @@ -0,0 +1,32 @@ +The Maven module should have the same dependencies and also the dependency on the common module. + +dependency:micronaut-build-plugin-sourcegen-annotations[groupId="io.micronaut.build.plugin.sourcegen"] + +dependency:test-plugin-common[scope="compileOnly", groupId="io.micronaut.test"] + +dependency:micronaut-build-plugin-sourcegen-generator[scope="annotationProcessor", groupId="io.micronaut.build.plugin.sourcegen"] + +dependency:test-plugin-common[scope="annotationProcessor", groupId="io.micronaut.test"] + +NOTE: Adding the common plugin to the annotation processor paths is currently required to retrieve javadoc. + +Use the link:{api}/io/micronaut/sourcegen/annotations/GenerateMavenMojo.html[GenerateMavenMojo] annotation to trigger generation of Maven Plugin sources. + +snippet::io.micronaut.sourcegen.example.plugin.maven.GenerateMojoTrigger[project-base="test-suite-maven", tags="content", source="main"] + +<1> Trigger generation of a mojo. Specify the task from common module annotated with link:{api}/io/micronaut/sourcegen/annotations/PluginTask.html[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. + +See documentation for link:{api}/io/micronaut/sourcegen/annotations/GenerateMavenMojo.html[GenerateMavenMojo] to view all the configurable properties. + +=== Mojo Customization + +Extend the Mojo to add custom Maven-specific behavior: + +snippet::io.micronaut.sourcegen.example.plugin.maven.GenerateSimpleRecordMojo[project-base="test-suite-maven", tags="content", source="main"] + +<2> Specify a name for Mojo. +<2> Add a property for enabling and disabling the mojo. +<3> Add the generated folder to sources. diff --git a/src/main/docs/guide/introduction.adoc b/src/main/docs/guide/introduction.adoc index 30404ce..ea55a5e 100644 --- a/src/main/docs/guide/introduction.adoc +++ b/src/main/docs/guide/introduction.adoc @@ -1 +1,7 @@ -TODO \ No newline at end of file +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 https://micronaut-projects.github.io/micronaut-sourcegen/latest/guide/[Micronaut sourcegen]. diff --git a/src/main/docs/guide/quickStart.adoc b/src/main/docs/guide/quickStart.adoc deleted file mode 100644 index 30404ce..0000000 --- a/src/main/docs/guide/quickStart.adoc +++ /dev/null @@ -1 +0,0 @@ -TODO \ No newline at end of file diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 55b5fcb..ba97ca6 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -1,7 +1,10 @@ introduction: title: Introduction releaseHistory: Release History -quickStart: - title: Quick Start +gettingStarted: + title: Getting Started + commonModule: Common Module + gradleModule: Gradle Module + mavenModule: Maven Module repository: Repository diff --git a/test-suite-common-java/build.gradle.kts b/test-suite-common-java/build.gradle.kts new file mode 100644 index 0000000..1a41fc3 --- /dev/null +++ b/test-suite-common-java/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("io.micronaut.build.internal.build-plugin-sourcegen-testsuite") +} + +dependencies { + annotationProcessor(mn.micronaut.inject) + annotationProcessor(mn.micronaut.inject.java) + annotationProcessor(projects.micronautBuildPluginSourcegenGenerator) + + implementation(projects.micronautBuildPluginSourcegenAnnotations) +} diff --git a/test-suite-common-java/src/main/java/io/micronaut/sourcegen/example/plugin/GenerateSimpleRecordTask.java b/test-suite-common-java/src/main/java/io/micronaut/sourcegen/example/plugin/GenerateSimpleRecordTask.java new file mode 100644 index 0000000..f6217cc --- /dev/null +++ b/test-suite-common-java/src/main/java/io/micronaut/sourcegen/example/plugin/GenerateSimpleRecordTask.java @@ -0,0 +1,109 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.example.plugin; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +// tag::content[] +import io.micronaut.sourcegen.annotations.PluginTask; +import io.micronaut.sourcegen.annotations.PluginTaskExecutable; +import io.micronaut.sourcegen.annotations.PluginTaskParameter; +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 properties, + List javadoc, + @PluginTaskParameter(output = true, directory = true, required = 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, "src" + File.separator + + "main" + File.separator + "java" + File.separator + + 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); + } + +} +// end::content[] diff --git a/test-suite-common-java/src/main/java/io/micronaut/sourcegen/example/plugin/GenerateSimpleResourceTask.java b/test-suite-common-java/src/main/java/io/micronaut/sourcegen/example/plugin/GenerateSimpleResourceTask.java new file mode 100644 index 0000000..c46a165 --- /dev/null +++ b/test-suite-common-java/src/main/java/io/micronaut/sourcegen/example/plugin/GenerateSimpleResourceTask.java @@ -0,0 +1,139 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.example.plugin; + +import io.micronaut.sourcegen.annotations.PluginTask; +import io.micronaut.sourcegen.annotations.PluginTaskExecutable; +import io.micronaut.sourcegen.annotations.PluginTaskParameter; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.logging.Logger; + +/** + * This is a configuration for another plugin task run. + * In this case it is a class instead of a record. + * The properties are parameters and the single method defines the task execution. + * The plugin generates a simple record. + */ +@PluginTask +public final class GenerateSimpleResourceTask { + + private static final Logger LOG = Logger.getLogger(GenerateSimpleResourceTask.class.getName()); + + /** + * The generated file name. + */ + @PluginTaskParameter(required = true, globalProperty = "fileName") + private String fileName; + + /** + * The content of the file. + */ + @PluginTaskParameter(required = true, globalProperty = "content") + private String content; + + /** + * The output folder. + */ + @PluginTaskParameter(output = true, directory = true, required = true) + private File outputFolder; + + /** + * How the file ends. + */ + @PluginTaskParameter(defaultValue = "NONE") + private Ending ending; + + /** + * Configure generating repeated file. + */ + @PluginTaskParameter() + private Repeat repeat; + + /** + * Generate a simple record in the supplied package and with the specified version. + * This javadoc will be copied to the respected plugin implementations. + */ + @PluginTaskExecutable + public void generateSimpleResource() { + generateOne(fileName, ending); + + if (repeat != null) { + for (int i = 0; i < repeat.number; ++i) { + generateOne(fileName + repeat.repeatSuffix + (i + 1), repeat.ending); + } + } + } + + private void generateOne(String fileName, Ending ending) { + LOG.info("Generating resource " + fileName); + + File outputFile = new File(outputFolder.getAbsolutePath() + File.separator + fileName); + outputFile.getParentFile().mkdirs(); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) { + writer.write(content + (ending == Ending.NEWLINE ? "\n" : "")); + } catch (IOException e) { + throw new RuntimeException(e); + } + + LOG.info("Finished resource " + fileName); + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public void setContent(String content) { + this.content = content; + } + + public void setOutputFolder(File outputFolder) { + this.outputFolder = outputFolder; + } + + public void setEnding(Ending ending) { + this.ending = ending; + } + + public void setRepeat(Repeat repeat) { + this.repeat = repeat; + } + + /** + * An enum representing how the file ends. + */ + public enum Ending { + NONE, + NEWLINE + } + + /** + * Configuration for repeating the file. + * + * @param number Number of repeats + * @param repeatSuffix The suffix to use + * @param ending The file ending + */ + public record Repeat( + int number, + String repeatSuffix, + Ending ending + ) { + } +} diff --git a/test-suite-gradle-java/build.gradle.kts b/test-suite-gradle-java/build.gradle.kts new file mode 100644 index 0000000..a87674c --- /dev/null +++ b/test-suite-gradle-java/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("java-gradle-plugin") + id("io.micronaut.build.internal.build-plugin-sourcegen-testsuite") +} + +repositories { + mavenCentral() +} + +dependencies { + api(projects.testSuiteCommonJava) + annotationProcessor(projects.testSuiteCommonJava) + annotationProcessor(mn.micronaut.inject) + annotationProcessor(mn.micronaut.inject.java) + annotationProcessor(projects.micronautBuildPluginSourcegenGenerator) + annotationProcessor(mnSourcegen.micronaut.sourcegen.generator.java) + implementation(projects.micronautBuildPluginSourcegenAnnotations) + + testImplementation(mnTest.micronaut.test.junit5) + testImplementation(mnTest.junit.jupiter.engine) +} + +tasks.withType { + testLogging { + showStandardStreams = true + } +} diff --git a/test-suite-gradle-java/src/main/java/io/micronaut/sourcegen/example/plugin/gradle/GeneratePluginTrigger.java b/test-suite-gradle-java/src/main/java/io/micronaut/sourcegen/example/plugin/gradle/GeneratePluginTrigger.java new file mode 100644 index 0000000..1fc5f97 --- /dev/null +++ b/test-suite-gradle-java/src/main/java/io/micronaut/sourcegen/example/plugin/gradle/GeneratePluginTrigger.java @@ -0,0 +1,40 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.example.plugin.gradle; + +// tag::content[] +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 { +} +// end::content[] diff --git a/test-suite-gradle-java/src/main/java/io/micronaut/sourcegen/example/plugin/gradle/TestExtensionImpl.java b/test-suite-gradle-java/src/main/java/io/micronaut/sourcegen/example/plugin/gradle/TestExtensionImpl.java new file mode 100644 index 0000000..b776f8c --- /dev/null +++ b/test-suite-gradle-java/src/main/java/io/micronaut/sourcegen/example/plugin/gradle/TestExtensionImpl.java @@ -0,0 +1,139 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.example.plugin.gradle; + +// tag::begin[] +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); + } + +// end::begin[] + +// tag::generateRecordWithName[] + /** + * 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 action) { + super.generateSimpleRecord("generate" + typeName, spec -> { + spec.getTypeName().set(typeName); + spec.getPackageName().set(packageName); + action.execute(spec); + }); + } +// end::generateRecordWithName[] + + /** + * This is another example of a utility method. + * + * @param name The task name + * @param fileName The file name + * @param content The content + */ + public void generateResource(String name, String fileName, String content) { + super.generateSimpleResource(name, spec -> { + spec.getFileName().set(fileName); + spec.getContent().set(content); + }); + } + +// tag::createGenerateSimpleRecordTask[] + /** + * 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 createGenerateSimpleRecordTask( + String name, Action configurator + ) { + TaskProvider task = super.createGenerateSimpleRecordTask(name, t -> { + configurator.execute(t); + t.getOutputFolder().convention( + project.getLayout().getBuildDirectory().dir("generated/" + t.getName()) + ); + }); + withJavaSourceSets(sourceSets -> { // <3> + var javaMain = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getJava(); + javaMain.srcDir(task.map(t -> t.getOutputFolder().dir("src/main/java"))); + }); + return task; + } +// end::createGenerateSimpleRecordTask[] + + /** + * Overriding a method to make sure that output directory has a correct default value. + * We are also adding to resource sets here. + * + * @param name The task name + * @param configurator The configurator action + * @return The task + */ + @Override + TaskProvider createGenerateSimpleResourceTask( + String name, Action configurator + ) { + TaskProvider task = super.createGenerateSimpleResourceTask(name, t -> { + configurator.execute(t); + t.getOutputFolder().convention( + project.getLayout().getBuildDirectory().dir("generated/" + t.getName()) + ); + }); + withJavaSourceSets(sourceSets -> { + var resources = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getResources(); + resources.srcDir(task.map(GenerateSimpleResourceTask::getOutputFolder)); + }); + return task; + } + +// tag::withJavaSourceSets[] + private void withJavaSourceSets(Consumer consumer) { + project.getPlugins().withId("java", unused -> { + var javaPluginExtension = project.getExtensions().findByType(JavaPluginExtension.class); + if (javaPluginExtension == null) { + throw new GradleException("No Java plugin extension found"); + } + consumer.accept(javaPluginExtension.getSourceSets()); + }); + } +// end::withJavaSourceSets[] + +// tag::end[] +} +// end::end[] diff --git a/test-suite-gradle-java/src/main/java/io/micronaut/sourcegen/example/plugin/gradle/TestPluginImpl.java b/test-suite-gradle-java/src/main/java/io/micronaut/sourcegen/example/plugin/gradle/TestPluginImpl.java new file mode 100644 index 0000000..9da0ac3 --- /dev/null +++ b/test-suite-gradle-java/src/main/java/io/micronaut/sourcegen/example/plugin/gradle/TestPluginImpl.java @@ -0,0 +1,33 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.example.plugin.gradle; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; + +/** + * This extends the generated plugin to make sure that the correct extension class is used. + * Only this plugin is registered with gradle, not the generated one. + */ +public class TestPluginImpl extends TestPlugin { + + @Override + protected TestExtension createExtension(Project project, Configuration classpath) { + return project.getExtensions().create( + TestExtensionImpl.class, "test", TestExtensionImpl.class, project, classpath + ); + } +} diff --git a/test-suite-gradle-java/src/main/resources/META-INF/gradle-plugins/io.micronaut.sourcegen.test.properties b/test-suite-gradle-java/src/main/resources/META-INF/gradle-plugins/io.micronaut.sourcegen.test.properties new file mode 100644 index 0000000..0368f73 --- /dev/null +++ b/test-suite-gradle-java/src/main/resources/META-INF/gradle-plugins/io.micronaut.sourcegen.test.properties @@ -0,0 +1 @@ +implementation-class=io.micronaut.sourcegen.example.plugin.gradle.TestPluginImpl diff --git a/test-suite-gradle-java/src/test/java/io/micronaut/sourcegen/example/plugin/gradle/AbstractPluginTest.java b/test-suite-gradle-java/src/test/java/io/micronaut/sourcegen/example/plugin/gradle/AbstractPluginTest.java new file mode 100644 index 0000000..bacbe99 --- /dev/null +++ b/test-suite-gradle-java/src/test/java/io/micronaut/sourcegen/example/plugin/gradle/AbstractPluginTest.java @@ -0,0 +1,81 @@ +package io.micronaut.sourcegen.example.plugin.gradle; + +import org.gradle.testkit.runner.GradleRunner; +import org.junit.jupiter.api.io.TempDir; + +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +abstract class AbstractPluginTest { + + @TempDir + public File baseDir; + + protected File settingsFile() { + return baseDir.toPath().resolve("settings.gradle").toFile(); + } + + protected void settingsFile(String content) { + try (FileOutputStream outputStream = new FileOutputStream(settingsFile())) { + outputStream.write(content.getBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected File buildFile() { + return baseDir.toPath().resolve("build.gradle").toFile(); + } + + protected void buildFile(String content) { + try (FileOutputStream outputStream = new FileOutputStream(buildFile())) { + outputStream.write(content.getBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected GradleRunner configureRunner(String ...args) { + List allArgs = new ArrayList<>(); + allArgs.add("--no-watch-fs"); + allArgs.add("-S"); + allArgs.add("-Porg.gradle.java.installations.auto-download=false"); + allArgs.add("-Porg.gradle.java.installations.auto-detect=false"); + allArgs.add("-Porg.gradle.java.installations.fromEnv=GRAALVM_HOME"); + allArgs.add("-Dio.micronaut.graalvm.rich.output=false"); + allArgs.addAll(Arrays.stream(args).toList()); + return GradleRunner.create() + .withPluginClasspath() + .withProjectDir(baseDir) + .withArguments(allArgs) + .forwardStdOutput(new BufferedWriter(new OutputStreamWriter(System.out))) + .forwardStdError(new BufferedWriter(new OutputStreamWriter(System.err))) + .withDebug(true); + } + + String micronautVersion() { + return " \"" + System.getProperty("micronautVersion") + "\""; + } + + File file(String relativePath) { + return baseDir.toPath().resolve(relativePath).toFile(); + } + + String content(File file) { + try (InputStream stream = new BufferedInputStream(new FileInputStream(file))) { + return new String(stream.readAllBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/test-suite-gradle-java/src/test/java/io/micronaut/sourcegen/example/plugin/gradle/TestPluginTest.java b/test-suite-gradle-java/src/test/java/io/micronaut/sourcegen/example/plugin/gradle/TestPluginTest.java new file mode 100644 index 0000000..49fe5b3 --- /dev/null +++ b/test-suite-gradle-java/src/test/java/io/micronaut/sourcegen/example/plugin/gradle/TestPluginTest.java @@ -0,0 +1,150 @@ +package io.micronaut.sourcegen.example.plugin.gradle; + +import org.gradle.testkit.runner.TaskOutcome; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TestPluginTest extends AbstractPluginTest { + + @Test + void generateAndBuildSimpleRecord() { + settingsFile("rootProject.name = 'test-project'"); + buildFile(""" + plugins { + id "io.micronaut.sourcegen.test" + id "java" + } + + test { + generateRecordWithName("MyRecord", "io.micronaut.test", spec -> { + spec.getJavadoc().add("A simple record") + spec.getProperties().put("title", "java.lang.String") + spec.getProperties().put("age", "java.lang.Integer") + }) + generateResource("generateHello", "META-INF/hello.txt", "Hello!"); + } + + dependencies { + } + """); + + var result = configureRunner(":build").build(); + + assertEquals(TaskOutcome.SUCCESS, result.task(":generateMyRecord").getOutcome()); + assertEquals(TaskOutcome.SUCCESS, result.task(":compileJava").getOutcome()); + + File generated = file("build/generated/generateMyRecord/src/main/java/io/micronaut/test/MyRecord.java"); + assertTrue(generated.exists()); + assertEquals(content(generated), """ + package io.micronaut.test; + + /** + * Version: 1 + * A simple record + */ + public record MyRecord( + java.lang.String title, + java.lang.Integer age + ) { + } + """); + + assertTrue(file("build/classes/java/main/io/micronaut/test/MyRecord.class").exists()); + + assertEquals(TaskOutcome.SUCCESS, result.task(":generateHello").getOutcome()); + File generatedResource = file("build/generated/generateHello/META-INF/hello.txt"); + assertTrue(generatedResource.exists()); + assertEquals("Hello!", content(generatedResource)); + } + + @Test + void generateSimpleResource() { + settingsFile("rootProject.name = 'test-project'"); + buildFile(""" + plugins { + id "io.micronaut.sourcegen.test" + id "java" + } + + test { + generateResource("generateHello", "META-INF/hello.txt", "Hello!"); + } + + dependencies { + } + """); + + var result = configureRunner(":generateHello").build(); + + assertEquals(TaskOutcome.SUCCESS, result.task(":generateHello").getOutcome()); + + File generatedResource = file("build/generated/generateHello/META-INF/hello.txt"); + assertTrue(generatedResource.exists()); + assertEquals("Hello!", content(generatedResource)); + } + + @Test + void generateSimpleResourceRepeated() { + settingsFile("rootProject.name = 'test-project'"); + buildFile(""" + import io.micronaut.sourcegen.example.plugin.gradle.model.Repeat + import io.micronaut.sourcegen.example.plugin.gradle.model.Ending + + plugins { + id "io.micronaut.sourcegen.test" + id "java" + } + + test { + generateSimpleResource("generateHello", spec -> { + spec.getFileName().set("META-INF/hello.txt") + spec.getContent().set("Hello!") + spec.getRepeat().set( + new Repeat().withNumber(2).withRepeatSuffix("_").withEnding(Ending.NEWLINE) + ) + }); + } + + dependencies { + } + """); + + var result = configureRunner(":generateHello").build(); + + assertEquals(TaskOutcome.SUCCESS, result.task(":generateHello").getOutcome()); + + File generatedResource1 = file("build/generated/generateHello/META-INF/hello.txt_1"); + assertTrue(generatedResource1.exists()); + assertEquals("Hello!\n", content(generatedResource1)); + + File generatedResource2 = file("build/generated/generateHello/META-INF/hello.txt_2"); + assertTrue(generatedResource2.exists()); + assertEquals("Hello!\n", content(generatedResource2)); + } + + @Test + void failOnRequiredProperty() { + settingsFile("rootProject.name = 'test-project'"); + buildFile(""" + plugins { + id "io.micronaut.sourcegen.test" + id "java" + } + + test { + generateSimpleRecord("generate1", spec -> {}) + } + + dependencies { + } + """); + + var result = configureRunner(":build").buildAndFail(); + assertTrue(result.getOutput().contains("property 'typeName' doesn't have a configured value.")); + } + +} diff --git a/test-suite-maven-java/build.gradle.kts b/test-suite-maven-java/build.gradle.kts new file mode 100644 index 0000000..a04ea01 --- /dev/null +++ b/test-suite-maven-java/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + id("maven-publish") + id("io.micronaut.build.internal.build-plugin-sourcegen-testsuite") +} + +repositories { + mavenCentral() +} + +dependencies { + api(projects.testSuiteCommonJava) + annotationProcessor(projects.testSuiteCommonJava) + annotationProcessor(mn.micronaut.inject) + annotationProcessor(mn.micronaut.inject.java) + annotationProcessor(projects.micronautBuildPluginSourcegenGenerator) + annotationProcessor(mnSourcegen.micronaut.sourcegen.generator.java) + implementation(projects.micronautBuildPluginSourcegenAnnotations) + + compileOnly(libs.maven.plugin.annotations) + implementation(libs.maven.plugin.api) + implementation(libs.maven.core) + testImplementation(libs.maven.plugin.testing.harness) + + testImplementation(mnTest.micronaut.test.junit5) + testImplementation(mnTest.junit.jupiter.engine) +} + +tasks.withType { + testLogging { + showStandardStreams = true + } +} diff --git a/test-suite-maven-java/src/main/java/io/micronaut/sourcegen/example/plugin/maven/GenerateMojoTrigger.java b/test-suite-maven-java/src/main/java/io/micronaut/sourcegen/example/plugin/maven/GenerateMojoTrigger.java new file mode 100644 index 0000000..ab07167 --- /dev/null +++ b/test-suite-maven-java/src/main/java/io/micronaut/sourcegen/example/plugin/maven/GenerateMojoTrigger.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.example.plugin.maven; + +// tag::content[] +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", + mavenPropertyPrefix = "test.generate.simple.record" +) +@GenerateMavenMojo( // <2> + namePrefix = "AbstractGenerateSimpleResource", + micronautPlugin = false, + source = "io.micronaut.sourcegen.example.plugin.GenerateSimpleResourceTask", + mavenPropertyPrefix = "test.generate.simple.resource" +) +public final class GenerateMojoTrigger { +} +// end::content[] diff --git a/test-suite-maven-java/src/main/java/io/micronaut/sourcegen/example/plugin/maven/GenerateSimpleRecordMojo.java b/test-suite-maven-java/src/main/java/io/micronaut/sourcegen/example/plugin/maven/GenerateSimpleRecordMojo.java new file mode 100644 index 0000000..687ceb6 --- /dev/null +++ b/test-suite-maven-java/src/main/java/io/micronaut/sourcegen/example/plugin/maven/GenerateSimpleRecordMojo.java @@ -0,0 +1,63 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.example.plugin.maven; + +import java.io.File; + +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +// tag::content[] +/** + * 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" + ) + private File outputFolder; + + @Parameter(property = "generate.simple.record.enabled", defaultValue = "true") + private boolean enabled; // <2> + + @Parameter(defaultValue = "${project}", readonly = true, required = true) + private MavenProject project; + + @Override + protected boolean isEnabled() { + return enabled; + } + + @Override + protected File getOutputFolder() { + return outputFolder; + } + + @Override + public void execute() { + if (project != null) { + project.addCompileSourceRoot( + new File(outputFolder, "src/main/java".replace("/", File.separator)).getAbsolutePath() + ); // <3> + } + super.execute(); + } +} +// end::content[] diff --git a/test-suite-maven-java/src/main/java/io/micronaut/sourcegen/example/plugin/maven/GenerateSimpleResourceMojo.java b/test-suite-maven-java/src/main/java/io/micronaut/sourcegen/example/plugin/maven/GenerateSimpleResourceMojo.java new file mode 100644 index 0000000..61607b6 --- /dev/null +++ b/test-suite-maven-java/src/main/java/io/micronaut/sourcegen/example/plugin/maven/GenerateSimpleResourceMojo.java @@ -0,0 +1,61 @@ +/* + * Copyright 2025 original authors + * + * 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 + * + * https://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 io.micronaut.sourcegen.example.plugin.maven; + +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +import java.io.File; + +/** + * An extension of the generated mojo that configures the output folder. + */ +@Mojo(name = "generateSimpleResource") +public class GenerateSimpleResourceMojo extends AbstractGenerateSimpleResourceMojo { + + @Parameter( + required = true, + defaultValue = "${project.build.directory}/generated/simpleResource" + ) + private File outputFolder; + + @Parameter(property = "generate.simple.resource.enabled", defaultValue = "true") + private boolean enabled; + + @Parameter(defaultValue = "${project}", readonly = true, required = true) + private MavenProject project; + + @Override + protected boolean isEnabled() { + return enabled; + } + + @Override + protected File getOutputFolder() { + return outputFolder; + } + + @Override + public void execute() { + if (project != null) { + project.addCompileSourceRoot( + outputFolder.getAbsolutePath() + ); + } + super.execute(); + } +} diff --git a/test-suite-maven-java/src/main/resources/META-INF/maven/plugin.xml b/test-suite-maven-java/src/main/resources/META-INF/maven/plugin.xml new file mode 100644 index 0000000..2390e0c --- /dev/null +++ b/test-suite-maven-java/src/main/resources/META-INF/maven/plugin.xml @@ -0,0 +1,20 @@ + + io.micronaut.test + test + 1.0.0 + + + + generateSimpleRecord + io.micronaut.sourcegen.example.plugin.maven.GenerateSimpleRecordMojo + generate-sources + false + + + generateSimpleResource + io.micronaut.sourcegen.example.plugin.maven.GenerateSimpleResourceMojo + generate-sources + false + + + diff --git a/test-suite-maven-java/src/main/resources/META-INF/plexus/components.xml b/test-suite-maven-java/src/main/resources/META-INF/plexus/components.xml new file mode 100644 index 0000000..37d0eb9 --- /dev/null +++ b/test-suite-maven-java/src/main/resources/META-INF/plexus/components.xml @@ -0,0 +1,14 @@ + + + + org.apache.maven.plugin.Mojo + io.micronaut.test:test:1.0.0:generateSimpleRecord + io.micronaut.sourcegen.example.plugin.maven.GenerateSimpleRecordMojo + + + org.apache.maven.plugin.Mojo + io.micronaut.test:test:1.0.0:generateSimpleResource + io.micronaut.sourcegen.example.plugin.maven.GenerateSimpleResourceMojo + + + diff --git a/test-suite-maven-java/src/test/java/io/micronaut/sourcegen/example/plugin/maven/AbstractMavenPluginTest.java b/test-suite-maven-java/src/test/java/io/micronaut/sourcegen/example/plugin/maven/AbstractMavenPluginTest.java new file mode 100644 index 0000000..2a192c7 --- /dev/null +++ b/test-suite-maven-java/src/test/java/io/micronaut/sourcegen/example/plugin/maven/AbstractMavenPluginTest.java @@ -0,0 +1,52 @@ +package io.micronaut.sourcegen.example.plugin.maven; + +import org.apache.maven.plugin.Mojo; +import org.apache.maven.plugin.testing.AbstractMojoTestCase; +import org.apache.maven.plugin.testing.ResolverExpressionEvaluatorStub; +import org.codehaus.plexus.component.configurator.ComponentConfigurator; +import org.codehaus.plexus.configuration.PlexusConfiguration; +import org.junit.jupiter.api.io.TempDir; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +abstract class AbstractMavenPluginTest extends AbstractMojoTestCase { + + @TempDir + public File baseDir; + + public Mojo findConfiguredMojo(String goal, File configurationPom) throws Exception { + Mojo mojo = lookupMojo( + "io.micronaut.test", + "test", + "1.0.0", + goal, + null + ); + PlexusConfiguration configuration = extractPluginConfiguration("test", configurationPom); + configuration.addChild("outputFolder", baseDir.getAbsolutePath()); + ComponentConfigurator configurator = getContainer().lookup(ComponentConfigurator.class, "basic" ); + configurator.configureComponent( + mojo, + configuration, + new ResolverExpressionEvaluatorStub(), + getContainer().getContainerRealm() + ); + return mojo; + } + + File file(String relativePath) { + return baseDir.toPath().resolve(relativePath).toFile(); + } + + String content(File file) { + try (InputStream stream = new BufferedInputStream(new FileInputStream(file))) { + return new String(stream.readAllBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/test-suite-maven-java/src/test/java/io/micronaut/sourcegen/example/plugin/maven/TestMavenPluginTest.java b/test-suite-maven-java/src/test/java/io/micronaut/sourcegen/example/plugin/maven/TestMavenPluginTest.java new file mode 100644 index 0000000..3b253d4 --- /dev/null +++ b/test-suite-maven-java/src/test/java/io/micronaut/sourcegen/example/plugin/maven/TestMavenPluginTest.java @@ -0,0 +1,61 @@ +package io.micronaut.sourcegen.example.plugin.maven; + +import org.junit.jupiter.api.Test; + +import java.io.File; + +class TestMavenPluginTest extends AbstractMavenPluginTest { + + @Test + void generateAndBuildSimpleRecord() throws Exception { + File pom = new File("src/test/resources/test-pom.xml"); + + GenerateSimpleRecordMojo mojo = (GenerateSimpleRecordMojo) findConfiguredMojo("generateSimpleRecord", pom); + mojo.execute(); + + File generated = file("src/main/java/io/micronaut/test/MyRecord.java"); + assertTrue(generated.exists()); + assertEquals(content(generated), """ + package io.micronaut.test; + + /** + * Version: 1 + * A simple record + */ + public record MyRecord( + java.lang.Integer age, + java.lang.String title + ) { + } + """); + } + + @Test + void generateSimpleResource() throws Exception { + File pom = new File("src/test/resources/test-resource-pom.xml"); + + GenerateSimpleResourceMojo mojo = (GenerateSimpleResourceMojo) findConfiguredMojo("generateSimpleResource", pom); + mojo.execute(); + + File generated = file("META-INF/hello.txt"); + assertTrue(generated.exists()); + assertEquals(content(generated), "Hello!"); + } + + @Test + void generateSimpleResourceWithRepeat() throws Exception { + File pom = new File("src/test/resources/test-resource-repeat-pom.xml"); + + GenerateSimpleResourceMojo mojo = (GenerateSimpleResourceMojo) findConfiguredMojo("generateSimpleResource", pom); + mojo.execute(); + + File generated1 = file("META-INF/hello.txt_1"); + assertTrue(generated1.exists()); + assertEquals(content(generated1), "Hello!\n"); + + File generated2 = file("META-INF/hello.txt_2"); + assertTrue(generated2.exists()); + assertEquals(content(generated2), "Hello!\n"); + } + +} diff --git a/test-suite-maven-java/src/test/resources/test-pom.xml b/test-suite-maven-java/src/test/resources/test-pom.xml new file mode 100644 index 0000000..5147105 --- /dev/null +++ b/test-suite-maven-java/src/test/resources/test-pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + com.example + test-maven-plugin + 1.0-SNAPSHOT + + + + + io.micronaut.test + test + 1.0.0 + + true + MyRecord + io.micronaut.test + A simple record + 1 + + java.lang.Integer + java.lang.String + + + + + + diff --git a/test-suite-maven-java/src/test/resources/test-resource-pom.xml b/test-suite-maven-java/src/test/resources/test-resource-pom.xml new file mode 100644 index 0000000..bd7a0b2 --- /dev/null +++ b/test-suite-maven-java/src/test/resources/test-resource-pom.xml @@ -0,0 +1,22 @@ + + 4.0.0 + com.example + test-maven-plugin + 1.0-SNAPSHOT + + + + + io.micronaut.test + test + 1.0.0 + + true + META-INF/hello.txt + Hello! + + + + + diff --git a/test-suite-maven-java/src/test/resources/test-resource-repeat-pom.xml b/test-suite-maven-java/src/test/resources/test-resource-repeat-pom.xml new file mode 100644 index 0000000..1b06189 --- /dev/null +++ b/test-suite-maven-java/src/test/resources/test-resource-repeat-pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + com.example + test-maven-plugin + 1.0-SNAPSHOT + + + + + io.micronaut.test + test + 1.0.0 + + true + META-INF/hello.txt + Hello! + + 2 + _ + NEWLINE + + + + + +