You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/02/21 00:28:16 UTC
(grails-core) 03/03: feat: hybrid ConfigReportCommand with static metadata and runtime values
This is an automated email from the ASF dual-hosted git repository.
jamesfredley pushed a commit to branch docs/configuration-hybrid
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 58327298862d376ee69b27c21d646d4dc041da36
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Fri Feb 20 19:28:00 2026 -0500
feat: hybrid ConfigReportCommand with static metadata and runtime values
Merge curated property metadata from the Application Properties
documentation with runtime-collected values from the Spring Environment.
The command now outputs a 3-column AsciiDoc table (Property, Description,
Default) organized by functional category (Core Properties, Web &
Controllers, CORS, GORM, etc.). Runtime values override static defaults
for known properties. Properties not found in the metadata appear in a
separate Other Properties section.
- Add config-properties.yml with ~160 properties across 21 categories
- Modify ConfigReportCommand to load YAML metadata and merge with runtime
- Update unit tests for 3-column hybrid format
- Update integration tests for hybrid category-based layout
Assisted-by: Claude Code <Cl...@Claude.ai>
---
.../grails/dev/commands/ConfigReportCommand.groovy | 130 +++-
.../META-INF/grails/config-properties.yml | 799 +++++++++++++++++++++
.../dev/commands/ConfigReportCommandSpec.groovy | 149 +++-
.../ConfigReportCommandIntegrationSpec.groovy | 111 +--
4 files changed, 1099 insertions(+), 90 deletions(-)
diff --git a/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy b/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy
index e10da60382..67e3aaf316 100644
--- a/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy
+++ b/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy
@@ -21,6 +21,8 @@ package grails.dev.commands
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
+import org.yaml.snakeyaml.Yaml
+
import org.springframework.core.env.ConfigurableEnvironment
import org.springframework.core.env.EnumerablePropertySource
import org.springframework.core.env.PropertySource
@@ -108,7 +110,26 @@ class ConfigReportCommand implements ApplicationCommand {
* @param sorted the sorted configuration properties
* @param reportFile the file to write the report to
*/
- void writeReport(Map<String, String> sorted, File reportFile) {
+ void writeReport(Map<String, String> runtimeProperties, File reportFile) {
+ Map<String, Map<String, String>> metadata = loadPropertyMetadata()
+ Map<String, List<Map<String, String>>> categories = new LinkedHashMap<String, List<Map<String, String>>>()
+ for (Map.Entry<String, Map<String, String>> entry : metadata.entrySet()) {
+ Map<String, String> property = entry.value
+ String category = property.get('category')
+ if (!categories.containsKey(category)) {
+ categories.put(category, new ArrayList<Map<String, String>>())
+ }
+ categories.get(category).add(property)
+ }
+
+ Set<String> knownKeys = metadata.keySet()
+ Map<String, String> otherProperties = new TreeMap<String, String>()
+ runtimeProperties.each { String key, String value ->
+ if (!knownKeys.contains(key)) {
+ otherProperties.put(key, value)
+ }
+ }
+
reportFile.withWriter('UTF-8') { BufferedWriter writer ->
writer.writeLine('= Grails Application Configuration Report')
writer.writeLine(':toc: left')
@@ -116,32 +137,107 @@ class ConfigReportCommand implements ApplicationCommand {
writer.writeLine(':source-highlighter: coderay')
writer.writeLine('')
- String currentSection = ''
- sorted.each { String key, String value ->
- String section = key.contains('.') ? key.substring(0, key.indexOf('.')) : key
- if (section != currentSection) {
- if (currentSection) {
- writer.writeLine('|===')
- writer.writeLine('')
+ categories.each { String categoryName, List<Map<String, String>> categoryProperties ->
+ writer.writeLine("== ${categoryName}")
+ writer.writeLine('')
+ writer.writeLine('[cols="2,5,2", options="header"]')
+ writer.writeLine('|===')
+ writer.writeLine('| Property | Description | Default')
+ writer.writeLine('')
+
+ categoryProperties.each { Map<String, String> property ->
+ String key = property.get('key')
+ String description = property.get('description')
+ String defaultValue = property.get('default')
+ String resolvedValue
+ if (runtimeProperties.containsKey(key)) {
+ resolvedValue = "`${escapeAsciidoc(runtimeProperties.get(key))}`"
}
- currentSection = section
- writer.writeLine("== ${section}")
- writer.writeLine('')
- writer.writeLine('[cols="2,3", options="header"]')
- writer.writeLine('|===')
- writer.writeLine('| Property | Value')
+ else {
+ resolvedValue = escapeAsciidoc(defaultValue)
+ }
+ writer.writeLine("| `${key}`")
+ writer.writeLine("| ${escapeAsciidoc(description)}")
+ writer.writeLine("| ${resolvedValue}")
writer.writeLine('')
}
- writer.writeLine("| `${key}`")
- writer.writeLine("| `${escapeAsciidoc(value)}`")
+ writer.writeLine('|===')
writer.writeLine('')
}
- if (currentSection) {
+
+ if (!otherProperties.isEmpty()) {
+ writer.writeLine('== Other Properties')
+ writer.writeLine('')
+ writer.writeLine('[cols="2,3", options="header"]')
+ writer.writeLine('|===')
+ writer.writeLine('| Property | Default')
+ writer.writeLine('')
+ otherProperties.each { String key, String value ->
+ writer.writeLine("| `${key}`")
+ writer.writeLine("| `${escapeAsciidoc(value)}`")
+ writer.writeLine('')
+ }
writer.writeLine('|===')
}
}
}
+ Map<String, Map<String, String>> loadPropertyMetadata() {
+ InputStream stream = ConfigReportCommand.classLoader.getResourceAsStream('META-INF/grails/config-properties.yml')
+ if (stream == null) {
+ return new LinkedHashMap<String, Map<String, String>>()
+ }
+ Map<String, Map<String, String>> metadata = new LinkedHashMap<String, Map<String, String>>()
+ Map<String, Object> yamlData
+ try {
+ yamlData = (Map<String, Object>) new Yaml().load(stream)
+ }
+ finally {
+ stream.close()
+ }
+ Object categories = yamlData.get('categories')
+ if (!(categories instanceof List)) {
+ return metadata
+ }
+ for (Object categoryObject : (List<Object>) categories) {
+ if (!(categoryObject instanceof Map)) {
+ continue
+ }
+ Map<String, Object> categoryMap = (Map<String, Object>) categoryObject
+ Object categoryNameObject = categoryMap.get('name')
+ if (!(categoryNameObject instanceof String)) {
+ continue
+ }
+ String categoryName = (String) categoryNameObject
+ Object properties = categoryMap.get('properties')
+ if (!(properties instanceof List)) {
+ continue
+ }
+ for (Object propertyObject : (List<Object>) properties) {
+ if (!(propertyObject instanceof Map)) {
+ continue
+ }
+ Map<String, Object> propertyMap = (Map<String, Object>) propertyObject
+ Object keyObject = propertyMap.get('key')
+ Object descriptionObject = propertyMap.get('description')
+ Object defaultObject = propertyMap.get('default')
+ if (!(keyObject instanceof String)) {
+ continue
+ }
+ String key = (String) keyObject
+ String description = descriptionObject instanceof String ? (String) descriptionObject : ''
+ String defaultValue = defaultObject instanceof String ? (String) defaultObject : ''
+ Map<String, String> entry = new LinkedHashMap<String, String>()
+ entry.put('key', key)
+ entry.put('description', description)
+ entry.put('default', defaultValue)
+ entry.put('category', categoryName)
+ metadata.put(key, entry)
+ }
+ }
+ metadata
+ }
+
/**
* Escapes special AsciiDoc characters in a value string.
*
diff --git a/grails-core/src/main/resources/META-INF/grails/config-properties.yml b/grails-core/src/main/resources/META-INF/grails/config-properties.yml
new file mode 100644
index 0000000000..f82c1b38e4
--- /dev/null
+++ b/grails-core/src/main/resources/META-INF/grails/config-properties.yml
@@ -0,0 +1,799 @@
+categories:
+ - name: "Core Properties"
+ properties:
+ - key: "grails.profile"
+ description: "The active Grails application profile (e.g., web, rest-api, plugin)."
+ default: "Set by project template"
+ - key: "grails.codegen.defaultPackage"
+ description: "The default package used when generating artefacts with grails create-* commands."
+ default: "Set by project template"
+ - key: "grails.serverURL"
+ description: "The server URL used to generate absolute links (e.g., https://my.app.com) and used by redirects."
+ default: "_(derived from request)_"
+ - key: "grails.enable.native2ascii"
+ description: "Whether to perform native2ascii conversion of i18n properties files."
+ default: "`true`"
+ - key: "grails.bootstrap.skip"
+ description: "Whether to skip execution of BootStrap.groovy classes on startup."
+ default: "`false`"
+ - key: "grails.spring.bean.packages"
+ description: "List of packages to scan for Spring beans."
+ default: "`[]`"
+ - key: "grails.spring.disable.aspectj.autoweaving"
+ description: "Whether to disable AspectJ auto-weaving."
+ default: "`false`"
+ - key: "grails.spring.placeholder.prefix"
+ description: "The prefix for property placeholder resolution."
+ default: "`${`"
+ - key: "grails.spring.transactionManagement.proxies"
+ description: "Whether to enable Spring proxy-based transaction management since @Transactional uses an AST transform and proxies are typically redundant."
+ default: "`false`"
+ - key: "grails.plugin.includes"
+ description: "List of plugin names to include in the plugin manager (all others excluded)."
+ default: "`[]` _(all plugins)_"
+ - key: "grails.plugin.excludes"
+ description: "List of plugin names to exclude from the plugin manager."
+ default: "`[]`"
+ - name: "Web & Controllers"
+ properties:
+ - key: "grails.controllers.defaultScope"
+ description: "The default scope for controllers (singleton, prototype, session)."
+ default: "`singleton`"
+ - key: "grails.controllers.upload.location"
+ description: "The directory for temporary file uploads."
+ default: "`System.getProperty('java.io.tmpdir')`"
+ - key: "grails.controllers.upload.maxFileSize"
+ description: "Maximum file size for uploads (in bytes)."
+ default: "`1048576` (1 MB)"
+ - key: "grails.controllers.upload.maxRequestSize"
+ description: "Maximum request size for multipart uploads (in bytes)."
+ default: "`10485760` (10 MB)"
+ - key: "grails.controllers.upload.fileSizeThreshold"
+ description: "File size threshold (in bytes) above which uploads are written to disk."
+ default: "`0`"
+ - key: "grails.web.url.converter"
+ description: "The URL token converter strategy, use hyphenated for hyphen-separated URLs."
+ default: "`camelCase`"
+ - key: "grails.web.linkGenerator.useCache"
+ description: "Whether to cache links generated by the link generator."
+ default: "`true`"
+ - key: "grails.web.servlet.path"
+ description: "The path the Grails dispatcher servlet is mapped to."
+ default: "`/+++*+++`"
+ - key: "grails.filter.encoding"
+ description: "The character encoding for the Grails character encoding filter."
+ default: "`UTF-8`"
+ - key: "grails.filter.forceEncoding"
+ description: "Whether to force the encoding filter to set the encoding on the response."
+ default: "`true`"
+ - key: "grails.exceptionresolver.logRequestParameters"
+ description: "Whether to log request parameters in exception stack traces."
+ default: "`true`"
+ - key: "grails.exceptionresolver.params.exclude"
+ description: "List of parameter names to mask (replace with [*****]) in exception stack traces, typically used for password and creditCard."
+ default: "`[]`"
+ - key: "grails.logging.stackTraceFiltererClass"
+ description: "Fully qualified class name of a custom StackTraceFilterer implementation."
+ default: "`org.grails.exceptions.reporting.DefaultStackTraceFilterer`"
+ - name: "CORS"
+ properties:
+ - key: "grails.cors.enabled"
+ description: "Whether CORS support is enabled."
+ default: "`false`"
+ - key: "grails.cors.filter"
+ description: "Whether CORS is handled via a servlet filter (true) or an interceptor (false)."
+ default: "`true`"
+ - key: "grails.cors.allowedOrigins"
+ description: "List of allowed origins (e.g., http://localhost:5000), only applies when grails.cors.enabled is true."
+ default: "`['*']`"
+ - key: "grails.cors.allowedMethods"
+ description: "List of allowed HTTP methods."
+ default: "`['*']`"
+ - key: "grails.cors.allowedHeaders"
+ description: "List of allowed request headers."
+ default: "`['*']`"
+ - key: "grails.cors.exposedHeaders"
+ description: "List of response headers to expose to the client."
+ default: "`[]`"
+ - key: "grails.cors.maxAge"
+ description: "How long (in seconds) the preflight response can be cached."
+ default: "`1800`"
+ - key: "grails.cors.allowCredentials"
+ description: "Whether credentials (cookies, authorization headers) are supported."
+ default: "`false`"
+ - key: "grails.cors.mappings"
+ description: "Map of URL patterns to per-path CORS configuration where defining any mapping disables the global /** mapping."
+ default: "`{}` _(global `/**` mapping)_"
+ - name: "Views & GSP"
+ properties:
+ - key: "grails.views.default.codec"
+ description: "The default encoding codec for GSP output where html reduces XSS risk and options are none, html, base64."
+ default: "`none`"
+ - key: "grails.views.gsp.encoding"
+ description: "The file encoding for GSP source files."
+ default: "`UTF-8`"
+ - key: "grails.views.gsp.htmlcodec"
+ description: "The HTML codec for GSP output (xml or html)."
+ default: "`xml`"
+ - key: "grails.views.gsp.codecs.expression"
+ description: "The codec applied to GSP ${} expressions."
+ default: "`html`"
+ - key: "grails.views.gsp.codecs.scriptlet"
+ description: "The codec applied to GSP <% %> scriptlet output."
+ default: "`html`"
+ - key: "grails.views.gsp.codecs.taglib"
+ description: "The codec applied to tag library output."
+ default: "`none`"
+ - key: "grails.views.gsp.codecs.staticparts"
+ description: "The codec applied to static HTML parts of GSP pages."
+ default: "`none`"
+ - key: "grails.views.gsp.layout.preprocess"
+ description: "Whether GSP layout preprocessing is enabled, where disabling allows Grails to parse rendered HTML but slows rendering."
+ default: "`true`"
+ - key: "grails.views.enable.jsessionid"
+ description: "Whether to include the jsessionid in rendered links."
+ default: "`false`"
+ - key: "grails.views.filteringCodecForContentType"
+ description: "Map of content types to encoding codecs."
+ default: "`{}`"
+ - key: "grails.gsp.disable.caching.resources"
+ description: "Whether to disable GSP resource caching."
+ default: "`false`"
+ - key: "grails.gsp.enable.reload"
+ description: "Whether to enable GSP reloading in production."
+ default: "`false`"
+ - key: "grails.gsp.view.dir"
+ description: "Custom directory for GSP view resolution."
+ default: "`grails-app/views`"
+ - name: "Content Negotiation & MIME Types"
+ properties:
+ - key: "grails.mime.types"
+ description: "Map of MIME type names to content type strings used for content negotiation."
+ default: "_(see web profile `application.yml`)_"
+ - key: "grails.mime.file.extensions"
+ description: "Whether to use the file extension to determine the MIME type in content negotiation."
+ default: "`true`"
+ - key: "grails.mime.use.accept.header"
+ description: "Whether to use the Accept header for content negotiation."
+ default: "`true`"
+ - key: "grails.mime.disable.accept.header.userAgents"
+ description: "List of user agent substrings (e.g., Gecko, WebKit) for which Accept header processing is disabled."
+ default: "`[]`"
+ - key: "grails.mime.disable.accept.header.userAgentsXhr"
+ description: "When true, XHR requests also respect the grails.mime.disable.accept.header.userAgents setting, while by default XHR requests ignore user agent filtering."
+ default: "`false`"
+ - key: "grails.converters.encoding"
+ description: "The character encoding for converter output (JSON or XML)."
+ default: "`UTF-8`"
+ - name: "Data Binding"
+ properties:
+ - key: "grails.databinding.trimStrings"
+ description: "Whether to trim whitespace from String values during data binding."
+ default: "`true`"
+ - key: "grails.databinding.convertEmptyStringsToNull"
+ description: "Whether empty String values are converted to null during data binding."
+ default: "`true`"
+ - key: "grails.databinding.autoGrowCollectionLimit"
+ description: "The maximum size to which indexed collections can auto-grow during data binding."
+ default: "`256`"
+ - key: "grails.databinding.dateFormats"
+ description: "List of date format strings used to parse date values during data binding."
+ default: "`[\"yyyy-MM-dd HH:mm:ss.S\", \"yyyy-MM-dd'T'HH:mm:ss'Z'\", \"yyyy-MM-dd HH:mm:ss.S z\", \"yyyy-MM-dd'T'HH:mm:ssX\"]`"
+ - key: "grails.databinding.dateParsingLenient"
+ description: "Whether date parsing is lenient (accepting invalid dates like Feb 30)."
+ default: "`false`"
+ - name: "Internationalization"
+ properties:
+ - key: "grails.i18n.cache.seconds"
+ description: "How long (in seconds) to cache resolved message bundles with -1 to cache indefinitely and 0 to disable caching."
+ default: "`-1`"
+ - key: "grails.i18n.filecache.seconds"
+ description: "How long (in seconds) to cache the message bundle file lookup with -1 to cache indefinitely."
+ default: "`-1`"
+ - name: "Static Resources"
+ properties:
+ - key: "grails.resources.enabled"
+ description: "Whether serving static files from src/main/resources/public is enabled."
+ default: "`true`"
+ - key: "grails.resources.pattern"
+ description: "The URL path pattern for serving static resources."
+ default: "`/static/**`"
+ - key: "grails.resources.cachePeriod"
+ description: "The cache period (in seconds) for static resource HTTP responses."
+ default: "`0` _(no caching)_"
+ - name: "URL Mappings"
+ properties:
+ - key: "grails.urlmapping.cache.maxsize"
+ description: "The maximum size of the URL mapping cache."
+ default: "`1000`"
+ - name: "Scaffolding"
+ properties:
+ - key: "grails.scaffolding.templates.domainSuffix"
+ description: "The suffix appended to domain class names when generating scaffolding templates."
+ default: "`\"\"`"
+ - name: "Development & Reloading"
+ properties:
+ - key: "grails.reload.includes"
+ description: "List of fully qualified class names to include in development reloading, when set only these classes are reloaded."
+ default: "`[]` _(all project classes)_"
+ - key: "grails.reload.excludes"
+ description: "List of fully qualified class names to exclude from development reloading."
+ default: "`[]`"
+ - name: "Events"
+ properties:
+ - key: "grails.events.spring"
+ description: "Whether to bridge GORM/Grails events to the Spring ApplicationEventPublisher, allowing EventListener methods to receive domain events."
+ default: "`true`"
+ - name: "JSON & Converters"
+ properties:
+ - key: "grails.json.legacy.builder"
+ description: "Whether to use the legacy JSON builder."
+ default: "`false`"
+ - key: "grails.converters.json.domain.include.class"
+ description: "Whether to include the class property when marshalling domain objects to JSON."
+ default: "`false`"
+ - key: "grails.converters.xml.domain.include.class"
+ description: "Whether to include the class attribute when marshalling domain objects to XML."
+ default: "`false`"
+ - name: "GORM"
+ properties:
+ - key: "grails.gorm.failOnError"
+ description: "When true, save() throws ValidationException on validation failure instead of returning null and can also be a list of package names to apply selectively."
+ default: "`false`"
+ - key: "grails.gorm.autoFlush"
+ description: "Whether to automatically flush the Hibernate session between queries."
+ default: "`false`"
+ - key: "grails.gorm.flushMode"
+ description: "The default Hibernate flush mode (AUTO, COMMIT, MANUAL)."
+ default: "`AUTO`"
+ - key: "grails.gorm.markDirty"
+ description: "Whether to mark a domain instance as dirty on an explicit save() call."
+ default: "`true`"
+ - key: "grails.gorm.autowire"
+ description: "Whether to autowire Spring beans into domain class instances."
+ default: "`true`"
+ - key: "grails.gorm.default.mapping"
+ description: "A closure applied as the default mapping block for all domain classes."
+ default: "`{}`"
+ - key: "grails.gorm.default.constraints"
+ description: "A closure applied as the default constraints for all domain classes."
+ default: "`{}`"
+ - key: "grails.gorm.custom.types"
+ description: "Map of custom GORM types."
+ default: "`{}`"
+ - key: "grails.gorm.reactor.events"
+ description: "Whether to translate GORM events into Reactor events, which is disabled by default for performance."
+ default: "`false`"
+ - key: "grails.gorm.events.autoTimestampInsertOverwrite"
+ description: "Whether auto-timestamp (dateCreated) overwrites a user-provided value on insert."
+ default: "`true`"
+ - key: "grails.gorm.multiTenancy.mode"
+ description: "The multi-tenancy mode: DISCRIMINATOR, DATABASE, SCHEMA, or NONE."
+ default: "`NONE`"
+ - key: "grails.gorm.multiTenancy.tenantResolverClass"
+ description: "Fully qualified class name of the TenantResolver implementation."
+ default: "_(required when mode is not NONE)_"
+ - name: "DataSource"
+ properties:
+ - key: "dataSource.driverClassName"
+ description: "The JDBC driver class name."
+ default: "`org.h2.Driver`"
+ - key: "dataSource.username"
+ description: "The database username."
+ default: "`sa`"
+ - key: "dataSource.password"
+ description: "The database password."
+ default: "`''`"
+ - key: "dataSource.url"
+ description: "The JDBC connection URL."
+ default: "`jdbc:h2:mem:devDb` (dev)"
+ - key: "dataSource.dbCreate"
+ description: "The schema generation strategy: create-drop, create, update, validate, or none, use none in production with a migration tool."
+ default: "`create-drop` (dev), `none` (prod)"
+ - key: "dataSource.pooled"
+ description: "Whether to use a connection pool."
+ default: "`true`"
+ - key: "dataSource.logSql"
+ description: "Whether to log SQL statements to stdout."
+ default: "`false`"
+ - key: "dataSource.formatSql"
+ description: "Whether to format logged SQL for readability."
+ default: "`false`"
+ - key: "dataSource.dialect"
+ description: "The Hibernate dialect class name or class."
+ default: "_(auto-detected from driver)_"
+ - key: "dataSource.readOnly"
+ description: "Whether the DataSource is read-only (calls setReadOnly(true) on connections)."
+ default: "`false`"
+ - key: "dataSource.transactional"
+ description: "For additional datasources, whether to include in the chained transaction manager."
+ default: "`true`"
+ - key: "dataSource.persistenceInterceptor"
+ description: "For additional datasources, whether to wire up the persistence interceptor (the default datasource is always wired)."
+ default: "`false`"
+ - key: "dataSource.jmxExport"
+ description: "Whether to register JMX MBeans for the DataSource."
+ default: "`true`"
+ - key: "dataSource.type"
+ description: "The connection pool implementation class when multiple are on the classpath."
+ default: "`com.zaxxer.hikari.HikariDataSource`"
+ - name: "Hibernate"
+ properties:
+ - key: "hibernate.cache.queries"
+ description: "Whether to cache Hibernate queries."
+ default: "`false`"
+ - key: "hibernate.cache.use_second_level_cache"
+ description: "Whether to enable Hibernate's second-level cache."
+ default: "`false`"
+ - key: "hibernate.cache.use_query_cache"
+ description: "Whether to enable Hibernate's query cache."
+ default: "`false`"
+ - name: "Database Migration Plugin"
+ properties:
+ - key: "grails.plugin.databasemigration.updateOnStart"
+ description: "Whether to automatically apply pending migrations on application startup."
+ default: "`false`"
+ - key: "grails.plugin.databasemigration.updateAllOnStart"
+ description: "Whether to apply migrations for all datasources on startup (overrides per-datasource updateOnStart)."
+ default: "`false`"
+ - key: "grails.plugin.databasemigration.updateOnStartFileName"
+ description: "The changelog filename to use when applying migrations on startup."
+ default: "`changelog.groovy` (default ds), `changelog-<name>.groovy` (named ds)"
+ - key: "grails.plugin.databasemigration.dropOnStart"
+ description: "Whether to drop and recreate the schema before applying migrations on startup."
+ default: "`false`"
+ - key: "grails.plugin.databasemigration.updateOnStartContexts"
+ description: "List of Liquibase contexts to apply during startup migration."
+ default: "`[]` _(all contexts)_"
+ - key: "grails.plugin.databasemigration.updateOnStartLabels"
+ description: "List of Liquibase labels to apply during startup migration."
+ default: "`[]` _(all labels)_"
+ - key: "grails.plugin.databasemigration.updateOnStartDefaultSchema"
+ description: "The default schema to use when applying migrations on startup."
+ default: "_(database default schema)_"
+ - key: "grails.plugin.databasemigration.databaseChangeLogTableName"
+ description: "Custom name for the Liquibase changelog tracking table."
+ default: "`DATABASECHANGELOG`"
+ - key: "grails.plugin.databasemigration.databaseChangeLogLockTableName"
+ description: "Custom name for the Liquibase lock table."
+ default: "`DATABASECHANGELOGLOCK`"
+ - key: "grails.plugin.databasemigration.changelogLocation"
+ description: "The directory containing migration changelog files."
+ default: "`grails-app/migrations`"
+ - key: "grails.plugin.databasemigration.changelogFileName"
+ description: "The default changelog filename for CLI commands."
+ default: "`changelog.groovy` (default ds), `changelog-<name>.groovy` (named ds)"
+ - key: "grails.plugin.databasemigration.contexts"
+ description: "List of Liquibase contexts for CLI commands."
+ default: "`[]` _(all contexts)_"
+ - key: "grails.plugin.databasemigration.excludeObjects"
+ description: "Comma-separated list of database object names to exclude from dbm-gorm-diff and dbm-generate-changelog output, cannot be combined with includeObjects."
+ default: "`''`"
+ - key: "grails.plugin.databasemigration.includeObjects"
+ description: "Comma-separated list of database object names to include in diff and changelog output (all others excluded), cannot be combined with excludeObjects."
+ default: "`''`"
+ - key: "grails.plugin.databasemigration.skipUpdateOnStartMainClasses"
+ description: "List of main class names that should skip auto-migration on startup (e.g., CLI command runners)."
+ default: "`['grails.ui.command.GrailsApplicationContextCommandRunner']`"
+ - name: "Cache Plugin"
+ properties:
+ - key: "grails.cache.enabled"
+ description: "Whether the cache plugin is enabled."
+ default: "`true`"
+ - key: "grails.cache.cleanAtStartup"
+ description: "Whether to clear all caches on application startup."
+ default: "`false`"
+ - key: "grails.cache.cacheManager"
+ description: "Fully qualified class name of the CacheManager implementation."
+ default: "`grails.plugin.cache.GrailsConcurrentMapCacheManager`"
+ - key: "grails.cache.clearAtStartup"
+ description: "Alias for cleanAtStartup (both are supported)."
+ default: "`false`"
+ - key: "grails.cache.ehcache.ehcacheXmlLocation"
+ description: "Classpath location of the ehcache.xml configuration file."
+ default: "`classpath:ehcache.xml`"
+ - key: "grails.cache.ehcache.lockTimeout"
+ description: "Timeout (in milliseconds) for cache lock acquisition."
+ default: "`200`"
+ - name: "Asset Pipeline Plugin"
+ properties:
+ - key: "grails.assets.mapping"
+ description: "The URL path segment for serving assets (e.g., /assets/*) and must be one level deep where foo is valid and foo/bar is not."
+ default: "`assets`"
+ - key: "grails.assets.bundle"
+ description: "Whether assets are bundled in development mode where false loads individual files for easier debugging."
+ default: "`false`"
+ - key: "grails.assets.url"
+ description: "Base URL for assets, useful for CDN integration (e.g., https://cdn.example.com/) and can also be a Closure accepting HttpServletRequest for dynamic URL generation in application.groovy."
+ default: "_(derived from request)_"
+ - key: "grails.assets.storagePath"
+ description: "Directory path to copy compiled assets on application startup (e.g., for CDN upload)."
+ default: "_(none)_"
+ - key: "grails.assets.useManifest"
+ description: "Whether to use the manifest.properties file for asset resolution in production."
+ default: "`true`"
+ - key: "grails.assets.skipNotFound"
+ description: "If true, missing assets pass through to the next filter instead of returning 404."
+ default: "`false`"
+ - key: "grails.assets.allowDebugParam"
+ description: "If true, allows ?_debugAssets=y query parameter to force non-bundled mode in production for debugging."
+ default: "`false`"
+ - key: "grails.assets.cacheLocation"
+ description: "Directory for caching compiled assets during development."
+ default: "`build/assetCache`"
+ - key: "grails.assets.minifyJs"
+ description: "Whether to minify JavaScript using Google Closure Compiler."
+ default: "`true`"
+ - key: "grails.assets.minifyCss"
+ description: "Whether to minify CSS."
+ default: "`true`"
+ - key: "grails.assets.enableSourceMaps"
+ description: "Whether to generate source maps for minified JavaScript files (.js.map)."
+ default: "`true`"
+ - key: "grails.assets.enableDigests"
+ description: "Whether to generate digest or fingerprinted filenames (e.g., app-abc123.js)."
+ default: "`true`"
+ - key: "grails.assets.skipNonDigests"
+ description: "If true, only digested filenames are generated and non-digested names are served via manifest mapping, reducing storage by 50%."
+ default: "`true`"
+ - key: "grails.assets.enableGzip"
+ description: "Whether to generate gzipped versions of assets (.gz files)."
+ default: "`true`"
+ - key: "grails.assets.excludesGzip"
+ description: "List of GLOB patterns for files to exclude from gzip compression (e.g., ['**/*.png', '**/*.jpg'])."
+ default: "`['+++**+++/+++*+++.png', '+++**+++/+++*+++.jpg']`"
+ - key: "grails.assets.minifyOptions"
+ description: "Map of options passed to Google Closure Compiler with keys languageMode, targetLanguage, and optimizationLevel."
+ default: "`{languageMode: 'ES5', targetLanguage: 'ES5', optimizationLevel: 'SIMPLE'}`"
+ - key: "grails.assets.excludes"
+ description: "List of GLOB patterns (or regex: prefixed) for files to exclude from compilation, excluded files can still be included via require directives."
+ default: "`[]`"
+ - key: "grails.assets.includes"
+ description: "List of GLOB patterns to override excludes and allow specific files to be compiled even if they match an exclude pattern."
+ default: "`[]`"
+ - key: "grails.assets.enableES6"
+ description: "Enable ES6+ transpilation via Babel/SWC where if not set it auto-detects ES6 syntax."
+ default: "_(auto-detect)_"
+ - key: "grails.assets.commonJs"
+ description: "Whether to enable CommonJS module support (require() / module.exports)."
+ default: "`true`"
+ - key: "grails.assets.nodeEnv"
+ description: "Value injected as process.env.NODE_ENV in JavaScript files (used by libraries like React)."
+ default: "`development`"
+ - name: "Spring Security Plugin"
+ properties:
+ - key: "grails.plugin.springsecurity.active"
+ description: "Whether the security plugin is active."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.printStatusMessages"
+ description: "Whether to print startup and status messages to the console."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.rejectIfNoRule"
+ description: "Whether to reject requests if no matching security rule is found."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.securityConfigType"
+ description: "The security configuration strategy (Annotation, Requestmap, InterceptUrlMap)."
+ default: "`Annotation`"
+ - key: "grails.plugin.springsecurity.roleHierarchy"
+ description: "The role hierarchy definition string."
+ default: "`''`"
+ - key: "grails.plugin.springsecurity.cacheUsers"
+ description: "Whether to cache user details in the user details service."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.useHttpSessionEventPublisher"
+ description: "Whether to register a HttpSessionEventPublisher bean."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.useSecurityEventListener"
+ description: "Whether to publish security events to the Grails event system."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.userLookup.userDomainClassName"
+ description: "The fully qualified name of the user domain class."
+ default: "`null` _(must be set)_"
+ - key: "grails.plugin.springsecurity.userLookup.usernamePropertyName"
+ description: "The property name for the username in the user domain class."
+ default: "`username`"
+ - key: "grails.plugin.springsecurity.userLookup.enabledPropertyName"
+ description: "The property name for the enabled status in the user domain class."
+ default: "`enabled`"
+ - key: "grails.plugin.springsecurity.userLookup.passwordPropertyName"
+ description: "The property name for the password in the user domain class."
+ default: "`password`"
+ - key: "grails.plugin.springsecurity.userLookup.authoritiesPropertyName"
+ description: "The property name for the authorities collection in the user domain class."
+ default: "`authorities`"
+ - key: "grails.plugin.springsecurity.userLookup.accountExpiredPropertyName"
+ description: "The property name for the account expired status in the user domain class."
+ default: "`accountExpired`"
+ - key: "grails.plugin.springsecurity.userLookup.accountLockedPropertyName"
+ description: "The property name for the account locked status in the user domain class."
+ default: "`accountLocked`"
+ - key: "grails.plugin.springsecurity.userLookup.passwordExpiredPropertyName"
+ description: "The property name for the password expired status in the user domain class."
+ default: "`passwordExpired`"
+ - key: "grails.plugin.springsecurity.userLookup.authorityJoinClassName"
+ description: "The fully qualified name of the user-authority join domain class."
+ default: "`null` _(must be set)_"
+ - key: "grails.plugin.springsecurity.userLookup.usernameIgnoreCase"
+ description: "Whether to ignore case when looking up users by username."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.authority.className"
+ description: "The fully qualified name of the authority (role) domain class."
+ default: "`null` _(must be set)_"
+ - key: "grails.plugin.springsecurity.authority.nameField"
+ description: "The property name for the authority name in the authority domain class."
+ default: "`authority`"
+ - key: "grails.plugin.springsecurity.useRoleGroups"
+ description: "Whether to enable support for role groups."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.apf.filterProcessesUrl"
+ description: "The URL the authentication processing filter handles."
+ default: "`/login/authenticate`"
+ - key: "grails.plugin.springsecurity.apf.usernameParameter"
+ description: "The HTTP parameter name for the username in login requests."
+ default: "`username`"
+ - key: "grails.plugin.springsecurity.apf.passwordParameter"
+ description: "The HTTP parameter name for the password in login requests."
+ default: "`password`"
+ - key: "grails.plugin.springsecurity.apf.postOnly"
+ description: "Whether to restrict authentication requests to HTTP POST."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.apf.allowSessionCreation"
+ description: "Whether to allow the authentication filter to create a new HTTP session."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.apf.storeLastUsername"
+ description: "Whether to store the last used username in the session after a failed login."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.auth.loginFormUrl"
+ description: "The URL of the login form page."
+ default: "`/login/auth`"
+ - key: "grails.plugin.springsecurity.auth.forceHttps"
+ description: "Whether to force HTTPS for the login page."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.auth.ajaxLoginFormUrl"
+ description: "The URL of the AJAX login form."
+ default: "`/login/authAjax`"
+ - key: "grails.plugin.springsecurity.auth.useForward"
+ description: "Whether to use a forward instead of a redirect to the login page."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.successHandler.defaultTargetUrl"
+ description: "The default URL to redirect to after a successful login."
+ default: "`/`"
+ - key: "grails.plugin.springsecurity.successHandler.alwaysUseDefault"
+ description: "Whether to always redirect to the default target URL after login."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.successHandler.ajaxSuccessUrl"
+ description: "The URL used for AJAX success responses."
+ default: "`/login/ajaxSuccess`"
+ - key: "grails.plugin.springsecurity.successHandler.useReferer"
+ description: "Whether to redirect to the Referer header URL after login."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.failureHandler.defaultFailureUrl"
+ description: "The default URL to redirect to after a failed login."
+ default: "`/login/authfail?login_error=1`"
+ - key: "grails.plugin.springsecurity.failureHandler.ajaxAuthFailUrl"
+ description: "The URL used for AJAX failure responses."
+ default: "`/login/authfail?ajax=true`"
+ - key: "grails.plugin.springsecurity.failureHandler.useForward"
+ description: "Whether to use a forward for authentication failure."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.logout.afterLogoutUrl"
+ description: "The URL to redirect to after logging out."
+ default: "`/`"
+ - key: "grails.plugin.springsecurity.logout.filterProcessesUrl"
+ description: "The URL the logout filter handles."
+ default: "`/logoff`"
+ - key: "grails.plugin.springsecurity.logout.postOnly"
+ description: "Whether to restrict logout requests to HTTP POST."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.logout.invalidateHttpSession"
+ description: "Whether to invalidate the HTTP session on logout."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.logout.clearAuthentication"
+ description: "Whether to clear the authentication from the security context on logout."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.logout.redirectToReferer"
+ description: "Whether to redirect to the Referer header URL after logout."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.adh.errorPage"
+ description: "The URL of the access denied page."
+ default: "`/login/denied`"
+ - key: "grails.plugin.springsecurity.adh.ajaxErrorPage"
+ description: "The URL of the AJAX access denied page."
+ default: "`/login/ajaxDenied`"
+ - key: "grails.plugin.springsecurity.adh.useForward"
+ description: "Whether to use a forward for access denied errors."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.password.algorithm"
+ description: "The password hashing algorithm."
+ default: "`bcrypt`"
+ - key: "grails.plugin.springsecurity.password.encodeHashAsBase64"
+ description: "Whether to encode the hashed password as Base64."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.password.bcrypt.logrounds"
+ description: "The number of log rounds for the BCrypt algorithm."
+ default: "`10` _(4 in test)_"
+ - key: "grails.plugin.springsecurity.password.hash.iterations"
+ description: "The number of hash iterations for algorithms that support it."
+ default: "`10000` _(1 in test)_"
+ - key: "grails.plugin.springsecurity.rememberMe.cookieName"
+ description: "The name of the remember-me cookie."
+ default: "`grails_remember_me`"
+ - key: "grails.plugin.springsecurity.rememberMe.alwaysRemember"
+ description: "Whether to always remember the user, even if the checkbox is not checked."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.rememberMe.tokenValiditySeconds"
+ description: "The validity period (in seconds) of the remember-me token."
+ default: "`1209600` _(14 days)_"
+ - key: "grails.plugin.springsecurity.rememberMe.parameter"
+ description: "The HTTP parameter name for the remember-me checkbox."
+ default: "`remember-me`"
+ - key: "grails.plugin.springsecurity.rememberMe.key"
+ description: "The secret key used to sign remember-me cookies."
+ default: "`grailsRocks`"
+ - key: "grails.plugin.springsecurity.rememberMe.persistent"
+ description: "Whether to use persistent (database-backed) remember-me tokens."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.rememberMe.useSecureCookie"
+ description: "Whether to use the Secure flag on the remember-me cookie."
+ default: "`null`"
+ - key: "grails.plugin.springsecurity.rememberMe.persistentToken.domainClassName"
+ description: "The fully qualified name of the persistent token domain class."
+ default: "`null`"
+ - key: "grails.plugin.springsecurity.controllerAnnotations.staticRules"
+ description: "Map of static URL rules for controller-based security."
+ default: "`[]`"
+ - key: "grails.plugin.springsecurity.interceptUrlMap"
+ description: "Map of URL patterns to security rules."
+ default: "`[]`"
+ - key: "grails.plugin.springsecurity.requestMap.className"
+ description: "The fully qualified name of the Requestmap domain class."
+ default: "`null`"
+ - key: "grails.plugin.springsecurity.requestMap.urlField"
+ description: "The property name for the URL in the Requestmap domain class."
+ default: "`url`"
+ - key: "grails.plugin.springsecurity.requestMap.configAttributeField"
+ description: "The property name for the config attribute in the Requestmap domain class."
+ default: "`configAttribute`"
+ - key: "grails.plugin.springsecurity.fii.rejectPublicInvocations"
+ description: "Whether to reject invocations that do not match any security rule."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.fii.alwaysReauthenticate"
+ description: "Whether to always re-authenticate on every request."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.fii.validateConfigAttributes"
+ description: "Whether to validate configuration attributes at startup."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.fii.observeOncePerRequest"
+ description: "Whether to ensure security checks are performed only once per request."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.useSessionFixationPrevention"
+ description: "Whether to enable session fixation prevention."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.sessionFixationPrevention.migrate"
+ description: "Whether to migrate session attributes to the new session after login."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.scr.allowSessionCreation"
+ description: "Whether the security context repository is allowed to create a session."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.scr.disableUrlRewriting"
+ description: "Whether to disable URL rewriting for session IDs."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.scpf.forceEagerSessionCreation"
+ description: "Whether to force eager creation of the HTTP session."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.sch.strategyName"
+ description: "The security context holder strategy (MODE_THREADLOCAL, MODE_INHERITABLETHREADLOCAL)."
+ default: "`MODE_THREADLOCAL`"
+ - key: "grails.plugin.springsecurity.useBasicAuth"
+ description: "Whether to enable HTTP Basic authentication."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.basic.realmName"
+ description: "The realm name used in HTTP Basic authentication."
+ default: "`Grails Realm`"
+ - key: "grails.plugin.springsecurity.useSwitchUserFilter"
+ description: "Whether to enable the switch user filter for user impersonation."
+ default: "`false`"
+ - key: "grails.plugin.springsecurity.switchUser.switchUserUrl"
+ description: "The URL used to initiate a user switch."
+ default: "`/login/impersonate`"
+ - key: "grails.plugin.springsecurity.switchUser.exitUserUrl"
+ description: "The URL used to exit a user switch and return to the original user."
+ default: "`/logout/impersonate`"
+ - key: "grails.plugin.springsecurity.gsp.layoutAuth"
+ description: "The Sitemesh layout used for the authentication page."
+ default: "`main`"
+ - key: "grails.plugin.springsecurity.gsp.layoutDenied"
+ description: "The Sitemesh layout used for the access denied page."
+ default: "`main`"
+ - key: "grails.plugin.springsecurity.ajaxHeader"
+ description: "The HTTP header name used to identify AJAX requests."
+ default: "`X-Requested-With`"
+ - key: "grails.plugin.springsecurity.ipRestrictions"
+ description: "List of IP-based restriction rules."
+ default: "`[]`"
+ - key: "grails.plugin.springsecurity.portMapper.httpPort"
+ description: "The standard HTTP port used for redirecting between secure and insecure pages."
+ default: "`8080`"
+ - key: "grails.plugin.springsecurity.portMapper.httpsPort"
+ description: "The standard HTTPS port used for redirecting between secure and insecure pages."
+ default: "`8443`"
+ - key: "grails.plugin.springsecurity.dao.hideUserNotFoundExceptions"
+ description: "Whether to hide UsernameNotFoundException and instead throw BadCredentialsException."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.providerManager.eraseCredentialsAfterAuthentication"
+ description: "Whether to erase password credentials from the Authentication object after successful authentication."
+ default: "`true`"
+ - key: "grails.plugin.springsecurity.debug.useFilter"
+ description: "Whether to enable the Spring Security debug filter."
+ default: "`false`"
+ - name: "MongoDB GORM Plugin"
+ properties:
+ - key: "grails.mongodb.url"
+ description: "The MongoDB connection string."
+ default: "`mongodb://localhost/test`"
+ - key: "grails.mongodb.host"
+ description: "The MongoDB server host."
+ default: "`localhost`"
+ - key: "grails.mongodb.port"
+ description: "The MongoDB server port."
+ default: "`27017`"
+ - key: "grails.mongodb.databaseName"
+ description: "The name of the MongoDB database."
+ default: "_(application name)_"
+ - key: "grails.mongodb.username"
+ description: "The database username."
+ default: "_(none)_"
+ - key: "grails.mongodb.password"
+ description: "The database password."
+ default: "_(none)_"
+ - key: "grails.mongodb.stateless"
+ description: "Whether the GORM implementation is stateless (disabling persistence context)."
+ default: "`false`"
+ - key: "grails.mongodb.decimalType"
+ description: "Whether to use the Decimal128 type for BigDecimal properties."
+ default: "`false`"
+ - key: "grails.mongodb.codecs"
+ description: "List of custom MongoDB codec classes."
+ default: "`[]`"
+ - key: "grails.mongodb.default.mapping"
+ description: "A closure applied as the default mapping block for MongoDB domain classes."
+ default: "`{}`"
+ - key: "grails.mongodb.options.connectionPoolSettings.maxSize"
+ description: "The maximum number of connections in the pool."
+ default: "`100`"
+ - key: "grails.mongodb.options.connectionPoolSettings.minSize"
+ description: "The minimum number of connections in the pool."
+ default: "`0`"
+ - key: "grails.mongodb.options.connectionPoolSettings.maxWaitTime"
+ description: "The maximum time (in milliseconds) a thread will wait for a connection."
+ default: "`120000`"
+ - key: "grails.mongodb.options.connectionPoolSettings.maxConnectionLifeTime"
+ description: "The maximum life time (in milliseconds) of a pooled connection."
+ default: "`0` _(unlimited)_"
+ - key: "grails.mongodb.options.connectionPoolSettings.maxConnectionIdleTime"
+ description: "The maximum idle time (in milliseconds) of a pooled connection."
+ default: "`0` _(unlimited)_"
+ - key: "grails.mongodb.options.readPreference"
+ description: "The read preference strategy."
+ default: "_(none)_"
+ - key: "grails.mongodb.options.writeConcern"
+ description: "The write concern strategy."
+ default: "_(none)_"
+ - key: "grails.mongodb.options.readConcern"
+ description: "The read concern strategy."
+ default: "_(none)_"
+ - key: "grails.mongodb.options.retryWrites"
+ description: "Whether to retry write operations on failure."
+ default: "_(none)_"
+ - key: "grails.mongodb.options.retryReads"
+ description: "Whether to retry read operations on failure."
+ default: "_(none)_"
+ - key: "grails.mongodb.options.applicationName"
+ description: "The name of the application (used for logging and monitoring)."
+ default: "_(none)_"
+ - key: "grails.mongodb.options.sslSettings.enabled"
+ description: "Whether SSL is enabled for connections."
+ default: "`false`"
+ - key: "grails.mongodb.options.sslSettings.invalidHostNameAllowed"
+ description: "Whether invalid hostnames are allowed in SSL certificates."
+ default: "`false`"
diff --git a/grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy b/grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy
index 4bd4f67370..3c05ddd910 100644
--- a/grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy
+++ b/grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy
@@ -86,16 +86,20 @@ class ConfigReportCommandSpec extends Specification {
File reportFile = new File(executionContext.baseDir, ConfigReportCommand.DEFAULT_REPORT_FILE)
reportFile.exists()
- and:
+ and: "report has correct AsciiDoc structure"
String content = reportFile.text
content.contains('= Grails Application Configuration Report')
- content.contains('== grails')
- content.contains('== server')
- content.contains('== spring')
+
+ and: "known Grails properties appear in their metadata category sections"
content.contains('`grails.profile`')
- content.contains('`web`')
+ content.contains('`grails.codegen.defaultPackage`')
+
+ and: "unknown runtime properties appear in the Other Properties section"
+ content.contains('== Other Properties')
content.contains('`server.port`')
content.contains('`8080`')
+ content.contains('`spring.main.banner-mode`')
+ content.contains('`off`')
cleanup:
reportFile?.delete()
@@ -178,50 +182,90 @@ class ConfigReportCommandSpec extends Specification {
!result.containsKey('app.bad')
}
- def "writeReport groups properties by top-level namespace"() {
+ def "writeReport uses 3-column format with metadata categories"() {
given:
- Map<String, String> sorted = new TreeMap<String, String>()
- sorted.put('grails.controllers.defaultScope', 'singleton')
- sorted.put('grails.profile', 'web')
- sorted.put('server.port', '8080')
+ Map<String, String> runtimeProperties = new TreeMap<String, String>()
+ runtimeProperties.put('grails.controllers.defaultScope', 'singleton')
+ runtimeProperties.put('grails.profile', 'web')
+ runtimeProperties.put('server.port', '8080')
File reportFile = new File(tempDir, 'test-report.adoc')
when:
- command.writeReport(sorted, reportFile)
+ command.writeReport(runtimeProperties, reportFile)
then:
String content = reportFile.text
- and: "report has correct AsciiDoc structure"
+ and: "report has correct AsciiDoc header"
content.startsWith('= Grails Application Configuration Report')
content.contains(':toc: left')
- content.contains('[cols="2,3", options="header"]')
- content.contains('| Property | Value')
- and: "properties are grouped by namespace"
- content.contains('== grails')
- content.contains('== server')
+ and: "metadata categories are used as section headers"
+ content.contains('== Core Properties')
+ content.contains('== Web & Controllers')
- and: "grails section appears before server section (alphabetical)"
- content.indexOf('== grails') < content.indexOf('== server')
+ and: "3-column table format is used for known properties"
+ content.contains('[cols="2,5,2", options="header"]')
+ content.contains('| Property | Description | Default')
- and: "properties are listed under correct sections"
+ and: "known properties appear with descriptions"
+ content.contains('`grails.profile`')
content.contains('`grails.controllers.defaultScope`')
+
+ and: "runtime values override static defaults for known properties"
+ content.contains('`web`')
content.contains('`singleton`')
+
+ and: "unknown runtime properties go to Other Properties section"
+ content.contains('== Other Properties')
content.contains('`server.port`')
content.contains('`8080`')
}
+ def "writeReport shows static defaults when no runtime value exists"() {
+ given: "no runtime properties provided"
+ Map<String, String> runtimeProperties = new TreeMap<String, String>()
+ File reportFile = new File(tempDir, 'defaults-report.adoc')
+
+ when:
+ command.writeReport(runtimeProperties, reportFile)
+
+ then:
+ String content = reportFile.text
+
+ and: "metadata categories are still present with static defaults"
+ content.contains('== Core Properties')
+ content.contains('`grails.profile`')
+ content.contains('Set by project template')
+ }
+
+ def "writeReport runtime values override static defaults"() {
+ given:
+ Map<String, String> runtimeProperties = new TreeMap<String, String>()
+ runtimeProperties.put('grails.profile', 'rest-api')
+
+ File reportFile = new File(tempDir, 'override-report.adoc')
+
+ when:
+ command.writeReport(runtimeProperties, reportFile)
+
+ then:
+ String content = reportFile.text
+
+ and: "runtime value overrides the static default"
+ content.contains('`rest-api`')
+ }
+
def "writeReport escapes pipe characters in values"() {
given:
- Map<String, String> sorted = new TreeMap<String, String>()
- sorted.put('test.key', 'value|with|pipes')
+ Map<String, String> runtimeProperties = new TreeMap<String, String>()
+ runtimeProperties.put('test.key', 'value|with|pipes')
File reportFile = new File(tempDir, 'escape-test.adoc')
when:
- command.writeReport(sorted, reportFile)
+ command.writeReport(runtimeProperties, reportFile)
then:
String content = reportFile.text
@@ -229,19 +273,70 @@ class ConfigReportCommandSpec extends Specification {
!content.contains('value|with|pipes')
}
- def "writeReport handles empty configuration"() {
+ def "writeReport handles empty configuration with no Other Properties"() {
given:
- Map<String, String> sorted = new TreeMap<String, String>()
+ Map<String, String> runtimeProperties = new TreeMap<String, String>()
File reportFile = new File(tempDir, 'empty-report.adoc')
when:
- command.writeReport(sorted, reportFile)
+ command.writeReport(runtimeProperties, reportFile)
then:
reportFile.exists()
String content = reportFile.text
content.contains('= Grails Application Configuration Report')
- !content.contains('|===')
+
+ and: "metadata categories still appear from the YAML"
+ content.contains('== Core Properties')
+
+ and: "no Other Properties section when no unknown runtime properties"
+ !content.contains('== Other Properties')
+ }
+
+ def "writeReport puts only unknown runtime properties in Other Properties"() {
+ given:
+ Map<String, String> runtimeProperties = new TreeMap<String, String>()
+ runtimeProperties.put('custom.app.setting', 'myvalue')
+ runtimeProperties.put('grails.profile', 'web')
+
+ File reportFile = new File(tempDir, 'other-props-report.adoc')
+
+ when:
+ command.writeReport(runtimeProperties, reportFile)
+
+ then:
+ String content = reportFile.text
+
+ and: "known property is in its category, not in Other Properties"
+ content.contains('== Core Properties')
+ content.contains('`grails.profile`')
+
+ and: "unknown property appears in Other Properties"
+ content.contains('== Other Properties')
+ content.contains('`custom.app.setting`')
+ content.contains('`myvalue`')
+
+ and: "Other Properties uses 2-column format"
+ int otherIdx = content.indexOf('== Other Properties')
+ String otherSection = content.substring(otherIdx)
+ otherSection.contains('[cols="2,3", options="header"]')
+ otherSection.contains('| Property | Default')
+ }
+
+ def "loadPropertyMetadata returns properties from classpath YAML"() {
+ when:
+ Map<String, Map<String, String>> metadata = command.loadPropertyMetadata()
+
+ then: "metadata is loaded from the config-properties.yml on the classpath"
+ !metadata.isEmpty()
+ metadata.containsKey('grails.profile')
+
+ and: "each entry has the expected fields"
+ Map<String, String> profileEntry = metadata.get('grails.profile')
+ profileEntry.get('key') == 'grails.profile'
+ profileEntry.get('description') != null
+ profileEntry.get('description').length() > 0
+ profileEntry.get('category') == 'Core Properties'
}
def "escapeAsciidoc handles null and empty strings"() {
diff --git a/grails-test-examples/config-report/src/integration-test/groovy/configreport/ConfigReportCommandIntegrationSpec.groovy b/grails-test-examples/config-report/src/integration-test/groovy/configreport/ConfigReportCommandIntegrationSpec.groovy
index fd85880a79..19a4362141 100644
--- a/grails-test-examples/config-report/src/integration-test/groovy/configreport/ConfigReportCommandIntegrationSpec.groovy
+++ b/grails-test-examples/config-report/src/integration-test/groovy/configreport/ConfigReportCommandIntegrationSpec.groovy
@@ -37,9 +37,14 @@ import spock.lang.TempDir
* <li>{@code application.groovy} - Groovy-based configuration</li>
* <li>{@code @ConfigurationProperties} - Type-safe configuration beans</li>
* </ul>
+ *
+ * <p>The hybrid report uses curated property metadata (from {@code config-properties.yml})
+ * to produce a 3-column AsciiDoc table (Property | Description | Default) for known
+ * Grails properties, with runtime values overriding static defaults. Properties not
+ * found in the metadata appear in a separate "Other Properties" section.
*/
@Integration
-@Narrative('Verifies that ConfigReportCommand generates an AsciiDoc report containing properties from application.yml, application.groovy, and @ConfigurationProperties sources')
+@Narrative('Verifies that ConfigReportCommand generates a hybrid AsciiDoc report merging static property metadata with runtime-collected values')
class ConfigReportCommandIntegrationSpec extends Specification {
@Autowired
@@ -57,14 +62,7 @@ class ConfigReportCommandIntegrationSpec extends Specification {
return command
}
- private File executeCommand(ConfigReportCommand command) {
- ExecutionContext executionContext = new ExecutionContext(Mock(CommandLine))
- File reportFile = new File(executionContext.baseDir, ConfigReportCommand.DEFAULT_REPORT_FILE)
- command.handle(executionContext)
- return reportFile
- }
-
- def "ConfigReportCommand generates a report file"() {
+ def "ConfigReportCommand generates a report file with hybrid format"() {
given: 'a ConfigReportCommand wired to the live application context'
ConfigReportCommand command = createCommand()
@@ -82,18 +80,18 @@ class ConfigReportCommandIntegrationSpec extends Specification {
reportFile.exists()
reportFile.length() > 0
- and: 'the report has valid AsciiDoc structure'
+ and: 'the report has valid AsciiDoc structure with 3-column format'
String content = reportFile.text
content.startsWith('= Grails Application Configuration Report')
content.contains(':toc: left')
- content.contains('[cols="2,3", options="header"]')
- content.contains('| Property | Value')
+ content.contains('[cols="2,5,2", options="header"]')
+ content.contains('| Property | Description | Default')
cleanup:
reportFile?.delete()
}
- def "report contains properties from application.yml"() {
+ def "report shows known Grails properties in metadata categories"() {
given: 'a ConfigReportCommand wired to the live application context'
ConfigReportCommand command = createCommand()
ExecutionContext executionContext = new ExecutionContext(Mock(CommandLine))
@@ -103,29 +101,49 @@ class ConfigReportCommandIntegrationSpec extends Specification {
command.handle(executionContext)
String content = reportFile.text
- then: 'YAML-defined properties are present in the report'
+ then: 'known metadata categories are present as section headers'
+ content.contains('== Core Properties')
+ content.contains('== Web & Controllers')
+ content.contains('== DataSource')
+
+ and: 'grails.profile appears in the Core Properties section with its description'
+ content.contains('`grails.profile`')
+
+ and: 'runtime value overrides the static default for grails.profile'
+ content.contains('`web`')
+
+ cleanup:
+ reportFile?.delete()
+ }
+
+ def "report puts custom application properties in Other Properties section"() {
+ given: 'a ConfigReportCommand wired to the live application context'
+ ConfigReportCommand command = createCommand()
+ ExecutionContext executionContext = new ExecutionContext(Mock(CommandLine))
+ File reportFile = new File(executionContext.baseDir, ConfigReportCommand.DEFAULT_REPORT_FILE)
+
+ when: 'the command is executed'
+ command.handle(executionContext)
+ String content = reportFile.text
+
+ then: 'YAML-defined custom properties appear in Other Properties'
+ content.contains('== Other Properties')
content.contains('`myapp.yaml.greeting`')
content.contains('`Hello from YAML`')
- and: 'YAML numeric properties are present'
+ and: 'YAML numeric properties are in Other Properties'
content.contains('`myapp.yaml.maxRetries`')
content.contains('`5`')
- and: 'YAML nested properties are present'
+ and: 'YAML nested properties are in Other Properties'
content.contains('`myapp.yaml.feature.enabled`')
- content.contains('`true`')
content.contains('`myapp.yaml.feature.timeout`')
- content.contains('`30000`')
-
- and: 'standard Grails YAML properties are present'
- content.contains('`grails.profile`')
- content.contains('`web`')
cleanup:
reportFile?.delete()
}
- def "report contains properties from application.groovy"() {
+ def "report contains properties from application.groovy in Other Properties"() {
given: 'a ConfigReportCommand wired to the live application context'
ConfigReportCommand command = createCommand()
ExecutionContext executionContext = new ExecutionContext(Mock(CommandLine))
@@ -135,7 +153,7 @@ class ConfigReportCommandIntegrationSpec extends Specification {
command.handle(executionContext)
String content = reportFile.text
- then: 'Groovy config properties are present in the report'
+ then: 'Groovy config properties are present in Other Properties'
content.contains('`myapp.groovy.appName`')
content.contains('`Config Report Test App`')
@@ -166,7 +184,7 @@ class ConfigReportCommandIntegrationSpec extends Specification {
reportFile?.delete()
}
- def "report contains properties bound via @ConfigurationProperties"() {
+ def "report contains properties bound via @ConfigurationProperties in Other Properties"() {
given: 'a ConfigReportCommand wired to the live application context'
ConfigReportCommand command = createCommand()
ExecutionContext executionContext = new ExecutionContext(Mock(CommandLine))
@@ -181,7 +199,7 @@ class ConfigReportCommandIntegrationSpec extends Specification {
appProperties.pageSize == 50
appProperties.debugEnabled == true
- and: 'the typed properties appear in the config report'
+ and: 'the typed properties appear in Other Properties'
content.contains('`myapp.typed.name`')
content.contains('`Configured App`')
content.contains('`myapp.typed.pageSize`')
@@ -193,7 +211,7 @@ class ConfigReportCommandIntegrationSpec extends Specification {
reportFile?.delete()
}
- def "report groups properties by top-level namespace"() {
+ def "report separates known metadata properties from custom properties"() {
given: 'a ConfigReportCommand wired to the live application context'
ConfigReportCommand command = createCommand()
ExecutionContext executionContext = new ExecutionContext(Mock(CommandLine))
@@ -203,20 +221,27 @@ class ConfigReportCommandIntegrationSpec extends Specification {
command.handle(executionContext)
String content = reportFile.text
- then: 'properties are organized into namespace sections'
- content.contains('== grails')
- content.contains('== myapp')
- content.contains('== dataSource')
+ then: 'known Grails properties appear in categorized sections before Other Properties'
+ int coreIdx = content.indexOf('== Core Properties')
+ int otherIdx = content.indexOf('== Other Properties')
+ coreIdx >= 0
+ otherIdx >= 0
+ coreIdx < otherIdx
+
+ and: 'grails.profile is in the Core Properties section (not Other Properties)'
+ String otherSection = content.substring(otherIdx)
+ !otherSection.contains('`grails.profile`')
- and: 'sections are in alphabetical order'
- content.indexOf('== dataSource') < content.indexOf('== grails')
- content.indexOf('== grails') < content.indexOf('== myapp')
+ and: 'custom myapp properties are in Other Properties (not in categorized sections)'
+ String beforeOther = content.substring(0, otherIdx)
+ !beforeOther.contains('`myapp.yaml.greeting`')
+ !beforeOther.contains('`myapp.groovy.appName`')
cleanup:
reportFile?.delete()
}
- def "report contains properties from all three config sources simultaneously"() {
+ def "report contains properties from all three config sources"() {
given: 'a ConfigReportCommand wired to the live application context'
ConfigReportCommand command = createCommand()
ExecutionContext executionContext = new ExecutionContext(Mock(CommandLine))
@@ -235,17 +260,11 @@ class ConfigReportCommandIntegrationSpec extends Specification {
and: 'typed @ConfigurationProperties are present'
content.contains('`myapp.typed.name`')
- and: 'all properties are in the same myapp section'
- int myappSectionIndex = content.indexOf('== myapp')
- myappSectionIndex >= 0
-
- and: 'each table row has the correct AsciiDoc format'
- content.contains('| `myapp.yaml.greeting`')
- content.contains('| `Hello from YAML`')
- content.contains('| `myapp.groovy.appName`')
- content.contains('| `Config Report Test App`')
- content.contains('| `myapp.typed.name`')
- content.contains('| `Configured App`')
+ and: 'Other Properties section uses 2-column format'
+ int otherIdx = content.indexOf('== Other Properties')
+ String otherSection = content.substring(otherIdx)
+ otherSection.contains('[cols="2,3", options="header"]')
+ otherSection.contains('| Property | Default')
cleanup:
reportFile?.delete()