diff --git a/build-logic/plugins/build.gradle b/build-logic/plugins/build.gradle index e85254e1528..90240300d6a 100644 --- a/build-logic/plugins/build.gradle +++ b/build-logic/plugins/build.gradle @@ -62,6 +62,14 @@ gradlePlugin { id = 'org.apache.grails.gradle.grails-code-style' implementationClass = 'org.apache.grails.buildsrc.GrailsCodeStylePlugin' } + register('groovydocEnhancer') { + id = 'org.apache.grails.buildsrc.groovydoc-enhancer' + implementationClass = 'org.apache.grails.buildsrc.GroovydocEnhancerPlugin' + } + register('grailsGroovydoc') { + id = 'org.apache.grails.buildsrc.groovydoc' + implementationClass = 'org.apache.grails.buildsrc.GrailsGroovydocPlugin' + } register('grailsRepoSettings') { id = 'org.apache.grails.buildsrc.repo' implementationClass = 'org.apache.grails.buildsrc.GrailsRepoSettingsPlugin' diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy new file mode 100644 index 00000000000..b10a77a93cb --- /dev/null +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsGroovydocPlugin.groovy @@ -0,0 +1,54 @@ +/* + * 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.buildsrc + +import groovy.transform.CompileStatic + +import org.gradle.api.Plugin +import org.gradle.api.Project + +@CompileStatic +class GrailsGroovydocPlugin implements Plugin { + + static final String MATOMO_FOOTER = '''\ + + +''' + + @Override + void apply(Project project) { + project.pluginManager.apply(GroovydocEnhancerPlugin) + project.extensions.getByType(GroovydocEnhancerExtension) + .footer.set(MATOMO_FOOTER) + } +} diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerExtension.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerExtension.groovy new file mode 100644 index 00000000000..4ef1eaebaa1 --- /dev/null +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerExtension.groovy @@ -0,0 +1,98 @@ +/* + * 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.buildsrc + +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 + +/** + * Extension for configuring the Groovydoc Enhancer convention plugin. + * + *

This plugin replaces Gradle's built-in Groovydoc task execution with + * a direct AntBuilder invocation of the Groovy {@code org.codehaus.groovy.ant.Groovydoc} + * Ant task. This enables the {@code javaVersion} parameter (added in Groovy 4.0.27, + * GROOVY-11668) which controls the JavaParser language level used when parsing + * Java source files.

+ * + *

When Gradle natively supports the {@code javaVersion} property + * (see gradle#33659), + * set {@link #useAntBuilder} to {@code false} to revert to Gradle's built-in + * Groovydoc task execution while retaining all other configuration (footer, + * defaults, etc.).

+ * + * @since 7.0.8 + */ +@CompileStatic +class GroovydocEnhancerExtension { + + /** + * The Java language level string passed to the groovydoc Ant task's + * {@code javaVersion} parameter (e.g. {@code "JAVA_17"}, {@code "JAVA_21"}). + * + *

Defaults to {@code "JAVA_${javaVersion}"} where {@code javaVersion} + * is read from the project property, falling back to {@code "JAVA_17"}.

+ */ + final Property javaVersion + + /** + * Whether to pass the {@code javaVersion} parameter to the groovydoc + * Ant task. Set to {@code false} for projects using Groovy versions + * older than 4.0.27 (which do not support the parameter). + * + *

Defaults to {@code true}.

+ */ + final Property javaVersionEnabled + + /** + * Whether to replace Gradle's built-in Groovydoc task execution with + * AntBuilder invocation. When {@code true} (default), the plugin clears + * the task's actions and replaces them with a {@code doLast} that uses + * AntBuilder. When {@code false}, the plugin only applies property + * defaults (footer, etc.) and lets Gradle's built-in task run normally. + * + *

Set to {@code false} when Gradle adds native {@code javaVersion} + * support (gradle/gradle#33659).

+ * + *

Defaults to {@code true}.

+ */ + final Property useAntBuilder + + /** + * HTML footer appended to every generated groovydoc page. Useful for + * analytics scripts, copyright notices, or custom branding. + * + *

Defaults to an empty string (no footer).

+ */ + final Property footer + + @Inject + GroovydocEnhancerExtension(ObjectFactory objects, Project project) { + javaVersion = objects.property(String).convention( + project.provider { "JAVA_${GradleUtils.findProperty(project, 'javaVersion') ?: '17'}" as String } + ) + javaVersionEnabled = objects.property(Boolean).convention(true) + useAntBuilder = objects.property(Boolean).convention(true) + footer = objects.property(String).convention('') + } +} diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerPlugin.groovy new file mode 100644 index 00000000000..38fb2b04e23 --- /dev/null +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocEnhancerPlugin.groovy @@ -0,0 +1,182 @@ +/* + * 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.buildsrc + +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 + +@CompileStatic +class GroovydocEnhancerPlugin implements Plugin { + + @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 resolveSourceDirectories(Groovydoc gdoc, Project project) { + if (gdoc.ext.has('groovydocSourceDirs') && gdoc.ext.groovydocSourceDirs) { + return (gdoc.ext.groovydocSourceDirs as List) + .findAll { it.exists() } + .unique() + } + + List 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> resolveLinks(Groovydoc gdoc) { + if (gdoc.ext.has('groovydocLinks')) { + return gdoc.ext.groovydocLinks as List> + } + [] + } + + static Object resolveGroovydocProperty(Object value) { + if (value instanceof Provider) { + return ((Provider) value).getOrNull() + } + value + } +} diff --git a/gradle/docs-dependencies.gradle b/gradle/docs-dependencies.gradle index df4244f39c1..ec9c6da7e14 100644 --- a/gradle/docs-dependencies.gradle +++ b/gradle/docs-dependencies.gradle @@ -16,15 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -configurations.register('documentation') { - canBeConsumed = false - canBeResolved = true - attributes { - attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY)) - attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL)) - attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME)) - } -} +apply plugin: 'org.apache.grails.buildsrc.groovydoc' dependencies { add('documentation', platform(project(':grails-bom'))) @@ -48,56 +40,35 @@ String resolveProjectVersion(String artifact) { if (!version) { return null } + version } tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> gdoc.exclude('META-INF/**', '*yml', '*properties', '*xml', '**/Application.groovy', '**/Bootstrap.groovy', '**/resources.groovy') - gdoc.groovyClasspath = configurations.documentation gdoc.windowTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" gdoc.docTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" - gdoc.access = GroovydocAccess.PROTECTED - gdoc.includeAuthor = false - gdoc.includeMainForScripts = false - gdoc.processScripts = false - gdoc.noTimestamp = true - gdoc.noVersionStamp = false - gdoc.footer = ''' - -''' - doFirst { + gdoc.doFirst { + List> links = [] def gebVersion = resolveProjectVersion('geb-spock') - if(gebVersion) { - gdoc.link("https://groovy.apache.org/geb/manual/${gebVersion}/api/", 'geb.') + if (gebVersion) { + links << [packages: 'geb.', href: "https://groovy.apache.org/geb/manual/${gebVersion}/api/"] } - def testContainersVersion = resolveProjectVersion('testcontainers') - if(testContainersVersion) { - gdoc.link("https://javadoc.io/doc/org.testcontainers/testcontainers/${testContainersVersion}/", 'org.testcontainers.') + if (testContainersVersion) { + links << [packages: 'org.testcontainers.', href: "https://javadoc.io/doc/org.testcontainers/testcontainers/${testContainersVersion}/"] } - def springVersion = resolveProjectVersion('spring-core') - if(springVersion) { - gdoc.link("https://docs.spring.io/spring-framework/docs/${springVersion}/javadoc-api/", 'org.springframework.core.') + if (springVersion) { + links << [packages: 'org.springframework.core.', href: "https://docs.spring.io/spring-framework/docs/${springVersion}/javadoc-api/"] } - def springBootVersion = resolveProjectVersion('spring-boot') - if(springBootVersion) { - gdoc.link("https://docs.spring.io/spring-boot/docs/${springBootVersion}/api/", 'org.springframework.boot.') + if (springBootVersion) { + links << [packages: 'org.springframework.boot.', href: "https://docs.spring.io/spring-boot/docs/${springBootVersion}/api/"] + } + if (gdoc.ext.has('groovydocLinks')) { + links.addAll(gdoc.ext.groovydocLinks as List>) } + gdoc.ext.groovydocLinks = links } } \ No newline at end of file diff --git a/grails-bom/build.gradle b/grails-bom/build.gradle index 260239fcedd..74042673f0a 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-data-docs/stage/build.gradle b/grails-data-docs/stage/build.gradle index b5d71cc03ad..5af7d885445 100644 --- a/grails-data-docs/stage/build.gradle +++ b/grails-data-docs/stage/build.gradle @@ -25,23 +25,6 @@ apply from: rootProject.layout.projectDirectory.file('gradle/docs-dependencies.g combinedGroovydoc.configure { Groovydoc task -> task.windowTitle = "Grails Data Mapping API - ${projectVersion}" task.docTitle = "Grails Data Mapping API - ${projectVersion}" - task.footer = ''' - -''' Set docProjects = rootProject.subprojects.findAll { it.name in [ @@ -77,14 +60,16 @@ combinedGroovydoc.configure { Groovydoc task -> } .flatten() - task.source(sources.collect{ SourceSet it -> [it.allSource.srcDirs, it.allSource.srcDirs] }.flatten().findAll { File srcDir -> - if(!(srcDir.name in ['java', 'groovy'])) { + def allSourceDirs = sources.collect { SourceSet it -> [it.allSource.srcDirs, it.allSource.srcDirs] }.flatten().findAll { File srcDir -> + if (!(srcDir.name in ['java', 'groovy'])) { return false } srcDir.exists() - }.unique()) - task.classpath = files(sources.collect{ SourceSet it -> it.compileClasspath.filter(File.&isDirectory) }.flatten().unique()) + }.unique() + task.source(allSourceDirs) + task.ext.groovydocSourceDirs = allSourceDirs + task.classpath = files(sources.collect { SourceSet it -> it.compileClasspath.filter(File.&isDirectory) }.flatten().unique()) task.destinationDir = project.layout.buildDirectory.dir('data-api/api').get().asFile task.inputs.files(task.source).withPropertyName("groovyDocSrc").withPathSensitivity(PathSensitivity.RELATIVE) diff --git a/grails-data-hibernate5/docs/build.gradle b/grails-data-hibernate5/docs/build.gradle index aed57721871..be9c04f90de 100644 --- a/grails-data-hibernate5/docs/build.gradle +++ b/grails-data-hibernate5/docs/build.gradle @@ -31,20 +31,18 @@ ext { coreProjects = ['grails-datastore-core', 'grails-datamapping-core'] } -configurations { - documentation { - attributes { - attribute(Bundling.BUNDLING_ATTRIBUTE, (Bundling) (objects.named(Bundling, 'external'))) - } - } -} +apply plugin: 'org.apache.grails.buildsrc.groovydoc' dependencies { documentation platform(project(':grails-bom')) documentation 'com.github.javaparser:javaparser-core' documentation "info.picocli:picocli:$picocliVersion" documentation 'org.apache.groovy:groovy-dateutil' + documentation 'org.apache.groovy:groovy-ant' + documentation 'org.apache.groovy:groovy-groovydoc' + documentation 'org.apache.groovy:groovy-templates' documentation 'org.fusesource.jansi:jansi' + documentation 'jline:jline' documentation project(':grails-bootstrap') documentation project(':grails-core') documentation project(':grails-spring') @@ -96,23 +94,6 @@ tasks.withType(Groovydoc).configureEach { .collect { ":${it.name}:groovydoc" }) it.docTitle = "GORM for Hibernate 5 - $project.version" - it.footer = ''' - -''' def sourceFiles = coreProjects.collect { rootProject.layout.projectDirectory.files("$it/src/main/groovy") @@ -124,13 +105,15 @@ tasks.withType(Groovydoc).configureEach { it.source = sourceFiles it.destinationDir = layout.buildDirectory.dir('combined-api/api').get().asFile - it.access = GroovydocAccess.PROTECTED - it.processScripts = false - it.includeMainForScripts = false - it.includeAuthor = false it.classpath = configurations.documentation - it.groovyClasspath += configurations.documentation - it.noVersionStamp = false + + List groovydocSrcDirs = coreProjects.collect { + rootProject.layout.projectDirectory.dir("$it/src/main/groovy").asFile + } + rootProject.subprojects + .findAll { sp -> sp.findProperty('gormApiDocs') } + .each { sp -> groovydocSrcDirs << new File(sp.projectDir, 'src/main/groovy') } + it.ext.groovydocSourceDirs = groovydocSrcDirs } tasks.register('docs', Sync).configure { Sync docTask -> diff --git a/grails-data-mongodb/docs/build.gradle b/grails-data-mongodb/docs/build.gradle index 89cfce45305..02de80eb950 100644 --- a/grails-data-mongodb/docs/build.gradle +++ b/grails-data-mongodb/docs/build.gradle @@ -31,13 +31,7 @@ ext { coreProjects = ['grails-datastore-core', 'grails-datamapping-core'] } -configurations { - documentation { - attributes { - attribute(Bundling.BUNDLING_ATTRIBUTE, (Bundling) (objects.named(Bundling, 'external'))) - } - } -} +apply plugin: 'org.apache.grails.buildsrc.groovydoc' tasks.register('resolveMongodbVersion').configure { Task docTask -> docTask.group = 'documentation' @@ -56,7 +50,10 @@ tasks.register('resolveMongodbVersion').configure { Task docTask -> dependencies { documentation platform(project(':grails-bom')) documentation 'org.fusesource.jansi:jansi' + documentation 'jline:jline' documentation 'org.apache.groovy:groovy' + documentation 'org.apache.groovy:groovy-ant' + documentation 'org.apache.groovy:groovy-groovydoc' documentation 'org.apache.groovy:groovy-templates' documentation 'org.apache.groovy:groovy-dateutil' documentation 'com.github.javaparser:javaparser-core' @@ -105,23 +102,7 @@ tasks.withType(Groovydoc).configureEach { Groovydoc groovydoc -> .findAll { it.findProperty('gormApiDocs') } .collect { ":${it.name}:groovydoc" }) groovydoc.docTitle = "GORM for MongoDB - $project.version" - groovydoc.footer = ''' - -''' + groovydoc.includeAuthor = true def sourceFiles = coreProjects.collect { layout.projectDirectory.files("$it/src/main/groovy") @@ -133,13 +114,15 @@ tasks.withType(Groovydoc).configureEach { Groovydoc groovydoc -> groovydoc.source = sourceFiles groovydoc.destinationDir = layout.buildDirectory.dir('combined-api/api').get().asFile - groovydoc.access = GroovydocAccess.PROTECTED - groovydoc.processScripts = false - groovydoc.includeMainForScripts = false - groovydoc.includeAuthor = true groovydoc.classpath = configurations.documentation - groovydoc.groovyClasspath += configurations.documentation - groovydoc.noVersionStamp = false + + List groovydocSrcDirs = coreProjects.collect { + layout.projectDirectory.dir("$it/src/main/groovy").asFile + } + rootProject.subprojects + .findAll { sp -> sp.findProperty('gormApiDocs') } + .each { sp -> groovydocSrcDirs << new File(sp.projectDir, 'src/main/groovy') } + groovydoc.ext.groovydocSourceDirs = groovydocSrcDirs } tasks.register('docs', Sync).configure { Sync docTask -> diff --git a/grails-doc/build.gradle b/grails-doc/build.gradle index 1802c28395b..4f7c1d9d879 100644 --- a/grails-doc/build.gradle +++ b/grails-doc/build.gradle @@ -63,23 +63,6 @@ apply from: rootProject.layout.projectDirectory.file('gradle/docs-dependencies.g combinedGroovydoc.configure { Groovydoc gdoc -> gdoc.windowTitle = "Grails $projectVersion" gdoc.docTitle = "Grails $projectVersion" - gdoc.footer = ''' - -''' def docProjects = rootProject.subprojects .findAll { it.findProperty('includeInApiDocs') } @@ -127,7 +110,9 @@ combinedGroovydoc.configure { Groovydoc gdoc -> sourceDirs } - gdoc.source(project.files((baseSourceDirs + includedBuildSourceDirs).unique())) + def allSourceDirs = (baseSourceDirs + includedBuildSourceDirs).unique() + gdoc.source(project.files(allSourceDirs)) + gdoc.ext.groovydocSourceDirs = allSourceDirs gdoc.classpath = files(sources.collect { SourceSet it -> it.compileClasspath.filter(File.&isDirectory) }.flatten().unique()) gdoc.destinationDir = project.layout.buildDirectory.dir('combined-api/api').get().asFile diff --git a/grails-forge/gradle/doc-config.gradle b/grails-forge/gradle/doc-config.gradle index d7b0eaf7fba..d850444c627 100644 --- a/grails-forge/gradle/doc-config.gradle +++ b/grails-forge/gradle/doc-config.gradle @@ -17,46 +17,21 @@ * under the License. */ -configurations.register('documentation') { - canBeConsumed = false - canBeResolved = true - attributes { - attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY)) - attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL)) - attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME)) - } +apply plugin: 'org.apache.grails.buildsrc.groovydoc' + +groovydocEnhancer { + javaVersionEnabled = false } dependencies { documentation "org.codehaus.groovy:groovy-templates:$groovyVersion" documentation "org.codehaus.groovy:groovy-dateutil:$groovyVersion" + documentation "org.codehaus.groovy:groovy-ant:$groovyVersion" + documentation "org.codehaus.groovy:groovy-groovydoc:$groovyVersion" } -tasks.withType(Groovydoc).configureEach { - classpath += project.configurations.documentation - windowTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" - docTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" - footer = ''' - -''' - access = GroovydocAccess.PROTECTED - includeAuthor = false - includeMainForScripts = false - processScripts = false - noTimestamp = true - noVersionStamp = false +tasks.withType(Groovydoc).configureEach { Groovydoc gdoc -> + gdoc.classpath += project.configurations.documentation + gdoc.windowTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" + gdoc.docTitle = "${project.findProperty('pomArtifactId') ?: project.name} - $projectVersion" } 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 00000000000..d040c446624 --- /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 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/docs-config.gradle b/grails-gradle/gradle/docs-config.gradle index be2aaf4f1c9..8426b70a632 100644 --- a/grails-gradle/gradle/docs-config.gradle +++ b/grails-gradle/gradle/docs-config.gradle @@ -17,15 +17,8 @@ * under the License. */ -configurations.register('documentation') { - canBeConsumed = false - canBeResolved = true - attributes { - attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY)) - attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL)) - attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME)) - } -} +apply plugin: 'org.apache.grails.buildsrc.groovydoc' + dependencies { add('documentation', platform(project(':grails-gradle-bom'))) add('documentation', 'org.fusesource.jansi:jansi') @@ -45,30 +38,6 @@ ext { TaskProvider groovydocTask = tasks.named('groovydoc', Groovydoc) groovydocTask.configure { Groovydoc it -> it.classpath = configurations.documentation - it.groovyClasspath = configurations.documentation - it.access = GroovydocAccess.PROTECTED - it.includeAuthor = false - it.includeMainForScripts = false - it.processScripts = false - it.noTimestamp = true - it.noVersionStamp = false - it.footer = ''' - -''' it.destinationDir = project.file('build/docs/api') } diff --git a/grails-gradle/gradle/publish-root-config.gradle b/grails-gradle/gradle/publish-root-config.gradle index 4282dbfe20c..dba8346e06e 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 00000000000..105c3d522c4 --- /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 00000000000..eaeab010368 --- /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 javaVersion + + final Property javaVersionEnabled + + final Property useAntBuilder + + final Property 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 00000000000..219ab891599 --- /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 + * GROOVY-11668), + * so projects using Java 17+ features (sealed classes, records, etc.) fail + * to generate Groovydoc. + * + *

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.

+ * + *

Configure via the {@code groovydocEnhancer} extension:

+ *
+ * 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>'
+ * }
+ * 
+ * + * @since 7.0.8 + * @see GroovydocEnhancerExtension + * @see gradle#33659 + */ +@CompileStatic +class GroovydocEnhancerPlugin implements Plugin { + + @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 resolveSourceDirectories(Groovydoc gdoc, Project project) { + if (gdoc.ext.has('groovydocSourceDirs') && gdoc.ext.groovydocSourceDirs) { + return (gdoc.ext.groovydocSourceDirs as List) + .findAll { it.exists() } + .unique() + } + + List 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> resolveLinks(Groovydoc gdoc) { + if (gdoc.ext.has('groovydocLinks')) { + return gdoc.ext.groovydocLinks as List> + } + [] + } + + 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 7d0b8a0a1b2..64a7bccebf5 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 f1b9b54e294..4d42ea9d40d 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