This is an automated email from the ASF dual-hosted git repository. jamesfredley pushed a commit to branch feat/publish-groovydoc-plugin in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 9115b7575364d29285db66d14a4202dda77188dd Author: James Fredley <[email protected]> AuthorDate: Sat Feb 21 11:12:02 2026 -0500 feat: publish GroovydocEnhancerPlugin for use in end-user Grails apps Add grails-gradle-groovydoc as a published Gradle plugin module that enables Grails applications using modern Java features (17+) to generate accurate Groovydoc documentation. Without this plugin, Groovydoc fails to parse Java sources using sealed classes, records, pattern matching, and other post-Java 11 language features. Changes: - New module grails-gradle/groovydoc with GroovydocEnhancerPlugin published as org.apache.grails.gradle:grails-gradle-groovydoc with plugin ID org.apache.grails.gradle.groovydoc - Forge DefaultFeature adds the plugin to all generated Grails apps - Profile base/profile.yml applies the plugin via grails CLI generation - grails-bom updated with the new artifact for BOM version management - build-logic retains its own copy for the framework build (bootstrap) Assisted-by: Claude Code <[email protected]> --- grails-bom/build.gradle | 1 + .../forge/feature/groovydoc/GroovydocEnhancer.java | 88 +++++++++ grails-gradle/gradle/publish-root-config.gradle | 1 + grails-gradle/groovydoc/build.gradle | 72 +++++++ .../groovydoc/GroovydocEnhancerExtension.groovy | 49 +++++ .../groovydoc/GroovydocEnhancerPlugin.groovy | 208 +++++++++++++++++++++ grails-gradle/settings.gradle | 3 + grails-profiles/base/profile.yml | 3 + 8 files changed, 425 insertions(+) diff --git a/grails-bom/build.gradle b/grails-bom/build.gradle index 260239fced..74042673f0 100644 --- a/grails-bom/build.gradle +++ b/grails-bom/build.gradle @@ -44,6 +44,7 @@ ext { // TODO: It should be possible to pull these build names using includedBuild, but I haven't found a way to do so gradleBuildProjects = [ 'grails-gradle-plugins':'org.apache.grails', + 'grails-gradle-groovydoc':'org.apache.grails.gradle', 'grails-gradle-model':'org.apache.grails.gradle', 'grails-gradle-common':'org.apache.grails.gradle', 'grails-gradle-tasks':'org.apache.grails', diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/groovydoc/GroovydocEnhancer.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/groovydoc/GroovydocEnhancer.java new file mode 100644 index 0000000000..d040c44662 --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/groovydoc/GroovydocEnhancer.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.grails.forge.feature.groovydoc; + +import io.micronaut.core.annotation.NonNull; +import jakarta.inject.Singleton; +import org.grails.forge.application.ApplicationType; +import org.grails.forge.application.generator.GeneratorContext; +import org.grails.forge.build.dependencies.Dependency; +import org.grails.forge.build.gradle.GradlePlugin; +import org.grails.forge.feature.Category; +import org.grails.forge.feature.DefaultFeature; +import org.grails.forge.feature.Feature; +import org.grails.forge.options.Options; + +import java.util.Set; + +@Singleton +public class GroovydocEnhancer implements DefaultFeature { + + @NonNull + @Override + public String getName() { + return "groovydoc-enhancer"; + } + + @Override + public String getTitle() { + return "Groovydoc Enhancer"; + } + + @NonNull + @Override + public String getDescription() { + return "Enables Groovydoc generation for projects using modern Java features (17+). " + + "Without this plugin, Groovydoc fails to parse Java sources that use sealed classes, " + + "records, pattern matching, and other post-Java 11 language features."; + } + + @Override + public boolean supports(ApplicationType applicationType) { + return true; + } + + @Override + public boolean isVisible() { + return false; + } + + @Override + public String getCategory() { + return Category.DOCUMENTATION; + } + + @Override + public boolean shouldApply(ApplicationType applicationType, Options options, Set<Feature> selectedFeatures) { + return true; + } + + @Override + public void apply(GeneratorContext generatorContext) { + generatorContext.addBuildscriptDependency(Dependency.builder() + .groupId("org.apache.grails.gradle") + .artifactId("grails-gradle-groovydoc") + .buildSrc()); + + generatorContext.addBuildPlugin(GradlePlugin.builder() + .id("org.apache.grails.gradle.groovydoc") + .useApplyPlugin(true) + .build()); + } +} diff --git a/grails-gradle/gradle/publish-root-config.gradle b/grails-gradle/gradle/publish-root-config.gradle index 4282dbfe20..dba8346e06 100644 --- a/grails-gradle/gradle/publish-root-config.gradle +++ b/grails-gradle/gradle/publish-root-config.gradle @@ -26,6 +26,7 @@ group = 'this.will.be.overridden' def publishedProjects = [ 'grails-gradle-bom', 'grails-gradle-common', + 'grails-gradle-groovydoc', 'grails-gradle-model', 'grails-gradle-plugins', 'grails-gradle-tasks', diff --git a/grails-gradle/groovydoc/build.gradle b/grails-gradle/groovydoc/build.gradle new file mode 100644 index 0000000000..105c3d522c --- /dev/null +++ b/grails-gradle/groovydoc/build.gradle @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +plugins { + id 'groovy' + id 'java-gradle-plugin' + id 'org.apache.grails.buildsrc.properties' + id 'org.apache.grails.buildsrc.compile' + id 'org.apache.grails.buildsrc.publish' + id 'org.apache.grails.buildsrc.sbom' + id 'org.apache.grails.gradle.grails-code-style' +} + +version = projectVersion +group = 'org.apache.grails.gradle' + +ext { + pomTitle = 'Grails Gradle Groovydoc Enhancer Plugin' + pomDescription = 'A Gradle plugin that enhances Groovydoc generation to support modern Java source levels, allowing Grails applications using Java 17+ features to generate accurate Groovydoc documentation' + pomMavenPublicationName = 'pluginMaven' +} + +dependencies { + implementation platform(project(':grails-gradle-bom')) + + // compile with the Groovy version provided by Gradle + // to ensure build compatibility with Gradle, currently Groovy 3.0.x + // see: https://docs.gradle.org/current/userguide/compatibility.html#groovy + compileOnly "org.codehaus.groovy:groovy" + + // Testing - Gradle TestKit is auto-added by java-gradle-plugin + testImplementation('org.spockframework:spock-core') { transitive = false } + testImplementation 'org.codehaus.groovy:groovy-test-junit5' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' +} + +configurations { + testCompileClasspath.exclude group: 'org.apache.groovy', module: 'groovy' + testRuntimeClasspath.exclude group: 'org.apache.groovy', module: 'groovy' +} + +gradlePlugin { + plugins { + groovydoc { + displayName = 'Grails Groovydoc Enhancer Plugin' + description = 'Enhances Groovydoc generation with Java source level support for modern Java features (17+)' + id = 'org.apache.grails.gradle.groovydoc' + implementationClass = 'org.apache.grails.gradle.groovydoc.GroovydocEnhancerPlugin' + } + } +} + +apply { + from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/test-config.gradle') +} diff --git a/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerExtension.groovy b/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerExtension.groovy new file mode 100644 index 0000000000..eaeab01036 --- /dev/null +++ b/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerExtension.groovy @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.grails.gradle.groovydoc + +import javax.inject.Inject + +import groovy.transform.CompileStatic + +import org.gradle.api.Project +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property + +@CompileStatic +class GroovydocEnhancerExtension { + + final Property<String> javaVersion + + final Property<Boolean> javaVersionEnabled + + final Property<Boolean> useAntBuilder + + final Property<String> footer + + @Inject + GroovydocEnhancerExtension(ObjectFactory objects, Project project) { + javaVersion = objects.property(String).convention( + project.provider { "JAVA_${project.findProperty('javaVersion') ?: '17'}" as String } + ) + javaVersionEnabled = objects.property(Boolean).convention(true) + useAntBuilder = objects.property(Boolean).convention(true) + footer = objects.property(String).convention('') + } +} diff --git a/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerPlugin.groovy b/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerPlugin.groovy new file mode 100644 index 0000000000..219ab89159 --- /dev/null +++ b/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerPlugin.groovy @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.grails.gradle.groovydoc + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.attributes.Bundling +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.Usage +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.javadoc.Groovydoc + +/** + * A Gradle plugin that enhances Groovydoc generation to support modern Java + * source levels. Gradle's built-in {@link Groovydoc} task does not expose + * the {@code javaVersion} parameter (added in Groovy 4.0.27 via + * <a href="https://issues.apache.org/jira/browse/GROOVY-11668">GROOVY-11668</a>), + * so projects using Java 17+ features (sealed classes, records, etc.) fail + * to generate Groovydoc. + * + * <p>This plugin replaces the built-in task execution with a direct AntBuilder + * invocation that passes the {@code javaVersion} parameter, enabling accurate + * Groovydoc generation for modern Java source levels.</p> + * + * <p>Configure via the {@code groovydocEnhancer} extension:</p> + * <pre> + * groovydocEnhancer { + * javaVersion = 'JAVA_17' // Java source level for parsing + * javaVersionEnabled = true // set false for Groovy < 4.0.27 + * useAntBuilder = true // set false when Gradle adds native support + * footer = '<p>My Footer</p>' + * } + * </pre> + * + * @since 7.0.8 + * @see GroovydocEnhancerExtension + * @see <a href="https://github.com/gradle/gradle/issues/33659">gradle#33659</a> + */ +@CompileStatic +class GroovydocEnhancerPlugin implements Plugin<Project> { + + @Override + void apply(Project project) { + GroovydocEnhancerExtension extension = project.extensions.create( + 'groovydocEnhancer', + GroovydocEnhancerExtension, + project + ) + registerDocumentationConfiguration(project) + configureGroovydocDefaults(project, extension) + configureAntBuilderExecution(project, extension) + } + + private static void registerDocumentationConfiguration(Project project) { + if (project.configurations.names.contains('documentation')) { + return + } + project.configurations.register('documentation') { + it.canBeConsumed = false + it.canBeResolved = true + it.attributes { + it.attribute(Category.CATEGORY_ATTRIBUTE, project.objects.named(Category, Category.LIBRARY)) + it.attribute(Bundling.BUNDLING_ATTRIBUTE, project.objects.named(Bundling, Bundling.EXTERNAL)) + it.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage, Usage.JAVA_RUNTIME)) + } + } + } + + @CompileDynamic + private static void configureGroovydocDefaults(Project project, GroovydocEnhancerExtension extension) { + project.tasks.withType(Groovydoc).configureEach { + it.includeAuthor.set(false) + it.includeMainForScripts.set(false) + it.processScripts.set(false) + it.noTimestamp = true + it.noVersionStamp = false + def footerValue = extension.footer.getOrElse('') + if (footerValue) { + it.footer = footerValue + } + if (project.configurations.names.contains('documentation')) { + it.groovyClasspath = project.configurations.getByName('documentation') + } + } + } + + @CompileDynamic + private static void configureAntBuilderExecution(Project project, GroovydocEnhancerExtension extension) { + project.tasks.withType(Groovydoc).configureEach { gdoc -> + if (!extension.useAntBuilder.get()) { + return + } + + gdoc.actions.clear() + gdoc.doLast { + def destDir = gdoc.destinationDir.tap { it.mkdirs() } + def sourceDirs = resolveSourceDirectories(gdoc, project) + if (sourceDirs.isEmpty()) { + project.logger.lifecycle( + 'Skipping groovydoc for {}: no source directories found', + gdoc.name + ) + return + } + + def docConfig = project.configurations.findByName('documentation') + if (!docConfig) { + project.logger.warn( + 'Skipping groovydoc for {}: \'documentation\' configuration not found', + gdoc.name + ) + return + } + + project.ant.taskdef( + name: 'groovydoc', + classname: 'org.codehaus.groovy.ant.Groovydoc', + classpath: docConfig.asPath + ) + + def links = resolveLinks(gdoc) + def sourcepath = sourceDirs + .collect { it.absolutePath } + .join(File.pathSeparator) + + def antArgs = [ + destdir: destDir.absolutePath, + sourcepath: sourcepath, + packagenames: '**.*', + windowtitle: gdoc.windowTitle ?: '', + doctitle: gdoc.docTitle ?: '', + footer: gdoc.footer ?: '', + access: resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected', + author: resolveGroovydocProperty(gdoc.includeAuthor) as String, + noTimestamp: resolveGroovydocProperty(gdoc.noTimestamp) as String, + noVersionStamp: resolveGroovydocProperty(gdoc.noVersionStamp) as String, + processScripts: resolveGroovydocProperty(gdoc.processScripts) as String, + includeMainForScripts: resolveGroovydocProperty(gdoc.includeMainForScripts) as String + ] + + if (extension.javaVersionEnabled.get()) { + antArgs.put('javaVersion', extension.javaVersion.get()) + } + + project.ant.groovydoc(antArgs) { + for (var l in links) { + link(packages: l.packages, href: l.href) + } + } + } + } + } + + @CompileDynamic + private static List<File> resolveSourceDirectories(Groovydoc gdoc, Project project) { + if (gdoc.ext.has('groovydocSourceDirs') && gdoc.ext.groovydocSourceDirs) { + return (gdoc.ext.groovydocSourceDirs as List<File>) + .findAll { it.exists() } + .unique() + } + + List<File> sourceDirs = [] + def sourceSets = project.extensions.findByType(SourceSetContainer) + if (sourceSets) { + def mainSS = sourceSets.findByName('main') + if (mainSS) { + sourceDirs.addAll(mainSS.groovy.srcDirs.findAll { it.exists() }) + sourceDirs.addAll(mainSS.java.srcDirs.findAll { it.exists() }) + } + } + sourceDirs.unique() + } + + @CompileDynamic + private static List<Map<String, String>> resolveLinks(Groovydoc gdoc) { + if (gdoc.ext.has('groovydocLinks')) { + return gdoc.ext.groovydocLinks as List<Map<String, String>> + } + [] + } + + static Object resolveGroovydocProperty(Object value) { + if (value instanceof Provider) { + return ((Provider) value).getOrNull() + } + value + } +} diff --git a/grails-gradle/settings.gradle b/grails-gradle/settings.gradle index 7d0b8a0a1b..64a7bccebf 100644 --- a/grails-gradle/settings.gradle +++ b/grails-gradle/settings.gradle @@ -74,3 +74,6 @@ project(':grails-gradle-model').projectDir = file('model') include 'grails-gradle-tasks' project(':grails-gradle-tasks').projectDir = file('tasks') + +include 'grails-gradle-groovydoc' +project(':grails-gradle-groovydoc').projectDir = file('groovydoc') diff --git a/grails-profiles/base/profile.yml b/grails-profiles/base/profile.yml index f1b9b54e29..4d42ea9d40 100644 --- a/grails-profiles/base/profile.yml +++ b/grails-profiles/base/profile.yml @@ -30,7 +30,10 @@ build: - eclipse - idea - org.apache.grails.gradle.grails-app + - org.apache.grails.gradle.groovydoc dependencies: + - scope: build + coords: "org.apache.grails.gradle:grails-gradle-groovydoc" - scope: build coords: "org.apache.grails:grails-gradle-plugins" - scope: developmentOnly
