From c10b7da360a747a20f9b375ca64be9d8f18c8776 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Tue, 17 Feb 2026 17:09:09 -0500 Subject: [PATCH 01/11] docs: add Configuration Reference to Quick Reference section Add a comprehensive Application Properties reference page under ref/Configuration/ listing all Grails-specific configuration keys with descriptions and default values. Covers core framework, GORM, DataSource, Hibernate, Database Migration Plugin, Cache Plugin, Asset Pipeline Plugin, Spring Security Plugin, and MongoDB GORM Plugin. All external links use doc variables ({springBootVersion}, {hibernate5Reference}) for version-safe URLs. Assisted-by: OpenCode Assisted-by: Claude --- .../Configuration/Application Properties.adoc | 1357 +++++++++++++++++ 1 file changed, 1357 insertions(+) create mode 100644 grails-doc/src/en/ref/Configuration/Application Properties.adoc diff --git a/grails-doc/src/en/ref/Configuration/Application Properties.adoc b/grails-doc/src/en/ref/Configuration/Application Properties.adoc new file mode 100644 index 00000000000..4a45e9708f2 --- /dev/null +++ b/grails-doc/src/en/ref/Configuration/Application Properties.adoc @@ -0,0 +1,1357 @@ +//// +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. +//// + +== Application Properties + +A comprehensive reference of all configuration properties specific to Grails and its bundled plugins. These properties are set in `grails-app/conf/application.yml` (or `application.groovy`). + +Since Grails is built on Spring Boot, all https://docs.spring.io/spring-boot/{springBootVersion}/appendix/application-properties/index.html[Spring Boot Common Application Properties] are also available (for example `server.port`, `logging.*`, `spring.datasource.hikari.*`, and `management.*`). This reference covers only Grails-specific properties. + +=== Core Properties + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.profile` +| The active Grails application profile (e.g., `web`, `rest-api`, `plugin`). +| Set by project template + +| `grails.codegen.defaultPackage` +| The default package used when generating artefacts with `grails create-*` commands. +| Set by project template + +| `grails.serverURL` +| The server URL used to generate absolute links (e.g., `\https://my.app.com`). Also used by redirects. +| _(derived from request)_ + +| `grails.enable.native2ascii` +| Whether to perform native2ascii conversion of i18n properties files. +| `true` + +| `grails.bootstrap.skip` +| Whether to skip execution of `BootStrap.groovy` classes on startup. +| `false` + +| `grails.spring.bean.packages` +| List of packages to scan for Spring beans. +| `[]` + +| `grails.spring.disable.aspectj.autoweaving` +| Whether to disable AspectJ auto-weaving. +| `false` + +| `grails.spring.placeholder.prefix` +| The prefix for property placeholder resolution. +| `${` + +| `grails.spring.transactionManagement.proxies` +| Whether to enable Spring proxy-based transaction management. Since `@Transactional` uses an AST transform, proxies are typically redundant. +| `false` + +| `grails.plugin.includes` +| List of plugin names to include in the plugin manager (all others excluded). +| `[]` _(all plugins)_ + +| `grails.plugin.excludes` +| List of plugin names to exclude from the plugin manager. +| `[]` +|=== + +=== Web & Controllers + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.controllers.defaultScope` +| The default scope for controllers (`singleton`, `prototype`, `session`). +| `singleton` + +| `grails.controllers.upload.location` +| The directory for temporary file uploads. +| `System.getProperty('java.io.tmpdir')` + +| `grails.controllers.upload.maxFileSize` +| Maximum file size for uploads (in bytes). +| `1048576` (1 MB) + +| `grails.controllers.upload.maxRequestSize` +| Maximum request size for multipart uploads (in bytes). +| `10485760` (10 MB) + +| `grails.controllers.upload.fileSizeThreshold` +| File size threshold (in bytes) above which uploads are written to disk. +| `0` + +| `grails.web.url.converter` +| The URL token converter strategy. Use `hyphenated` for hyphen-separated URLs. +| `camelCase` + +| `grails.web.linkGenerator.useCache` +| Whether to cache links generated by the link generator. +| `true` + +| `grails.web.servlet.path` +| The path the Grails dispatcher servlet is mapped to. +| `/+++*+++` + +| `grails.filter.encoding` +| The character encoding for the Grails character encoding filter. +| `UTF-8` + +| `grails.filter.forceEncoding` +| Whether to force the encoding filter to set the encoding on the response. +| `true` + +| `grails.exceptionresolver.logRequestParameters` +| Whether to log request parameters in exception stack traces. +| `true` + +| `grails.exceptionresolver.params.exclude` +| List of parameter names to mask (replace with `[*****]`) in exception stack traces. Typically used for `password`, `creditCard`, etc. +| `[]` + +| `grails.logging.stackTraceFiltererClass` +| Fully qualified class name of a custom `StackTraceFilterer` implementation. +| `org.grails.exceptions.reporting.DefaultStackTraceFilterer` +|=== + +=== CORS + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.cors.enabled` +| Whether CORS support is enabled. +| `false` + +| `grails.cors.filter` +| Whether CORS is handled via a servlet filter (`true`) or an interceptor (`false`). +| `true` + +| `grails.cors.allowedOrigins` +| List of allowed origins (e.g., `\http://localhost:5000`). Only applies when `grails.cors.enabled` is `true`. +| `['*']` + +| `grails.cors.allowedMethods` +| List of allowed HTTP methods. +| `['*']` + +| `grails.cors.allowedHeaders` +| List of allowed request headers. +| `['*']` + +| `grails.cors.exposedHeaders` +| List of response headers to expose to the client. +| `[]` + +| `grails.cors.maxAge` +| How long (in seconds) the preflight response can be cached. +| `1800` + +| `grails.cors.allowCredentials` +| Whether credentials (cookies, authorization headers) are supported. +| `false` + +| `grails.cors.mappings` +| Map of URL patterns to per-path CORS configuration. Defining any mapping disables the global `/**` mapping. +| `{}` _(global `/**` mapping)_ +|=== + +=== Views & GSP + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.views.default.codec` +| The default encoding codec for GSP output. Set to `html` to reduce XSS risk. Options: `none`, `html`, `base64`. +| `none` + +| `grails.views.gsp.encoding` +| The file encoding for GSP source files. +| `UTF-8` + +| `grails.views.gsp.htmlcodec` +| The HTML codec for GSP output (`xml` or `html`). +| `xml` + +| `grails.views.gsp.codecs.expression` +| The codec applied to GSP `${}` expressions. +| `html` + +| `grails.views.gsp.codecs.scriptlet` +| The codec applied to GSP `<% %>` scriptlet output. +| `html` + +| `grails.views.gsp.codecs.taglib` +| The codec applied to tag library output. +| `none` + +| `grails.views.gsp.codecs.staticparts` +| The codec applied to static HTML parts of GSP pages. +| `none` + +| `grails.views.gsp.layout.preprocess` +| Whether GSP layout preprocessing is enabled. Disabling allows Grails to parse rendered HTML but slows rendering. +| `true` + +| `grails.views.enable.jsessionid` +| Whether to include the `jsessionid` in rendered links. +| `false` + +| `grails.views.filteringCodecForContentType` +| Map of content types to encoding codecs. +| `{}` + +| `grails.gsp.disable.caching.resources` +| Whether to disable GSP resource caching. +| `false` + +| `grails.gsp.enable.reload` +| Whether to enable GSP reloading in production. +| `false` + +| `grails.gsp.view.dir` +| Custom directory for GSP view resolution. +| `grails-app/views` +|=== + +=== Content Negotiation & MIME Types + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.mime.types` +| Map of MIME type names to content type strings used for content negotiation. +| _(see web profile `application.yml`)_ + +| `grails.mime.file.extensions` +| Whether to use the file extension to determine the MIME type in content negotiation. +| `true` + +| `grails.mime.use.accept.header` +| Whether to use the `Accept` header for content negotiation. +| `true` + +| `grails.mime.disable.accept.header.userAgents` +| List of user agent substrings (e.g., `Gecko`, `WebKit`) for which `Accept` header processing is disabled. +| `[]` + +| `grails.mime.disable.accept.header.userAgentsXhr` +| When `true`, XHR requests also respect the `grails.mime.disable.accept.header.userAgents` setting. By default, XHR requests ignore user agent filtering. +| `false` + +| `grails.converters.encoding` +| The character encoding for converter output (JSON/XML). +| `UTF-8` +|=== + +=== Data Binding + +These properties configure Grails' data binding behavior. Defined via `@ConfigurationProperties("grails.databinding")`. + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.databinding.trimStrings` +| Whether to trim whitespace from String values during data binding. +| `true` + +| `grails.databinding.convertEmptyStringsToNull` +| Whether empty String values are converted to `null` during data binding. +| `true` + +| `grails.databinding.autoGrowCollectionLimit` +| The maximum size to which indexed collections can auto-grow during data binding. +| `256` + +| `grails.databinding.dateFormats` +| List of date format strings used to parse date values during data binding. +| `["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"]` + +| `grails.databinding.dateParsingLenient` +| Whether date parsing is lenient (accepting invalid dates like Feb 30). +| `false` +|=== + +=== Internationalization + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.i18n.cache.seconds` +| How long (in seconds) to cache resolved message bundles. Set to `-1` to cache indefinitely, `0` to disable caching. +| `-1` + +| `grails.i18n.filecache.seconds` +| How long (in seconds) to cache the message bundle file lookup. Set to `-1` to cache indefinitely. +| `-1` +|=== + +=== Static Resources + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.resources.enabled` +| Whether serving static files from `src/main/resources/public` is enabled. +| `true` + +| `grails.resources.pattern` +| The URL path pattern for serving static resources. +| `/static/**` + +| `grails.resources.cachePeriod` +| The cache period (in seconds) for static resource HTTP responses. +| `0` _(no caching)_ +|=== + +=== URL Mappings + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.urlmapping.cache.maxsize` +| The maximum size of the URL mapping cache. +| `1000` +|=== + +=== Scaffolding + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.scaffolding.templates.domainSuffix` +| The suffix appended to domain class names when generating scaffolding templates. +| `""` +|=== + +=== Development & Reloading + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.reload.includes` +| List of fully qualified class names to include in development reloading. When set, only these classes are reloaded. +| `[]` _(all project classes)_ + +| `grails.reload.excludes` +| List of fully qualified class names to exclude from development reloading. +| `[]` +|=== + +=== Events + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.events.spring` +| Whether to bridge GORM/Grails events to the Spring `ApplicationEventPublisher`, allowing `@EventListener` methods to receive domain events. +| `true` +|=== + +=== JSON & Converters + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.json.legacy.builder` +| Whether to use the legacy JSON builder. +| `false` + +| `grails.converters.json.domain.include.class` +| Whether to include the `class` property when marshalling domain objects to JSON. +| `false` + +| `grails.converters.xml.domain.include.class` +| Whether to include the `class` attribute when marshalling domain objects to XML. +| `false` +|=== + +=== GORM + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.gorm.failOnError` +| When `true`, `save()` throws `ValidationException` on validation failure instead of returning `null`. Can also be a list of package names to apply selectively. +| `false` + +| `grails.gorm.autoFlush` +| Whether to automatically flush the Hibernate session between queries. +| `false` + +| `grails.gorm.flushMode` +| The default Hibernate flush mode (`AUTO`, `COMMIT`, `MANUAL`). +| `AUTO` + +| `grails.gorm.markDirty` +| Whether to mark a domain instance as dirty on an explicit `save()` call. +| `true` + +| `grails.gorm.autowire` +| Whether to autowire Spring beans into domain class instances. +| `true` + +| `grails.gorm.default.mapping` +| A closure applied as the default mapping block for all domain classes. +| `{}` + +| `grails.gorm.default.constraints` +| A closure applied as the default constraints for all domain classes. +| `{}` + +| `grails.gorm.custom.types` +| Map of custom GORM types. +| `{}` + +| `grails.gorm.reactor.events` +| Whether to translate GORM events into Reactor events (disabled by default for performance). +| `false` + +| `grails.gorm.events.autoTimestampInsertOverwrite` +| Whether auto-timestamp (`dateCreated`) overwrites a user-provided value on insert. +| `true` +|=== + +==== Multi-Tenancy + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.gorm.multiTenancy.mode` +| The multi-tenancy mode: `DISCRIMINATOR`, `DATABASE`, `SCHEMA`, or `NONE`. +| `NONE` + +| `grails.gorm.multiTenancy.tenantResolverClass` +| Fully qualified class name of the `TenantResolver` implementation. +| _(required when mode is not NONE)_ +|=== + +=== DataSource + +Spring Boot auto-configures the connection pool. https://github.com/brettwooldridge/HikariCP[HikariCP] is the default and preferred pool. Pool-specific tuning is configured via `spring.datasource.hikari.*` - see the https://docs.spring.io/spring-boot/{springBootVersion}/reference/data/sql.html#data.sql.datasource.connection-pool[Spring Boot Connection Pool documentation]. + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `dataSource.driverClassName` +| The JDBC driver class name. +| `org.h2.Driver` + +| `dataSource.username` +| The database username. +| `sa` + +| `dataSource.password` +| The database password. +| `''` + +| `dataSource.url` +| The JDBC connection URL. +| `jdbc:h2:mem:devDb` (dev) + +| `dataSource.dbCreate` +| The schema generation strategy: `create-drop`, `create`, `update`, `validate`, or `none`. Use `none` in production with a migration tool. +| `create-drop` (dev), `none` (prod) + +| `dataSource.pooled` +| Whether to use a connection pool. +| `true` + +| `dataSource.logSql` +| Whether to log SQL statements to stdout. +| `false` + +| `dataSource.formatSql` +| Whether to format logged SQL for readability. +| `false` + +| `dataSource.dialect` +| The Hibernate dialect class name or class. +| _(auto-detected from driver)_ + +| `dataSource.readOnly` +| Whether the DataSource is read-only (calls `setReadOnly(true)` on connections). +| `false` + +| `dataSource.transactional` +| For additional (non-default) datasources: whether to include in the chained transaction manager. +| `true` + +| `dataSource.persistenceInterceptor` +| For additional datasources: whether to wire up the persistence interceptor (the default datasource is always wired). +| `false` + +| `dataSource.jmxExport` +| Whether to register JMX MBeans for the DataSource. +| `true` + +| `dataSource.type` +| The connection pool implementation class when multiple are on the classpath. +| `com.zaxxer.hikari.HikariDataSource` +|=== + +Additional datasources are configured under the `dataSources` key. Each named datasource supports the same properties as the default `dataSource`. + +[source,yaml] +---- +dataSources: + lookup: + pooled: true + jmxExport: true + driverClassName: org.h2.Driver + url: jdbc:h2:mem:simpleDb +---- + +=== Hibernate + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `hibernate.cache.queries` +| Whether to cache Hibernate queries. +| `false` + +| `hibernate.cache.use_second_level_cache` +| Whether to enable Hibernate's second-level cache. +| `false` + +| `hibernate.cache.use_query_cache` +| Whether to enable Hibernate's query cache. +| `false` +|=== + +NOTE: All standard {hibernate5Reference}#configurations[Hibernate configuration properties] are supported under the `hibernate` key. + +=== Database Migration Plugin + +These properties are read from the `grails.plugin.databasemigration` namespace. For multi-datasource setups, per-datasource configuration uses `grails.plugin.databasemigration.` (e.g., `grails.plugin.databasemigration.analytics.updateOnStart`). + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.plugin.databasemigration.updateOnStart` +| Whether to automatically apply pending migrations on application startup. +| `false` + +| `grails.plugin.databasemigration.updateAllOnStart` +| Whether to apply migrations for all datasources on startup (overrides per-datasource `updateOnStart`). +| `false` + +| `grails.plugin.databasemigration.updateOnStartFileName` +| The changelog filename to use when applying migrations on startup. +| `changelog.groovy` (default ds), `changelog-.groovy` (named ds) + +| `grails.plugin.databasemigration.dropOnStart` +| Whether to drop and recreate the schema before applying migrations on startup. +| `false` + +| `grails.plugin.databasemigration.updateOnStartContexts` +| List of Liquibase contexts to apply during startup migration. +| `[]` _(all contexts)_ + +| `grails.plugin.databasemigration.updateOnStartLabels` +| List of Liquibase labels to apply during startup migration. +| `[]` _(all labels)_ + +| `grails.plugin.databasemigration.updateOnStartDefaultSchema` +| The default schema to use when applying migrations on startup. +| _(database default schema)_ + +| `grails.plugin.databasemigration.databaseChangeLogTableName` +| Custom name for the Liquibase changelog tracking table. +| `DATABASECHANGELOG` + +| `grails.plugin.databasemigration.databaseChangeLogLockTableName` +| Custom name for the Liquibase lock table. +| `DATABASECHANGELOGLOCK` + +| `grails.plugin.databasemigration.changelogLocation` +| The directory containing migration changelog files. +| `grails-app/migrations` + +| `grails.plugin.databasemigration.changelogFileName` +| The default changelog filename for CLI commands. +| `changelog.groovy` (default ds), `changelog-.groovy` (named ds) + +| `grails.plugin.databasemigration.contexts` +| List of Liquibase contexts for CLI commands. +| `[]` _(all contexts)_ + +| `grails.plugin.databasemigration.excludeObjects` +| Comma-separated list of database object names to exclude from `dbm-gorm-diff` and `dbm-generate-changelog` output. Cannot be combined with `includeObjects`. +| `''` + +| `grails.plugin.databasemigration.includeObjects` +| Comma-separated list of database object names to include in diff/changelog output (all others excluded). Cannot be combined with `excludeObjects`. +| `''` + +| `grails.plugin.databasemigration.skipUpdateOnStartMainClasses` +| List of main class names that should skip auto-migration on startup (e.g., CLI command runners). +| `['grails.ui.command.GrailsApplicationContextCommandRunner']` +|=== + +=== Cache Plugin + +The Grails Cache plugin provides annotation-driven caching (`@Cacheable`, `@CacheEvict`, `@CachePut`). It is included as a default feature in generated Grails applications. + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.cache.enabled` +| Whether the cache plugin is enabled. +| `true` + +| `grails.cache.cleanAtStartup` +| Whether to clear all caches on application startup. +| `false` + +| `grails.cache.cacheManager` +| Fully qualified class name of the `CacheManager` implementation. +| `grails.plugin.cache.GrailsConcurrentMapCacheManager` + +| `grails.cache.clearAtStartup` +| Alias for `cleanAtStartup` (both are supported). +| `false` +|=== + +When using EHCache as the cache provider, additional properties are available: + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.cache.ehcache.ehcacheXmlLocation` +| Classpath location of the `ehcache.xml` configuration file. +| `classpath:ehcache.xml` + +| `grails.cache.ehcache.lockTimeout` +| Timeout (in milliseconds) for cache lock acquisition. +| `200` +|=== + +=== Asset Pipeline Plugin + +The https://github.com/wondrify/asset-pipeline[Asset Pipeline plugin] manages and processes static assets (JavaScript, CSS, images) in Grails applications. It is included as a default feature in generated web applications. These properties are set under the `grails.assets` namespace. + +NOTE: Build-time properties (minification, digests, gzip) can also be configured in `build.gradle` via the `assets {}` block. The `application.yml` values shown here apply at runtime. + +==== Runtime Behavior + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.assets.mapping` +| The URL path segment for serving assets (e.g., `/assets/+++*+++`). Must be one level deep - `foo` is valid, `foo/bar` is not. +| `assets` + +| `grails.assets.bundle` +| Whether assets are bundled in development mode. When `false`, individual files are loaded separately for easier debugging. +| `false` + +| `grails.assets.url` +| Base URL for assets. Useful for CDN integration (e.g., `\https://cdn.example.com/`). Can also be a `Closure` accepting `HttpServletRequest` for dynamic URL generation in `application.groovy`. +| _(derived from request)_ + +| `grails.assets.storagePath` +| Directory path to copy compiled assets on application startup (e.g., for CDN upload). +| _(none)_ + +| `grails.assets.useManifest` +| Whether to use the `manifest.properties` file for asset resolution in production. +| `true` + +| `grails.assets.skipNotFound` +| If `true`, missing assets pass through to the next filter instead of returning 404. +| `false` + +| `grails.assets.allowDebugParam` +| If `true`, allows `?_debugAssets=y` query parameter to force non-bundled mode in production for debugging. +| `false` + +| `grails.assets.cacheLocation` +| Directory for caching compiled assets during development. +| `build/assetCache` +|=== + +==== Minification & Optimization + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.assets.minifyJs` +| Whether to minify JavaScript using Google Closure Compiler. +| `true` + +| `grails.assets.minifyCss` +| Whether to minify CSS. +| `true` + +| `grails.assets.enableSourceMaps` +| Whether to generate source maps for minified JavaScript files (`.js.map`). +| `true` + +| `grails.assets.enableDigests` +| Whether to generate digest/fingerprinted filenames (e.g., `app-abc123.js`). +| `true` + +| `grails.assets.skipNonDigests` +| If `true`, only digested filenames are generated. Non-digested names are served via manifest mapping. Reduces storage by 50%. +| `true` + +| `grails.assets.enableGzip` +| Whether to generate gzipped versions of assets (`.gz` files). +| `true` + +| `grails.assets.excludesGzip` +| List of GLOB patterns for files to exclude from gzip compression (e.g., `['+++**+++/+++*+++.png', '+++**+++/+++*+++.jpg']`). +| `[]` + +| `grails.assets.minifyOptions` +| Map of options passed to Google Closure Compiler. Keys: `languageMode` (input), `targetLanguage` (output), `optimizationLevel` (`SIMPLE`, `ADVANCED`, `WHITESPACE_ONLY`). +| `{languageMode: 'ES5', targetLanguage: 'ES5', optimizationLevel: 'SIMPLE'}` +|=== + +==== File Inclusion & Exclusion + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.assets.excludes` +| List of GLOB patterns (or `regex:` prefixed) for files to exclude from compilation. Excluded files can still be included via `require` directives. +| `[]` + +| `grails.assets.includes` +| List of GLOB patterns to override excludes. Allows specific files to be compiled even if they match an exclude pattern. +| `[]` +|=== + +==== JavaScript Processing + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.assets.enableES6` +| Enable ES6+ transpilation via Babel/SWC. If not set, auto-detects ES6 syntax. +| _(auto-detect)_ + +| `grails.assets.commonJs` +| Whether to enable CommonJS module support (`require()` / `module.exports`). +| `true` + +| `grails.assets.nodeEnv` +| Value injected as `process.env.NODE_ENV` in JavaScript files (used by libraries like React). +| `development` +|=== + +=== Spring Security Plugin + +The https://grails-plugins.github.io/grails-spring-security-core/[Grails Spring Security plugin] provides comprehensive authentication and authorization support for Grails applications. Detailed documentation is available in the plugin's full reference guide. + +==== General Settings + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.plugin.springsecurity.active` +| Whether the security plugin is active. +| `true` + +| `grails.plugin.springsecurity.printStatusMessages` +| Whether to print startup and status messages to the console. +| `true` + +| `grails.plugin.springsecurity.rejectIfNoRule` +| Whether to reject requests if no matching security rule is found. +| `true` + +| `grails.plugin.springsecurity.securityConfigType` +| The security configuration strategy (`Annotation`, `Requestmap`, `InterceptUrlMap`). +| `Annotation` + +| `grails.plugin.springsecurity.roleHierarchy` +| The role hierarchy definition string. +| `''` + +| `grails.plugin.springsecurity.cacheUsers` +| Whether to cache user details in the user details service. +| `false` + +| `grails.plugin.springsecurity.useHttpSessionEventPublisher` +| Whether to register a `HttpSessionEventPublisher` bean. +| `false` + +| `grails.plugin.springsecurity.useSecurityEventListener` +| Whether to publish security events to the Grails event system. +| `false` +|=== + +==== User & Role Configuration + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.plugin.springsecurity.userLookup.userDomainClassName` +| The fully qualified name of the user domain class. +| `null` _(must be set)_ + +| `grails.plugin.springsecurity.userLookup.usernamePropertyName` +| The property name for the username in the user domain class. +| `username` + +| `grails.plugin.springsecurity.userLookup.enabledPropertyName` +| The property name for the enabled status in the user domain class. +| `enabled` + +| `grails.plugin.springsecurity.userLookup.passwordPropertyName` +| The property name for the password in the user domain class. +| `password` + +| `grails.plugin.springsecurity.userLookup.authoritiesPropertyName` +| The property name for the authorities collection in the user domain class. +| `authorities` + +| `grails.plugin.springsecurity.userLookup.accountExpiredPropertyName` +| The property name for the account expired status in the user domain class. +| `accountExpired` + +| `grails.plugin.springsecurity.userLookup.accountLockedPropertyName` +| The property name for the account locked status in the user domain class. +| `accountLocked` + +| `grails.plugin.springsecurity.userLookup.passwordExpiredPropertyName` +| The property name for the password expired status in the user domain class. +| `passwordExpired` + +| `grails.plugin.springsecurity.userLookup.authorityJoinClassName` +| The fully qualified name of the user-authority join domain class. +| `null` _(must be set)_ + +| `grails.plugin.springsecurity.userLookup.usernameIgnoreCase` +| Whether to ignore case when looking up users by username. +| `false` + +| `grails.plugin.springsecurity.authority.className` +| The fully qualified name of the authority (role) domain class. +| `null` _(must be set)_ + +| `grails.plugin.springsecurity.authority.nameField` +| The property name for the authority name in the authority domain class. +| `authority` + +| `grails.plugin.springsecurity.useRoleGroups` +| Whether to enable support for role groups. +| `false` +|=== + +==== Authentication + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.plugin.springsecurity.apf.filterProcessesUrl` +| The URL the authentication processing filter handles. +| `/login/authenticate` + +| `grails.plugin.springsecurity.apf.usernameParameter` +| The HTTP parameter name for the username in login requests. +| `username` + +| `grails.plugin.springsecurity.apf.passwordParameter` +| The HTTP parameter name for the password in login requests. +| `password` + +| `grails.plugin.springsecurity.apf.postOnly` +| Whether to restrict authentication requests to HTTP POST. +| `true` + +| `grails.plugin.springsecurity.apf.allowSessionCreation` +| Whether to allow the authentication filter to create a new HTTP session. +| `true` + +| `grails.plugin.springsecurity.apf.storeLastUsername` +| Whether to store the last used username in the session after a failed login. +| `false` +|=== + +==== Login & Logout + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.plugin.springsecurity.auth.loginFormUrl` +| The URL of the login form page. +| `/login/auth` + +| `grails.plugin.springsecurity.auth.forceHttps` +| Whether to force HTTPS for the login page. +| `false` + +| `grails.plugin.springsecurity.auth.ajaxLoginFormUrl` +| The URL of the AJAX login form. +| `/login/authAjax` + +| `grails.plugin.springsecurity.auth.useForward` +| Whether to use a forward instead of a redirect to the login page. +| `false` + +| `grails.plugin.springsecurity.successHandler.defaultTargetUrl` +| The default URL to redirect to after a successful login. +| `/` + +| `grails.plugin.springsecurity.successHandler.alwaysUseDefault` +| Whether to always redirect to the default target URL after login. +| `false` + +| `grails.plugin.springsecurity.successHandler.ajaxSuccessUrl` +| The URL used for AJAX success responses. +| `/login/ajaxSuccess` + +| `grails.plugin.springsecurity.successHandler.useReferer` +| Whether to redirect to the `Referer` header URL after login. +| `false` + +| `grails.plugin.springsecurity.failureHandler.defaultFailureUrl` +| The default URL to redirect to after a failed login. +| `/login/authfail?login_error=1` + +| `grails.plugin.springsecurity.failureHandler.ajaxAuthFailUrl` +| The URL used for AJAX failure responses. +| `/login/authfail?ajax=true` + +| `grails.plugin.springsecurity.failureHandler.useForward` +| Whether to use a forward for authentication failure. +| `false` + +| `grails.plugin.springsecurity.logout.afterLogoutUrl` +| The URL to redirect to after logging out. +| `/` + +| `grails.plugin.springsecurity.logout.filterProcessesUrl` +| The URL the logout filter handles. +| `/logoff` + +| `grails.plugin.springsecurity.logout.postOnly` +| Whether to restrict logout requests to HTTP POST. +| `true` + +| `grails.plugin.springsecurity.logout.invalidateHttpSession` +| Whether to invalidate the HTTP session on logout. +| `true` + +| `grails.plugin.springsecurity.logout.clearAuthentication` +| Whether to clear the authentication from the security context on logout. +| `true` + +| `grails.plugin.springsecurity.logout.redirectToReferer` +| Whether to redirect to the `Referer` header URL after logout. +| `false` + +| `grails.plugin.springsecurity.adh.errorPage` +| The URL of the access denied page. +| `/login/denied` + +| `grails.plugin.springsecurity.adh.ajaxErrorPage` +| The URL of the AJAX access denied page. +| `/login/ajaxDenied` + +| `grails.plugin.springsecurity.adh.useForward` +| Whether to use a forward for access denied errors. +| `true` +|=== + +==== Password Encoding + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.plugin.springsecurity.password.algorithm` +| The password hashing algorithm. +| `bcrypt` + +| `grails.plugin.springsecurity.password.encodeHashAsBase64` +| Whether to encode the hashed password as Base64. +| `false` + +| `grails.plugin.springsecurity.password.bcrypt.logrounds` +| The number of log rounds for the BCrypt algorithm. +| `10` _(4 in test)_ + +| `grails.plugin.springsecurity.password.hash.iterations` +| The number of hash iterations for algorithms that support it. +| `10000` _(1 in test)_ +|=== + +==== Remember Me + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.plugin.springsecurity.rememberMe.cookieName` +| The name of the remember-me cookie. +| `grails_remember_me` + +| `grails.plugin.springsecurity.rememberMe.alwaysRemember` +| Whether to always remember the user, even if the checkbox is not checked. +| `false` + +| `grails.plugin.springsecurity.rememberMe.tokenValiditySeconds` +| The validity period (in seconds) of the remember-me token. +| `1209600` _(14 days)_ + +| `grails.plugin.springsecurity.rememberMe.parameter` +| The HTTP parameter name for the remember-me checkbox. +| `remember-me` + +| `grails.plugin.springsecurity.rememberMe.key` +| The secret key used to sign remember-me cookies. +| `grailsRocks` + +| `grails.plugin.springsecurity.rememberMe.persistent` +| Whether to use persistent (database-backed) remember-me tokens. +| `false` + +| `grails.plugin.springsecurity.rememberMe.useSecureCookie` +| Whether to use the `Secure` flag on the remember-me cookie. +| `null` + +| `grails.plugin.springsecurity.rememberMe.persistentToken.domainClassName` +| The fully qualified name of the persistent token domain class. +| `null` +|=== + +==== URL Mapping & Access Control + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.plugin.springsecurity.controllerAnnotations.staticRules` +| Map of static URL rules for controller-based security. +| `[]` + +| `grails.plugin.springsecurity.interceptUrlMap` +| Map of URL patterns to security rules. +| `[]` + +| `grails.plugin.springsecurity.requestMap.className` +| The fully qualified name of the `Requestmap` domain class. +| `null` + +| `grails.plugin.springsecurity.requestMap.urlField` +| The property name for the URL in the `Requestmap` domain class. +| `url` + +| `grails.plugin.springsecurity.requestMap.configAttributeField` +| The property name for the config attribute in the `Requestmap` domain class. +| `configAttribute` + +| `grails.plugin.springsecurity.fii.rejectPublicInvocations` +| Whether to reject invocations that do not match any security rule. +| `true` + +| `grails.plugin.springsecurity.fii.alwaysReauthenticate` +| Whether to always re-authenticate on every request. +| `false` + +| `grails.plugin.springsecurity.fii.validateConfigAttributes` +| Whether to validate configuration attributes at startup. +| `true` + +| `grails.plugin.springsecurity.fii.observeOncePerRequest` +| Whether to ensure security checks are performed only once per request. +| `true` +|=== + +==== Session & Security Context + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.plugin.springsecurity.useSessionFixationPrevention` +| Whether to enable session fixation prevention. +| `true` + +| `grails.plugin.springsecurity.sessionFixationPrevention.migrate` +| Whether to migrate session attributes to the new session after login. +| `true` + +| `grails.plugin.springsecurity.scr.allowSessionCreation` +| Whether the security context repository is allowed to create a session. +| `true` + +| `grails.plugin.springsecurity.scr.disableUrlRewriting` +| Whether to disable URL rewriting for session IDs. +| `true` + +| `grails.plugin.springsecurity.scpf.forceEagerSessionCreation` +| Whether to force eager creation of the HTTP session. +| `false` + +| `grails.plugin.springsecurity.sch.strategyName` +| The security context holder strategy (`MODE_THREADLOCAL`, `MODE_INHERITABLETHREADLOCAL`). +| `MODE_THREADLOCAL` +|=== + +==== Additional Authentication Methods + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.plugin.springsecurity.useBasicAuth` +| Whether to enable HTTP Basic authentication. +| `false` + +| `grails.plugin.springsecurity.basic.realmName` +| The realm name used in HTTP Basic authentication. +| `Grails Realm` + +| `grails.plugin.springsecurity.useSwitchUserFilter` +| Whether to enable the switch user filter for user impersonation. +| `false` + +| `grails.plugin.springsecurity.switchUser.switchUserUrl` +| The URL used to initiate a user switch. +| `/login/impersonate` + +| `grails.plugin.springsecurity.switchUser.exitUserUrl` +| The URL used to exit a user switch and return to the original user. +| `/logout/impersonate` +|=== + +==== GSP Layouts + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.plugin.springsecurity.gsp.layoutAuth` +| The Sitemesh layout used for the authentication page. +| `main` + +| `grails.plugin.springsecurity.gsp.layoutDenied` +| The Sitemesh layout used for the access denied page. +| `main` +|=== + +==== Other + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.plugin.springsecurity.ajaxHeader` +| The HTTP header name used to identify AJAX requests. +| `X-Requested-With` + +| `grails.plugin.springsecurity.ipRestrictions` +| List of IP-based restriction rules. +| `[]` + +| `grails.plugin.springsecurity.portMapper.httpPort` +| The standard HTTP port used for redirecting between secure and insecure pages. +| `8080` + +| `grails.plugin.springsecurity.portMapper.httpsPort` +| The standard HTTPS port used for redirecting between secure and insecure pages. +| `8443` + +| `grails.plugin.springsecurity.dao.hideUserNotFoundExceptions` +| Whether to hide `UsernameNotFoundException` and instead throw `BadCredentialsException`. +| `true` + +| `grails.plugin.springsecurity.providerManager.eraseCredentialsAfterAuthentication` +| Whether to erase password credentials from the `Authentication` object after successful authentication. +| `true` + +| `grails.plugin.springsecurity.debug.useFilter` +| Whether to enable the Spring Security debug filter. +| `false` +|=== + +=== MongoDB GORM Plugin + +The MongoDB GORM plugin provides GORM support for the MongoDB document database. Properties under `grails.mongodb.options.*` use reflection to map to the `MongoClientSettings.Builder` class. + +==== Connection + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.mongodb.url` +| The MongoDB connection string. +| `mongodb://localhost/test` + +| `grails.mongodb.host` +| The MongoDB server host. +| `localhost` + +| `grails.mongodb.port` +| The MongoDB server port. +| `27017` + +| `grails.mongodb.databaseName` +| The name of the MongoDB database. +| _(application name)_ + +| `grails.mongodb.username` +| The database username. +| _(none)_ + +| `grails.mongodb.password` +| The database password. +| _(none)_ +|=== + +==== Behavior + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.mongodb.stateless` +| Whether the GORM implementation is stateless (disabling persistence context). +| `false` + +| `grails.mongodb.decimalType` +| Whether to use the `Decimal128` type for `BigDecimal` properties. +| `false` + +| `grails.mongodb.codecs` +| List of custom MongoDB codec classes. +| `[]` + +| `grails.mongodb.default.mapping` +| A closure applied as the default mapping block for MongoDB domain classes. +| `{}` +|=== + +==== Connection Pool + +These properties are set under `grails.mongodb.options.connectionPoolSettings.*`. + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.mongodb.options.connectionPoolSettings.maxSize` +| The maximum number of connections in the pool. +| `100` + +| `grails.mongodb.options.connectionPoolSettings.minSize` +| The minimum number of connections in the pool. +| `0` + +| `grails.mongodb.options.connectionPoolSettings.maxWaitTime` +| The maximum time (in milliseconds) a thread will wait for a connection. +| `120000` + +| `grails.mongodb.options.connectionPoolSettings.maxConnectionLifeTime` +| The maximum life time (in milliseconds) of a pooled connection. +| `0` _(unlimited)_ + +| `grails.mongodb.options.connectionPoolSettings.maxConnectionIdleTime` +| The maximum idle time (in milliseconds) of a pooled connection. +| `0` _(unlimited)_ +|=== + +==== Client Options + +These properties are set under `grails.mongodb.options.*`. + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.mongodb.options.readPreference` +| The read preference strategy. +| _(none)_ + +| `grails.mongodb.options.writeConcern` +| The write concern strategy. +| _(none)_ + +| `grails.mongodb.options.readConcern` +| The read concern strategy. +| _(none)_ + +| `grails.mongodb.options.retryWrites` +| Whether to retry write operations on failure. +| _(none)_ + +| `grails.mongodb.options.retryReads` +| Whether to retry read operations on failure. +| _(none)_ + +| `grails.mongodb.options.applicationName` +| The name of the application (used for logging and monitoring). +| _(none)_ +|=== + +==== SSL + +These properties are set under `grails.mongodb.options.sslSettings.*`. + +[cols="3,5,2", options="header"] +|=== +| Property | Description | Default + +| `grails.mongodb.options.sslSettings.enabled` +| Whether SSL is enabled for connections. +| `false` + +| `grails.mongodb.options.sslSettings.invalidHostNameAllowed` +| Whether invalid hostnames are allowed in SSL certificates. +| `false` +|=== + + + From d3dff0efdb3ad448ccc5e26c104bfce9adddfc5f Mon Sep 17 00:00:00 2001 From: James Daugherty Date: Thu, 19 Feb 2026 11:09:38 -0500 Subject: [PATCH 02/11] proof of concept for a config report command --- .../dev/commands/ConfigReportCommand.groovy | 158 +++++++++++ .../commands/ConfigReportCommandSpec.groovy | 255 ++++++++++++++++++ .../config-report/build.gradle | 63 +++++ .../grails-app/conf/application.groovy | 29 ++ .../grails-app/conf/application.yml | 53 ++++ .../config-report/grails-app/conf/logback.xml | 10 + .../configreport/UrlMappings.groovy | 35 +++ .../init/configreport/Application.groovy | 32 +++ .../ConfigReportCommandIntegrationSpec.groovy | 254 +++++++++++++++++ .../groovy/configreport/AppProperties.groovy | 52 ++++ settings.gradle | 2 + 11 files changed, 943 insertions(+) create mode 100644 grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy create mode 100644 grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy create mode 100644 grails-test-examples/config-report/build.gradle create mode 100644 grails-test-examples/config-report/grails-app/conf/application.groovy create mode 100644 grails-test-examples/config-report/grails-app/conf/application.yml create mode 100644 grails-test-examples/config-report/grails-app/conf/logback.xml create mode 100644 grails-test-examples/config-report/grails-app/controllers/configreport/UrlMappings.groovy create mode 100644 grails-test-examples/config-report/grails-app/init/configreport/Application.groovy create mode 100644 grails-test-examples/config-report/src/integration-test/groovy/configreport/ConfigReportCommandIntegrationSpec.groovy create mode 100644 grails-test-examples/config-report/src/main/groovy/configreport/AppProperties.groovy diff --git a/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy b/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy new file mode 100644 index 00000000000..e10da603826 --- /dev/null +++ b/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy @@ -0,0 +1,158 @@ +/* + * 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 grails.dev.commands + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + +import org.springframework.core.env.ConfigurableEnvironment +import org.springframework.core.env.EnumerablePropertySource +import org.springframework.core.env.PropertySource + +/** + * An {@link ApplicationCommand} that generates an AsciiDoc report + * of the application's resolved configuration properties. + * + *

Properties are collected directly from the Spring {@link ConfigurableEnvironment}, + * iterating all {@link EnumerablePropertySource} instances to capture every + * resolvable property regardless of how it was defined (YAML, Groovy config, + * system properties, environment variables, etc.). + * + *

Usage: + *

+ *     grails config-report
+ *     ./gradlew configReport
+ * 
+ * + *

The report is written to {@code config-report.adoc} in the project's base directory. + * + * @since 7.0 + */ +@Slf4j +@CompileStatic +class ConfigReportCommand implements ApplicationCommand { + + static final String DEFAULT_REPORT_FILE = 'config-report.adoc' + + final String description = 'Generates an AsciiDoc report of the application configuration' + + @Override + boolean handle(ExecutionContext executionContext) { + try { + ConfigurableEnvironment environment = (ConfigurableEnvironment) applicationContext.getEnvironment() + Map sorted = collectProperties(environment) + + File reportFile = new File(executionContext.baseDir, DEFAULT_REPORT_FILE) + writeReport(sorted, reportFile) + + log.info('Configuration report written to {}', reportFile.absolutePath) + return true + } + catch (Throwable e) { + log.error("Failed to generate configuration report: ${e.message}", e) + return false + } + } + + /** + * Collects all configuration properties from the Spring {@link ConfigurableEnvironment} + * by iterating its {@link EnumerablePropertySource} instances. Property values are + * resolved through the environment to ensure placeholders are expanded and + * the correct precedence order is applied. + * + * @param environment the Spring environment + * @return a sorted map of property names to their resolved values + */ + Map collectProperties(ConfigurableEnvironment environment) { + Map sorted = new TreeMap() + for (PropertySource propertySource : environment.getPropertySources()) { + if (propertySource instanceof EnumerablePropertySource) { + EnumerablePropertySource enumerable = (EnumerablePropertySource) propertySource + for (String propertyName : enumerable.getPropertyNames()) { + if (!sorted.containsKey(propertyName)) { + try { + String value = environment.getProperty(propertyName) + if (value != null) { + sorted.put(propertyName, value) + } + } + catch (Exception e) { + log.debug('Could not resolve property {}: {}', propertyName, e.message) + } + } + } + } + } + sorted + } + + /** + * Writes the configuration properties as an AsciiDoc file grouped by top-level namespace. + * + * @param sorted the sorted configuration properties + * @param reportFile the file to write the report to + */ + void writeReport(Map sorted, File reportFile) { + reportFile.withWriter('UTF-8') { BufferedWriter writer -> + writer.writeLine('= Grails Application Configuration Report') + writer.writeLine(':toc: left') + writer.writeLine(':toclevels: 2') + 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('') + } + currentSection = section + writer.writeLine("== ${section}") + writer.writeLine('') + writer.writeLine('[cols="2,3", options="header"]') + writer.writeLine('|===') + writer.writeLine('| Property | Value') + writer.writeLine('') + } + writer.writeLine("| `${key}`") + writer.writeLine("| `${escapeAsciidoc(value)}`") + writer.writeLine('') + } + if (currentSection) { + writer.writeLine('|===') + } + } + } + + /** + * Escapes special AsciiDoc characters in a value string. + * + * @param value the raw value + * @return the escaped value safe for AsciiDoc table cells + */ + static String escapeAsciidoc(String value) { + if (!value) { + return value + } + value.replace('|', '\\|') + } + +} diff --git a/grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy b/grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy new file mode 100644 index 00000000000..4bd4f673705 --- /dev/null +++ b/grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy @@ -0,0 +1,255 @@ +/* + * 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 grails.dev.commands + +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.core.env.ConfigurableEnvironment +import org.springframework.core.env.MapPropertySource +import org.springframework.core.env.MutablePropertySources + +import org.grails.build.parsing.CommandLine +import spock.lang.Specification +import spock.lang.TempDir + +class ConfigReportCommandSpec extends Specification { + + @TempDir + File tempDir + + ConfigReportCommand command + + ConfigurableApplicationContext applicationContext + + ConfigurableEnvironment environment + + MutablePropertySources propertySources + + def setup() { + propertySources = new MutablePropertySources() + environment = Mock(ConfigurableEnvironment) + environment.getPropertySources() >> propertySources + applicationContext = Mock(ConfigurableApplicationContext) + applicationContext.getEnvironment() >> environment + + command = new ConfigReportCommand() + command.applicationContext = applicationContext + } + + def "command name is derived from class name"() { + expect: + command.name == 'config-report' + } + + def "command has a description"() { + expect: + command.description == 'Generates an AsciiDoc report of the application configuration' + } + + def "handle generates AsciiDoc report file"() { + given: + Map props = [ + 'grails.profile': 'web', + 'grails.codegen.defaultPackage': 'myapp', + 'server.port': '8080', + 'spring.main.banner-mode': 'off' + ] + propertySources.addFirst(new MapPropertySource('test', props)) + props.each { String key, Object value -> + environment.getProperty(key) >> value.toString() + } + + ExecutionContext executionContext = new ExecutionContext(Mock(CommandLine)) + + when: + boolean result = command.handle(executionContext) + + then: + result + + and: "report file is written to the base directory" + File reportFile = new File(executionContext.baseDir, ConfigReportCommand.DEFAULT_REPORT_FILE) + reportFile.exists() + + and: + String content = reportFile.text + content.contains('= Grails Application Configuration Report') + content.contains('== grails') + content.contains('== server') + content.contains('== spring') + content.contains('`grails.profile`') + content.contains('`web`') + content.contains('`server.port`') + content.contains('`8080`') + + cleanup: + reportFile?.delete() + } + + def "handle returns false when an error occurs"() { + given: + ConfigurableApplicationContext failingContext = Mock(ConfigurableApplicationContext) + failingContext.getEnvironment() >> { throw new RuntimeException('test error') } + ConfigReportCommand failingCommand = new ConfigReportCommand() + failingCommand.applicationContext = failingContext + ExecutionContext executionContext = new ExecutionContext(Mock(CommandLine)) + + when: + boolean result = failingCommand.handle(executionContext) + + then: + !result + } + + def "collectProperties gathers from all enumerable property sources"() { + given: + Map yamlProps = ['myapp.yaml.greeting': 'Hello'] + Map groovyProps = ['myapp.groovy.name': 'TestApp'] + propertySources.addLast(new MapPropertySource('yaml', yamlProps)) + propertySources.addLast(new MapPropertySource('groovy', groovyProps)) + environment.getProperty('myapp.yaml.greeting') >> 'Hello' + environment.getProperty('myapp.groovy.name') >> 'TestApp' + + when: + Map result = command.collectProperties(environment) + + then: + result['myapp.yaml.greeting'] == 'Hello' + result['myapp.groovy.name'] == 'TestApp' + } + + def "collectProperties respects property source precedence"() { + given: 'two sources with the same key, higher-priority source listed first' + Map overrideProps = ['app.name': 'Override'] + Map defaultProps = ['app.name': 'Default'] + propertySources.addLast(new MapPropertySource('override', overrideProps)) + propertySources.addLast(new MapPropertySource('default', defaultProps)) + environment.getProperty('app.name') >> 'Override' + + when: + Map result = command.collectProperties(environment) + + then: 'the higher-priority value wins' + result['app.name'] == 'Override' + } + + def "collectProperties skips properties that resolve to null"() { + given: + Map props = ['app.present': 'value', 'app.missing': 'placeholder'] + propertySources.addFirst(new MapPropertySource('test', props)) + environment.getProperty('app.present') >> 'value' + environment.getProperty('app.missing') >> null + + when: + Map result = command.collectProperties(environment) + + then: + result.containsKey('app.present') + !result.containsKey('app.missing') + } + + def "collectProperties handles resolution errors gracefully"() { + given: + Map props = ['app.good': 'value', 'app.bad': '${unresolved}'] + propertySources.addFirst(new MapPropertySource('test', props)) + environment.getProperty('app.good') >> 'value' + environment.getProperty('app.bad') >> { throw new IllegalArgumentException('unresolved placeholder') } + + when: + Map result = command.collectProperties(environment) + + then: 'the good property is collected and the bad one is skipped' + result['app.good'] == 'value' + !result.containsKey('app.bad') + } + + def "writeReport groups properties by top-level namespace"() { + given: + Map sorted = new TreeMap() + sorted.put('grails.controllers.defaultScope', 'singleton') + sorted.put('grails.profile', 'web') + sorted.put('server.port', '8080') + + File reportFile = new File(tempDir, 'test-report.adoc') + + when: + command.writeReport(sorted, reportFile) + + then: + String content = reportFile.text + + and: "report has correct AsciiDoc structure" + 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: "grails section appears before server section (alphabetical)" + content.indexOf('== grails') < content.indexOf('== server') + + and: "properties are listed under correct sections" + content.contains('`grails.controllers.defaultScope`') + content.contains('`singleton`') + content.contains('`server.port`') + content.contains('`8080`') + } + + def "writeReport escapes pipe characters in values"() { + given: + Map sorted = new TreeMap() + sorted.put('test.key', 'value|with|pipes') + + File reportFile = new File(tempDir, 'escape-test.adoc') + + when: + command.writeReport(sorted, reportFile) + + then: + String content = reportFile.text + content.contains('value\\|with\\|pipes') + !content.contains('value|with|pipes') + } + + def "writeReport handles empty configuration"() { + given: + Map sorted = new TreeMap() + File reportFile = new File(tempDir, 'empty-report.adoc') + + when: + command.writeReport(sorted, reportFile) + + then: + reportFile.exists() + String content = reportFile.text + content.contains('= Grails Application Configuration Report') + !content.contains('|===') + } + + def "escapeAsciidoc handles null and empty strings"() { + expect: + ConfigReportCommand.escapeAsciidoc(null) == null + ConfigReportCommand.escapeAsciidoc('') == '' + ConfigReportCommand.escapeAsciidoc('simple') == 'simple' + ConfigReportCommand.escapeAsciidoc('a|b') == 'a\\|b' + } + +} diff --git a/grails-test-examples/config-report/build.gradle b/grails-test-examples/config-report/build.gradle new file mode 100644 index 00000000000..231fef7d7da --- /dev/null +++ b/grails-test-examples/config-report/build.gradle @@ -0,0 +1,63 @@ +/* + * 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 'org.apache.grails.buildsrc.properties' + id 'org.apache.grails.buildsrc.compile' +} + +version = '0.1' +group = 'configreport' + +apply plugin: 'groovy' +apply plugin: 'org.apache.grails.gradle.grails-web' + +dependencies { + implementation platform(project(':grails-bom')) + + implementation 'org.apache.grails:grails-core' + implementation 'org.apache.grails:grails-logging' + implementation 'org.apache.grails:grails-databinding' + implementation 'org.apache.grails:grails-interceptors' + implementation 'org.apache.grails:grails-services' + implementation 'org.apache.grails:grails-url-mappings' + implementation 'org.apache.grails:grails-web-boot' + if (System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-sitemesh3' + } + else { + implementation 'org.apache.grails:grails-layout' + } + implementation 'org.apache.grails:grails-data-hibernate5' + implementation 'org.springframework.boot:spring-boot-autoconfigure' + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-logging' + implementation 'org.springframework.boot:spring-boot-starter-tomcat' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + runtimeOnly 'com.h2database:h2' + runtimeOnly 'org.apache.tomcat:tomcat-jdbc' + + testImplementation 'org.apache.grails:grails-testing-support-web' + testImplementation 'org.spockframework:spock-core' +} + +apply { + from rootProject.layout.projectDirectory.file('gradle/functional-test-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/grails-extension-gradle-config.gradle') +} diff --git a/grails-test-examples/config-report/grails-app/conf/application.groovy b/grails-test-examples/config-report/grails-app/conf/application.groovy new file mode 100644 index 00000000000..1ab156638c7 --- /dev/null +++ b/grails-test-examples/config-report/grails-app/conf/application.groovy @@ -0,0 +1,29 @@ +/* + * 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. + */ + +// Properties defined in Groovy config that should appear in the config report +myapp { + groovy { + appName = 'Config Report Test App' + version = '1.2.3' + } +} + +// A property with a pipe character to test AsciiDoc escaping +myapp.groovy.delimitedValue = 'value1|value2|value3' diff --git a/grails-test-examples/config-report/grails-app/conf/application.yml b/grails-test-examples/config-report/grails-app/conf/application.yml new file mode 100644 index 00000000000..7eb0d4524ca --- /dev/null +++ b/grails-test-examples/config-report/grails-app/conf/application.yml @@ -0,0 +1,53 @@ +# 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. + +--- +grails: + profile: web + codegen: + defaultPackage: configreport + +--- +# Properties defined in YAML that should appear in the config report +myapp: + yaml: + greeting: Hello from YAML + maxRetries: 5 + feature: + enabled: true + timeout: 30000 + typed: + name: Configured App + pageSize: 50 + debugEnabled: true + +--- +# Server configuration for testing namespace grouping +server: + port: 0 + +--- +dataSource: + pooled: true + jmxExport: true + driverClassName: org.h2.Driver + username: sa + password: + +environments: + test: + dataSource: + dbCreate: create-drop + url: jdbc:h2:mem:configReportTestDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE diff --git a/grails-test-examples/config-report/grails-app/conf/logback.xml b/grails-test-examples/config-report/grails-app/conf/logback.xml new file mode 100644 index 00000000000..bcc455d190c --- /dev/null +++ b/grails-test-examples/config-report/grails-app/conf/logback.xml @@ -0,0 +1,10 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/grails-test-examples/config-report/grails-app/controllers/configreport/UrlMappings.groovy b/grails-test-examples/config-report/grails-app/controllers/configreport/UrlMappings.groovy new file mode 100644 index 00000000000..7e141297b33 --- /dev/null +++ b/grails-test-examples/config-report/grails-app/controllers/configreport/UrlMappings.groovy @@ -0,0 +1,35 @@ +/* + * 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 configreport + +class UrlMappings { + + static mappings = { + "/$controller/$action?/$id?(.$format)?" { + constraints { + // apply constraints here + } + } + + "/"(view: '/index') + "500"(view: '/error') + "404"(view: '/notFound') + } + +} diff --git a/grails-test-examples/config-report/grails-app/init/configreport/Application.groovy b/grails-test-examples/config-report/grails-app/init/configreport/Application.groovy new file mode 100644 index 00000000000..dfe6cc0ab1e --- /dev/null +++ b/grails-test-examples/config-report/grails-app/init/configreport/Application.groovy @@ -0,0 +1,32 @@ +/* + * 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 configreport + +import grails.boot.GrailsApp +import grails.boot.config.GrailsAutoConfiguration +import org.springframework.boot.context.properties.EnableConfigurationProperties + +@EnableConfigurationProperties(AppProperties) +class Application extends GrailsAutoConfiguration { + + static void main(String[] args) { + GrailsApp.run(Application) + } + +} 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 new file mode 100644 index 00000000000..fd85880a79e --- /dev/null +++ b/grails-test-examples/config-report/src/integration-test/groovy/configreport/ConfigReportCommandIntegrationSpec.groovy @@ -0,0 +1,254 @@ +/* + * 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 configreport + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ConfigurableApplicationContext + +import grails.dev.commands.ConfigReportCommand +import grails.dev.commands.ExecutionContext +import grails.testing.mixin.integration.Integration +import org.grails.build.parsing.CommandLine +import spock.lang.Narrative +import spock.lang.Specification +import spock.lang.TempDir + +/** + * Integration tests for {@link ConfigReportCommand} that verify the command + * correctly reports configuration from multiple sources: + *

    + *
  • {@code application.yml} - YAML-based configuration
  • + *
  • {@code application.groovy} - Groovy-based configuration
  • + *
  • {@code @ConfigurationProperties} - Type-safe configuration beans
  • + *
+ */ +@Integration +@Narrative('Verifies that ConfigReportCommand generates an AsciiDoc report containing properties from application.yml, application.groovy, and @ConfigurationProperties sources') +class ConfigReportCommandIntegrationSpec extends Specification { + + @Autowired + ConfigurableApplicationContext applicationContext + + @Autowired + AppProperties appProperties + + @TempDir + File tempDir + + private ConfigReportCommand createCommand() { + ConfigReportCommand command = new ConfigReportCommand() + command.applicationContext = applicationContext + 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"() { + given: 'a ConfigReportCommand wired to the live application context' + ConfigReportCommand command = createCommand() + + and: 'an execution context pointing to a temporary directory' + ExecutionContext executionContext = new ExecutionContext(Mock(CommandLine)) + File reportFile = new File(executionContext.baseDir, ConfigReportCommand.DEFAULT_REPORT_FILE) + + when: 'the command is executed' + boolean result = command.handle(executionContext) + + then: 'the command succeeds' + result + + and: 'the report file is created' + reportFile.exists() + reportFile.length() > 0 + + and: 'the report has valid AsciiDoc structure' + 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') + + cleanup: + reportFile?.delete() + } + + def "report contains properties from application.yml"() { + 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 properties are present in the report' + content.contains('`myapp.yaml.greeting`') + content.contains('`Hello from YAML`') + + and: 'YAML numeric properties are present' + content.contains('`myapp.yaml.maxRetries`') + content.contains('`5`') + + and: 'YAML nested properties are present' + 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"() { + 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: 'Groovy config properties are present in the report' + content.contains('`myapp.groovy.appName`') + content.contains('`Config Report Test App`') + + and: 'Groovy config version property is present' + content.contains('`myapp.groovy.version`') + content.contains('`1.2.3`') + + cleanup: + reportFile?.delete() + } + + def "report escapes pipe characters from application.groovy values"() { + 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: 'pipe characters are escaped for valid AsciiDoc' + content.contains('`myapp.groovy.delimitedValue`') + content.contains('value1\\|value2\\|value3') + !content.contains('value1|value2|value3') + + cleanup: + reportFile?.delete() + } + + def "report contains properties bound via @ConfigurationProperties"() { + 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: 'the @ConfigurationProperties bean was correctly populated' + appProperties.name == 'Configured App' + appProperties.pageSize == 50 + appProperties.debugEnabled == true + + and: 'the typed properties appear in the config report' + content.contains('`myapp.typed.name`') + content.contains('`Configured App`') + content.contains('`myapp.typed.pageSize`') + content.contains('`50`') + content.contains('`myapp.typed.debugEnabled`') + content.contains('`true`') + + cleanup: + reportFile?.delete() + } + + def "report groups properties by top-level namespace"() { + 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: 'properties are organized into namespace sections' + content.contains('== grails') + content.contains('== myapp') + content.contains('== dataSource') + + and: 'sections are in alphabetical order' + content.indexOf('== dataSource') < content.indexOf('== grails') + content.indexOf('== grails') < content.indexOf('== myapp') + + cleanup: + reportFile?.delete() + } + + def "report contains properties from all three config sources simultaneously"() { + 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 properties are present' + content.contains('`myapp.yaml.greeting`') + + and: 'Groovy properties are present' + content.contains('`myapp.groovy.appName`') + + 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`') + + cleanup: + reportFile?.delete() + } + +} diff --git a/grails-test-examples/config-report/src/main/groovy/configreport/AppProperties.groovy b/grails-test-examples/config-report/src/main/groovy/configreport/AppProperties.groovy new file mode 100644 index 00000000000..80e0b34048c --- /dev/null +++ b/grails-test-examples/config-report/src/main/groovy/configreport/AppProperties.groovy @@ -0,0 +1,52 @@ +/* + * 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 configreport + +import groovy.transform.CompileStatic +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.validation.annotation.Validated + +/** + * A Spring Boot {@code @ConfigurationProperties} bean that binds to + * the {@code myapp.typed} prefix. + * + *

Properties for this bean are defined in {@code application.yml} + * and verified in the ConfigReportCommand integration test. + */ +@CompileStatic +@Validated +@ConfigurationProperties(prefix = 'myapp.typed') +class AppProperties { + + /** + * The display name of the application. + */ + String name = 'Default App' + + /** + * The maximum number of items per page. + */ + Integer pageSize = 25 + + /** + * Whether debug mode is active. + */ + Boolean debugEnabled = false + +} diff --git a/settings.gradle b/settings.gradle index 122eb4aa5db..8c742d0dfd6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -386,6 +386,7 @@ include( 'grails-test-examples-plugins-exploded', 'grails-test-examples-plugins-issue-11767', 'grails-test-examples-cache', + 'grails-test-examples-config-report', 'grails-test-examples-scaffolding', 'grails-test-examples-scaffolding-fields', 'grails-test-examples-views-functional-tests', @@ -420,6 +421,7 @@ project(':grails-test-examples-issue-15228').projectDir = file('grails-test-exam project(':grails-test-examples-plugins-exploded').projectDir = file('grails-test-examples/plugins/exploded') project(':grails-test-examples-plugins-issue-11767').projectDir = file('grails-test-examples/plugins/issue-11767') project(':grails-test-examples-cache').projectDir = file('grails-test-examples/cache') +project(':grails-test-examples-config-report').projectDir = file('grails-test-examples/config-report') project(':grails-test-examples-scaffolding').projectDir = file('grails-test-examples/scaffolding') project(':grails-test-examples-scaffolding-fields').projectDir = file('grails-test-examples/scaffolding-fields') project(':grails-test-examples-views-functional-tests').projectDir = file('grails-test-examples/views-functional-tests') From 58327298862d376ee69b27c21d646d4dc041da36 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 20 Feb 2026 19:28:00 -0500 Subject: [PATCH 03/11] 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 --- .../dev/commands/ConfigReportCommand.groovy | 130 ++- .../META-INF/grails/config-properties.yml | 799 ++++++++++++++++++ .../commands/ConfigReportCommandSpec.groovy | 149 +++- .../ConfigReportCommandIntegrationSpec.groovy | 111 ++- 4 files changed, 1099 insertions(+), 90 deletions(-) create mode 100644 grails-core/src/main/resources/META-INF/grails/config-properties.yml 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 e10da603826..67e3aaf3168 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 sorted, File reportFile) { + void writeReport(Map runtimeProperties, File reportFile) { + Map> metadata = loadPropertyMetadata() + Map>> categories = new LinkedHashMap>>() + for (Map.Entry> entry : metadata.entrySet()) { + Map property = entry.value + String category = property.get('category') + if (!categories.containsKey(category)) { + categories.put(category, new ArrayList>()) + } + categories.get(category).add(property) + } + + Set knownKeys = metadata.keySet() + Map otherProperties = new TreeMap() + 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> 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 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> loadPropertyMetadata() { + InputStream stream = ConfigReportCommand.classLoader.getResourceAsStream('META-INF/grails/config-properties.yml') + if (stream == null) { + return new LinkedHashMap>() + } + Map> metadata = new LinkedHashMap>() + Map yamlData + try { + yamlData = (Map) new Yaml().load(stream) + } + finally { + stream.close() + } + Object categories = yamlData.get('categories') + if (!(categories instanceof List)) { + return metadata + } + for (Object categoryObject : (List) categories) { + if (!(categoryObject instanceof Map)) { + continue + } + Map categoryMap = (Map) 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) properties) { + if (!(propertyObject instanceof Map)) { + continue + } + Map propertyMap = (Map) 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 entry = new LinkedHashMap() + 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 00000000000..f82c1b38e48 --- /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-.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-.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 4bd4f673705..3c05ddd9108 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 sorted = new TreeMap() - sorted.put('grails.controllers.defaultScope', 'singleton') - sorted.put('grails.profile', 'web') - sorted.put('server.port', '8080') + Map runtimeProperties = new TreeMap() + 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 runtimeProperties = new TreeMap() + 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 runtimeProperties = new TreeMap() + 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 sorted = new TreeMap() - sorted.put('test.key', 'value|with|pipes') + Map runtimeProperties = new TreeMap() + 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 sorted = new TreeMap() + Map runtimeProperties = new TreeMap() 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 runtimeProperties = new TreeMap() + 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> 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 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 fd85880a79e..19a43621413 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 *
  • {@code application.groovy} - Groovy-based configuration
  • *
  • {@code @ConfigurationProperties} - Type-safe configuration beans
  • * + * + *

    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() From 35970c349a575c3d8f103f9a98468a5568f43593 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 20 Feb 2026 22:17:58 -0500 Subject: [PATCH 04/11] chore: add missing Apache license headers for RAT audit Add Apache license headers to config-properties.yml and logback.xml that were missing required ASF headers, causing CI RAT audit failure. Assisted-by: Claude Code --- .../META-INF/grails/config-properties.yml | 15 +++++++++++++ .../config-report/grails-app/conf/logback.xml | 21 +++++++++++++++++++ 2 files changed, 36 insertions(+) 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 index f82c1b38e48..bb2c3a9f338 100644 --- a/grails-core/src/main/resources/META-INF/grails/config-properties.yml +++ b/grails-core/src/main/resources/META-INF/grails/config-properties.yml @@ -1,3 +1,18 @@ +# 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. + categories: - name: "Core Properties" properties: diff --git a/grails-test-examples/config-report/grails-app/conf/logback.xml b/grails-test-examples/config-report/grails-app/conf/logback.xml index bcc455d190c..0c32cbb056a 100644 --- a/grails-test-examples/config-report/grails-app/conf/logback.xml +++ b/grails-test-examples/config-report/grails-app/conf/logback.xml @@ -1,3 +1,24 @@ + + From 7830c47b46391975c56663e99a6b5e2a6f8e0755 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Wed, 25 Feb 2026 20:44:54 -0500 Subject: [PATCH 05/11] Address Copilot review: add YAML null guard and remove unused @TempDir Add instanceof Map guard after Yaml.load() in loadPropertyMetadata() to prevent NPE when the YAML resource is empty or contains a non-Map root. Remove unused @TempDir field and its import from the integration test. Assisted-by: Claude Code --- .../groovy/grails/dev/commands/ConfigReportCommand.groovy | 3 +++ .../configreport/ConfigReportCommandIntegrationSpec.groovy | 4 +--- 2 files changed, 4 insertions(+), 3 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 67e3aaf3168..05097222327 100644 --- a/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy +++ b/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy @@ -195,6 +195,9 @@ class ConfigReportCommand implements ApplicationCommand { finally { stream.close() } + if (!(yamlData instanceof Map)) { + return metadata + } Object categories = yamlData.get('categories') if (!(categories instanceof List)) { return metadata 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 19a43621413..9fe8e0bce32 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 @@ -27,7 +27,7 @@ import grails.testing.mixin.integration.Integration import org.grails.build.parsing.CommandLine import spock.lang.Narrative import spock.lang.Specification -import spock.lang.TempDir + /** * Integration tests for {@link ConfigReportCommand} that verify the command @@ -53,8 +53,6 @@ class ConfigReportCommandIntegrationSpec extends Specification { @Autowired AppProperties appProperties - @TempDir - File tempDir private ConfigReportCommand createCommand() { ConfigReportCommand command = new ConfigReportCommand() From 262ec6b94aa83f214ce7a6fced4875481800b1a8 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 26 Feb 2026 10:29:47 -0500 Subject: [PATCH 06/11] Add environment-specific configuration section to Application Properties doc Document how config properties are global by default and can be overridden per environment. Include default environments table (development/dev, test/test, production/prod), custom environment support, and activation via -Dgrails.env / GRAILS_ENV. Assisted-by: Claude Code --- .../Configuration/Application Properties.adoc | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/grails-doc/src/en/ref/Configuration/Application Properties.adoc b/grails-doc/src/en/ref/Configuration/Application Properties.adoc index 4a45e9708f2..2c31b00d332 100644 --- a/grails-doc/src/en/ref/Configuration/Application Properties.adoc +++ b/grails-doc/src/en/ref/Configuration/Application Properties.adoc @@ -23,6 +23,89 @@ A comprehensive reference of all configuration properties specific to Grails and Since Grails is built on Spring Boot, all https://docs.spring.io/spring-boot/{springBootVersion}/appendix/application-properties/index.html[Spring Boot Common Application Properties] are also available (for example `server.port`, `logging.*`, `spring.datasource.hikari.*`, and `management.*`). This reference covers only Grails-specific properties. +=== Environment-Specific Configuration + +Properties in `application.yml` (or `application.groovy`) are **global by default** - they apply to all environments. To override a property for a specific environment, nest it under an `environments` block: + +[source,yaml] +---- +# Global (applies to all environments) +grails: + serverURL: https://my.app.com + +# Environment-specific overrides +environments: + development: + grails: + serverURL: http://localhost:8080 + test: + grails: + serverURL: http://localhost:8080 + production: + grails: + serverURL: https://my.app.com +---- + +Or in `application.groovy`: + +[source,groovy] +---- +grails.serverURL = 'https://my.app.com' + +environments { + development { + grails.serverURL = 'http://localhost:8080' + } + test { + grails.serverURL = 'http://localhost:8080' + } + production { + grails.serverURL = 'https://my.app.com' + } +} +---- + +==== Default Environments + +Grails ships with three standard environments: + +[cols="2,2,5", options="header"] +|=== +| Environment | Short Name | Description + +| `development` +| `dev` +| Active during `grails run-app` and local development. Enables reloading and development-mode defaults. + +| `test` +| `test` +| Active during `grails test-app` and test execution. + +| `production` +| `prod` +| Active when the application is packaged and deployed (e.g., `java -jar`). The default when no environment is explicitly set in a packaged application. +|=== + +You can also define custom environments by name. Any environment name not matching the three defaults is treated as a custom environment: + +[source,yaml] +---- +environments: + staging: + grails: + serverURL: https://staging.my.app.com +---- + +Run with a custom environment using the `-Dgrails.env` system property or the `GRAILS_ENV` environment variable: + +[source,bash] +---- +java -Dgrails.env=staging -jar my-app.jar +---- + +Environment-specific values **override** global values. If a property is not set under the current environment block, the global value is used. This allows you to define sensible defaults globally and override only what differs per environment. + + === Core Properties [cols="3,5,2", options="header"] From 73c5e9a4cfa37c99f7ef2a87065dd9ed4ebdb06e Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 26 Feb 2026 12:57:31 -0500 Subject: [PATCH 07/11] Use Spring configuration metadata for config report Replace custom YAML metadata with Spring configuration metadata, refactor config reporting to parse JSON resources, and generate docs during guide builds. Assisted-by: OpenCode Assisted-by: Claude Code --- grails-core/build.gradle | 3 +- .../dev/commands/ConfigReportCommand.groovy | 234 ++- .../META-INF/grails/config-properties.yml | 814 ---------- .../spring-configuration-metadata.json | 340 ++++ .../commands/ConfigReportCommandSpec.groovy | 107 +- .../spring-configuration-metadata.json | 28 + .../spring-configuration-metadata.json | 40 + grails-doc/build.gradle | 312 +++- .../Configuration/Application Properties.adoc | 1440 ----------------- .../spring-configuration-metadata.json | 100 ++ .../ConfigReportCommandIntegrationSpec.groovy | 2 +- .../spring-configuration-metadata.json | 248 +++ 12 files changed, 1337 insertions(+), 2331 deletions(-) delete mode 100644 grails-core/src/main/resources/META-INF/grails/config-properties.yml create mode 100644 grails-core/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 grails-data-hibernate5/core/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 grails-databinding/src/main/resources/META-INF/spring-configuration-metadata.json delete mode 100644 grails-doc/src/en/ref/Configuration/Application Properties.adoc create mode 100644 grails-gsp/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 grails-web-core/src/main/resources/META-INF/spring-configuration-metadata.json diff --git a/grails-core/build.gradle b/grails-core/build.gradle index 54b420bc31b..101475ebf05 100644 --- a/grails-core/build.gradle +++ b/grails-core/build.gradle @@ -43,6 +43,7 @@ dependencies { implementation 'com.github.ben-manes.caffeine:caffeine' api 'org.apache.groovy:groovy' + implementation 'org.apache.groovy:groovy-json' api 'org.springframework.boot:spring-boot' api 'org.springframework:spring-core' api 'org.springframework:spring-tx' @@ -97,4 +98,4 @@ tasks.named('processResources', ProcessResources).configure { ProcessResources i apply { from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle') from rootProject.layout.projectDirectory.file('gradle/test-config.gradle') -} \ No newline at end of file +} 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 05097222327..5a669171115 100644 --- a/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy +++ b/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy @@ -18,10 +18,13 @@ */ package grails.dev.commands +import groovy.json.JsonSlurper import groovy.transform.CompileStatic import groovy.util.logging.Slf4j -import org.yaml.snakeyaml.Yaml +import java.util.Enumeration +import java.util.Locale +import java.net.URL import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.env.EnumerablePropertySource @@ -111,21 +114,23 @@ class ConfigReportCommand implements ApplicationCommand { * @param reportFile the file to write the report to */ void writeReport(Map runtimeProperties, File reportFile) { - Map> metadata = loadPropertyMetadata() - Map>> categories = new LinkedHashMap>>() - for (Map.Entry> entry : metadata.entrySet()) { - Map property = entry.value - String category = property.get('category') + MetadataResult metadataResult = loadPropertyMetadata() + List metadata = metadataResult.properties + Map groupDescriptions = metadataResult.groupDescriptions + Map> categories = new LinkedHashMap>() + for (ConfigPropertyMetadata property : metadata) { + String category = groupDescriptions.get(property.group) ?: property.group if (!categories.containsKey(category)) { - categories.put(category, new ArrayList>()) + categories.put(category, new ArrayList()) } categories.get(category).add(property) } - Set knownKeys = metadata.keySet() + Set knownKeys = metadata.collect { ConfigPropertyMetadata property -> property.name }.toSet() + Map environmentProperties = collectEnvironmentProperties(runtimeProperties) Map otherProperties = new TreeMap() runtimeProperties.each { String key, String value -> - if (!knownKeys.contains(key)) { + if (!knownKeys.contains(key) && !environmentProperties.containsKey(key)) { otherProperties.put(key, value) } } @@ -137,7 +142,7 @@ class ConfigReportCommand implements ApplicationCommand { writer.writeLine(':source-highlighter: coderay') writer.writeLine('') - categories.each { String categoryName, List> categoryProperties -> + categories.each { String categoryName, List categoryProperties -> writer.writeLine("== ${categoryName}") writer.writeLine('') writer.writeLine('[cols="2,5,2", options="header"]') @@ -145,10 +150,10 @@ class ConfigReportCommand implements ApplicationCommand { writer.writeLine('| Property | Description | Default') writer.writeLine('') - categoryProperties.each { Map property -> - String key = property.get('key') - String description = property.get('description') - String defaultValue = property.get('default') + categoryProperties.each { ConfigPropertyMetadata property -> + String key = property.name + String description = property.description + String defaultValue = formatDefaultValue(property.defaultValue) String resolvedValue if (runtimeProperties.containsKey(key)) { resolvedValue = "`${escapeAsciidoc(runtimeProperties.get(key))}`" @@ -179,66 +184,153 @@ class ConfigReportCommand implements ApplicationCommand { } writer.writeLine('|===') } + + if (!environmentProperties.isEmpty()) { + if (!otherProperties.isEmpty()) { + writer.writeLine('') + } + writer.writeLine('== Environment Properties') + writer.writeLine('') + writer.writeLine('[cols="2,3", options="header"]') + writer.writeLine('|===') + writer.writeLine('| Property | Default') + writer.writeLine('') + environmentProperties.each { String key, String value -> + writer.writeLine("| `${key}`") + writer.writeLine("| `${escapeAsciidoc(value)}`") + writer.writeLine('') + } + writer.writeLine('|===') + } } } - Map> loadPropertyMetadata() { - InputStream stream = ConfigReportCommand.classLoader.getResourceAsStream('META-INF/grails/config-properties.yml') - if (stream == null) { - return new LinkedHashMap>() - } - Map> metadata = new LinkedHashMap>() - Map yamlData - try { - yamlData = (Map) new Yaml().load(stream) - } - finally { - stream.close() - } - if (!(yamlData instanceof Map)) { - return metadata - } - Object categories = yamlData.get('categories') - if (!(categories instanceof List)) { - return metadata - } - for (Object categoryObject : (List) categories) { - if (!(categoryObject instanceof Map)) { - continue + MetadataResult loadPropertyMetadata() { + Enumeration resources = ConfigReportCommand.classLoader.getResources('META-INF/spring-configuration-metadata.json') + List metadata = new ArrayList() + Map groupDescriptions = new LinkedHashMap() + JsonSlurper slurper = new JsonSlurper() + while (resources.hasMoreElements()) { + URL resource = resources.nextElement() + InputStream stream = resource.openStream() + Map jsonData + try { + jsonData = (Map) slurper.parse(stream) + } + finally { + stream.close() } - Map categoryMap = (Map) categoryObject - Object categoryNameObject = categoryMap.get('name') - if (!(categoryNameObject instanceof String)) { + if (!(jsonData instanceof Map)) { continue } - String categoryName = (String) categoryNameObject - Object properties = categoryMap.get('properties') - if (!(properties instanceof List)) { + groupDescriptions.putAll(loadGroupDescriptions(jsonData.get('groups'))) + Object propertiesObject = jsonData.get('properties') + if (!(propertiesObject instanceof List)) { continue } - for (Object propertyObject : (List) properties) { + for (Object propertyObject : (List) propertiesObject) { if (!(propertyObject instanceof Map)) { continue } Map propertyMap = (Map) propertyObject - Object keyObject = propertyMap.get('key') - Object descriptionObject = propertyMap.get('description') - Object defaultObject = propertyMap.get('default') - if (!(keyObject instanceof String)) { + Object nameObject = propertyMap.get('name') + if (!(nameObject instanceof String)) { + continue + } + String name = (String) nameObject + if (!isGrailsProperty(name)) { continue } - String key = (String) keyObject - String description = descriptionObject instanceof String ? (String) descriptionObject : '' - String defaultValue = defaultObject instanceof String ? (String) defaultObject : '' - Map entry = new LinkedHashMap() - entry.put('key', key) - entry.put('description', description) - entry.put('default', defaultValue) - entry.put('category', categoryName) - metadata.put(key, entry) + String description = propertyMap.get('description') instanceof String ? (String) propertyMap.get('description') : '' + String type = propertyMap.get('type') instanceof String ? (String) propertyMap.get('type') : 'java.lang.String' + Object defaultValue = propertyMap.get('defaultValue') + String group = propertyMap.get('group') instanceof String ? (String) propertyMap.get('group') : resolveGroup(name, groupDescriptions.keySet()) + metadata.add(new ConfigPropertyMetadata(name, type, description, defaultValue, group)) } } - metadata + metadata.sort { ConfigPropertyMetadata left, ConfigPropertyMetadata right -> left.name <=> right.name } + new MetadataResult(metadata, groupDescriptions) + } + + Map loadGroupDescriptions(Object groupsObject) { + if (!(groupsObject instanceof List)) { + return new LinkedHashMap() + } + Map descriptions = new LinkedHashMap() + for (Object groupObject : (List) groupsObject) { + if (!(groupObject instanceof Map)) { + continue + } + Map groupMap = (Map) groupObject + Object nameObject = groupMap.get('name') + if (!(nameObject instanceof String)) { + continue + } + String name = (String) nameObject + Object descriptionObject = groupMap.get('description') + if (descriptionObject instanceof String) { + descriptions.put(name, (String) descriptionObject) + } + } + descriptions + } + + Map collectEnvironmentProperties(Map runtimeProperties) { + Map environmentProperties = new TreeMap() + Set normalizedKeys = new LinkedHashSet() + for (String envKey : System.getenv().keySet()) { + String lowerKey = envKey.toLowerCase(Locale.ENGLISH) + normalizedKeys.add(lowerKey.replace('_', '.')) + normalizedKeys.add(lowerKey.replace('_', '-')) + } + runtimeProperties.each { String key, String value -> + if (normalizedKeys.contains(key.toLowerCase(Locale.ENGLISH))) { + environmentProperties.put(key, value) + } + } + environmentProperties + } + + String resolveGroup(String name, Set groupNames) { + if (groupNames == null || groupNames.isEmpty()) { + return fallbackGroup(name) + } + String match = groupNames.findAll { String groupName -> + name == groupName || name.startsWith("${groupName}.") + }.sort { String left, String right -> right.length() <=> left.length() } + .find { String groupName -> groupName } + match ?: fallbackGroup(name) + } + + String fallbackGroup(String name) { + int delimiter = name.lastIndexOf('.') + if (delimiter <= 0) { + return name + } + name.substring(0, delimiter) + } + + boolean isGrailsProperty(String name) { + name.startsWith('grails.') || name.startsWith('dataSource.') || name.startsWith('hibernate.') + } + + String formatDefaultValue(Object defaultValue) { + if (defaultValue == null) { + return '' + } + if (defaultValue instanceof List) { + List values = (List) defaultValue + String joined = values.collect { Object item -> "\"${item?.toString()}\"" }.join(', ') + return "[${joined}]" + } + if (defaultValue instanceof Map) { + Map mapValue = (Map) defaultValue + if (mapValue.isEmpty()) { + return '{}' + } + return mapValue.toString() + } + defaultValue.toString() } /** @@ -254,4 +346,30 @@ class ConfigReportCommand implements ApplicationCommand { value.replace('|', '\\|') } + static class ConfigPropertyMetadata { + final String name + final String type + final String description + final Object defaultValue + final String group + + ConfigPropertyMetadata(String name, String type, String description, Object defaultValue, String group) { + this.name = name + this.type = type + this.description = description + this.defaultValue = defaultValue + this.group = group + } + } + + static class MetadataResult { + final List properties + final Map groupDescriptions + + MetadataResult(List properties, Map groupDescriptions) { + this.properties = properties + this.groupDescriptions = groupDescriptions + } + } + } 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 deleted file mode 100644 index bb2c3a9f338..00000000000 --- a/grails-core/src/main/resources/META-INF/grails/config-properties.yml +++ /dev/null @@ -1,814 +0,0 @@ -# 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. - -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-.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-.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/main/resources/META-INF/spring-configuration-metadata.json b/grails-core/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000000..2db82bc455d --- /dev/null +++ b/grails-core/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,340 @@ +{ + "groups": [ + { + "name": "grails", + "description": "Core Properties" + }, + { + "name": "grails.codegen", + "description": "Core Properties" + }, + { + "name": "grails.bootstrap", + "description": "Core Properties" + }, + { + "name": "grails.spring", + "description": "Core Properties" + }, + { + "name": "grails.plugin", + "description": "Core Properties" + }, + { + "name": "grails.reload", + "description": "Development & Reloading" + }, + { + "name": "grails.events", + "description": "Events" + }, + { + "name": "grails.json", + "description": "JSON & Converters" + }, + { + "name": "grails.converters.json", + "description": "JSON & Converters" + }, + { + "name": "grails.converters.xml", + "description": "JSON & Converters" + }, + { + "name": "grails.i18n", + "description": "Internationalization" + }, + { + "name": "grails.gorm", + "description": "GORM" + }, + { + "name": "grails.gorm.reactor", + "description": "GORM" + }, + { + "name": "grails.gorm.events", + "description": "GORM" + }, + { + "name": "grails.gorm.multiTenancy", + "description": "GORM" + }, + { + "name": "dataSource", + "description": "DataSource" + } + ], + "properties": [ + { + "name": "grails.profile", + "type": "java.lang.String", + "description": "The active Grails application profile (e.g., web, rest-api, plugin).", + "defaultValue": "Set by project template" + }, + { + "name": "grails.codegen.defaultPackage", + "type": "java.lang.String", + "description": "The default package used when generating artefacts with grails create-* commands.", + "defaultValue": "Set by project template" + }, + { + "name": "grails.serverURL", + "type": "java.lang.String", + "description": "The server URL used to generate absolute links (e.g., https://my.app.com) and used by redirects.", + "defaultValue": "derived from request" + }, + { + "name": "grails.enable.native2ascii", + "type": "java.lang.Boolean", + "description": "Whether to perform native2ascii conversion of i18n properties files.", + "defaultValue": true + }, + { + "name": "grails.bootstrap.skip", + "type": "java.lang.Boolean", + "description": "Whether to skip execution of BootStrap.groovy classes on startup.", + "defaultValue": false + }, + { + "name": "grails.spring.bean.packages", + "type": "java.util.List", + "description": "List of packages to scan for Spring beans.", + "defaultValue": [] + }, + { + "name": "grails.spring.disable.aspectj.autoweaving", + "type": "java.lang.Boolean", + "description": "Whether to disable AspectJ auto-weaving.", + "defaultValue": false + }, + { + "name": "grails.spring.placeholder.prefix", + "type": "java.lang.String", + "description": "The prefix for property placeholder resolution.", + "defaultValue": "${" + }, + { + "name": "grails.spring.transactionManagement.proxies", + "type": "java.lang.Boolean", + "description": "Whether to enable Spring proxy-based transaction management since @Transactional uses an AST transform and proxies are typically redundant.", + "defaultValue": false + }, + { + "name": "grails.plugin.includes", + "type": "java.util.List", + "description": "List of plugin names to include in the plugin manager (all others excluded).", + "defaultValue": [] + }, + { + "name": "grails.plugin.excludes", + "type": "java.util.List", + "description": "List of plugin names to exclude from the plugin manager.", + "defaultValue": [] + }, + { + "name": "grails.reload.includes", + "type": "java.util.List", + "description": "List of fully qualified class names to include in development reloading, when set only these classes are reloaded.", + "defaultValue": [] + }, + { + "name": "grails.reload.excludes", + "type": "java.util.List", + "description": "List of fully qualified class names to exclude from development reloading.", + "defaultValue": [] + }, + { + "name": "grails.events.spring", + "type": "java.lang.Boolean", + "description": "Whether to bridge GORM/Grails events to the Spring ApplicationEventPublisher, allowing EventListener methods to receive domain events.", + "defaultValue": true + }, + { + "name": "grails.json.legacy.builder", + "type": "java.lang.Boolean", + "description": "Whether to use the legacy JSON builder.", + "defaultValue": false + }, + { + "name": "grails.converters.json.domain.include.class", + "type": "java.lang.Boolean", + "description": "Whether to include the class property when marshalling domain objects to JSON.", + "defaultValue": false + }, + { + "name": "grails.converters.xml.domain.include.class", + "type": "java.lang.Boolean", + "description": "Whether to include the class attribute when marshalling domain objects to XML.", + "defaultValue": false + }, + { + "name": "grails.i18n.cache.seconds", + "type": "java.lang.Integer", + "description": "How long (in seconds) to cache resolved message bundles with -1 to cache indefinitely and 0 to disable caching.", + "defaultValue": -1 + }, + { + "name": "grails.i18n.filecache.seconds", + "type": "java.lang.Integer", + "description": "How long (in seconds) to cache the message bundle file lookup with -1 to cache indefinitely.", + "defaultValue": -1 + }, + { + "name": "grails.gorm.failOnError", + "type": "java.lang.Boolean", + "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.", + "defaultValue": false + }, + { + "name": "grails.gorm.autoFlush", + "type": "java.lang.Boolean", + "description": "Whether to automatically flush the Hibernate session between queries.", + "defaultValue": false + }, + { + "name": "grails.gorm.flushMode", + "type": "java.lang.String", + "description": "The default Hibernate flush mode (AUTO, COMMIT, MANUAL).", + "defaultValue": "AUTO" + }, + { + "name": "grails.gorm.markDirty", + "type": "java.lang.Boolean", + "description": "Whether to mark a domain instance as dirty on an explicit save() call.", + "defaultValue": true + }, + { + "name": "grails.gorm.autowire", + "type": "java.lang.Boolean", + "description": "Whether to autowire Spring beans into domain class instances.", + "defaultValue": true + }, + { + "name": "grails.gorm.default.mapping", + "type": "java.util.Map", + "description": "A closure applied as the default mapping block for all domain classes.", + "defaultValue": {} + }, + { + "name": "grails.gorm.default.constraints", + "type": "java.util.Map", + "description": "A closure applied as the default constraints for all domain classes.", + "defaultValue": {} + }, + { + "name": "grails.gorm.custom.types", + "type": "java.util.Map", + "description": "Map of custom GORM types.", + "defaultValue": {} + }, + { + "name": "grails.gorm.reactor.events", + "type": "java.lang.Boolean", + "description": "Whether to translate GORM events into Reactor events, which is disabled by default for performance.", + "defaultValue": false + }, + { + "name": "grails.gorm.events.autoTimestampInsertOverwrite", + "type": "java.lang.Boolean", + "description": "Whether auto-timestamp (dateCreated) overwrites a user-provided value on insert.", + "defaultValue": true + }, + { + "name": "grails.gorm.multiTenancy.mode", + "type": "java.lang.String", + "description": "The multi-tenancy mode: DISCRIMINATOR, DATABASE, SCHEMA, or NONE.", + "defaultValue": "NONE" + }, + { + "name": "grails.gorm.multiTenancy.tenantResolverClass", + "type": "java.lang.String", + "description": "Fully qualified class name of the TenantResolver implementation.", + "defaultValue": "required when mode is not NONE" + }, + { + "name": "dataSource.driverClassName", + "type": "java.lang.String", + "description": "The JDBC driver class name.", + "defaultValue": "org.h2.Driver" + }, + { + "name": "dataSource.username", + "type": "java.lang.String", + "description": "The database username.", + "defaultValue": "sa" + }, + { + "name": "dataSource.password", + "type": "java.lang.String", + "description": "The database password.", + "defaultValue": "" + }, + { + "name": "dataSource.url", + "type": "java.lang.String", + "description": "The JDBC connection URL.", + "defaultValue": "jdbc:h2:mem:devDb" + }, + { + "name": "dataSource.dbCreate", + "type": "java.lang.String", + "description": "The schema generation strategy: create-drop, create, update, validate, or none, use none in production with a migration tool.", + "defaultValue": "create-drop (dev), none (prod)" + }, + { + "name": "dataSource.pooled", + "type": "java.lang.Boolean", + "description": "Whether to use a connection pool.", + "defaultValue": true + }, + { + "name": "dataSource.logSql", + "type": "java.lang.Boolean", + "description": "Whether to log SQL statements to stdout.", + "defaultValue": false + }, + { + "name": "dataSource.formatSql", + "type": "java.lang.Boolean", + "description": "Whether to format logged SQL for readability.", + "defaultValue": false + }, + { + "name": "dataSource.dialect", + "type": "java.lang.String", + "description": "The Hibernate dialect class name or class.", + "defaultValue": "auto-detected from driver" + }, + { + "name": "dataSource.readOnly", + "type": "java.lang.Boolean", + "description": "Whether the DataSource is read-only (calls setReadOnly(true) on connections).", + "defaultValue": false + }, + { + "name": "dataSource.transactional", + "type": "java.lang.Boolean", + "description": "For additional datasources, whether to include in the chained transaction manager.", + "defaultValue": true + }, + { + "name": "dataSource.persistenceInterceptor", + "type": "java.lang.Boolean", + "description": "For additional datasources, whether to wire up the persistence interceptor (the default datasource is always wired).", + "defaultValue": false + }, + { + "name": "dataSource.jmxExport", + "type": "java.lang.Boolean", + "description": "Whether to register JMX MBeans for the DataSource.", + "defaultValue": true + }, + { + "name": "dataSource.type", + "type": "java.lang.String", + "description": "The connection pool implementation class when multiple are on the classpath.", + "defaultValue": "com.zaxxer.hikari.HikariDataSource" + } + ] +} 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 3c05ddd9108..c6cb06a98fc 100644 --- a/grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy +++ b/grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy @@ -67,7 +67,8 @@ class ConfigReportCommandSpec extends Specification { 'grails.profile': 'web', 'grails.codegen.defaultPackage': 'myapp', 'server.port': '8080', - 'spring.main.banner-mode': 'off' + 'spring.main.banner-mode': 'off', + 'my.custom.prop': 'value' ] propertySources.addFirst(new MapPropertySource('test', props)) props.each { String key, Object value -> @@ -100,6 +101,8 @@ class ConfigReportCommandSpec extends Specification { content.contains('`8080`') content.contains('`spring.main.banner-mode`') content.contains('`off`') + content.contains('`my.custom.prop`') + content.contains('`value`') cleanup: reportFile?.delete() @@ -185,7 +188,7 @@ class ConfigReportCommandSpec extends Specification { def "writeReport uses 3-column format with metadata categories"() { given: Map runtimeProperties = new TreeMap() - runtimeProperties.put('grails.controllers.defaultScope', 'singleton') + runtimeProperties.put('grails.gorm.autoFlush', 'true') runtimeProperties.put('grails.profile', 'web') runtimeProperties.put('server.port', '8080') @@ -203,7 +206,7 @@ class ConfigReportCommandSpec extends Specification { and: "metadata categories are used as section headers" content.contains('== Core Properties') - content.contains('== Web & Controllers') + content.contains('== GORM') and: "3-column table format is used for known properties" content.contains('[cols="2,5,2", options="header"]') @@ -211,11 +214,11 @@ class ConfigReportCommandSpec extends Specification { and: "known properties appear with descriptions" content.contains('`grails.profile`') - content.contains('`grails.controllers.defaultScope`') + content.contains('`grails.gorm.autoFlush`') and: "runtime values override static defaults for known properties" content.contains('`web`') - content.contains('`singleton`') + content.contains('`true`') and: "unknown runtime properties go to Other Properties section" content.contains('== Other Properties') @@ -286,7 +289,7 @@ class ConfigReportCommandSpec extends Specification { String content = reportFile.text content.contains('= Grails Application Configuration Report') - and: "metadata categories still appear from the YAML" + and: "metadata categories still appear from the metadata" content.contains('== Core Properties') and: "no Other Properties section when no unknown runtime properties" @@ -323,20 +326,57 @@ class ConfigReportCommandSpec extends Specification { otherSection.contains('| Property | Default') } - def "loadPropertyMetadata returns properties from classpath YAML"() { + def "writeReport moves environment-derived properties to environment section"() { + given: + Map runtimeProperties = new TreeMap() + runtimeProperties.put('my.custom.value', 'custom') + runtimeProperties.put('grails.profile', 'web') + File reportFile = new File(tempDir, 'env-report.adoc') + + and: + String envKey = 'MY_CUSTOM_VALUE' + String originalValue = System.getenv(envKey) + setEnvVar(envKey, 'from-env') + when: - Map> metadata = command.loadPropertyMetadata() + command.writeReport(runtimeProperties, reportFile) - then: "metadata is loaded from the config-properties.yml on the classpath" - !metadata.isEmpty() - metadata.containsKey('grails.profile') + then: + String content = reportFile.text + int envIndex = content.indexOf('== Environment Properties') + envIndex > -1 + String envSection = content.substring(envIndex) + envSection.contains('`my.custom.value`') + envSection.contains('`custom`') + + and: + int otherIndex = content.indexOf('== Other Properties') + String otherSection = otherIndex > -1 ? content.substring(otherIndex, envIndex) : '' + !otherSection.contains('`my.custom.value`') + + cleanup: + if (originalValue != null) { + setEnvVar(envKey, originalValue) + } + else { + clearEnvVar(envKey) + } + } + + def "loadPropertyMetadata returns properties from classpath JSON metadata"() { + when: + ConfigReportCommand.MetadataResult metadataResult = command.loadPropertyMetadata() + + then: "metadata is loaded from spring-configuration-metadata.json on the classpath" + !metadataResult.properties.isEmpty() + metadataResult.properties.find { ConfigReportCommand.ConfigPropertyMetadata property -> property.name == 'grails.profile' } and: "each entry has the expected fields" - Map profileEntry = metadata.get('grails.profile') - profileEntry.get('key') == 'grails.profile' - profileEntry.get('description') != null - profileEntry.get('description').length() > 0 - profileEntry.get('category') == 'Core Properties' + ConfigReportCommand.ConfigPropertyMetadata profileEntry = metadataResult.properties.find { ConfigReportCommand.ConfigPropertyMetadata property -> property.name == 'grails.profile' } + profileEntry.name == 'grails.profile' + profileEntry.description != null + profileEntry.description.length() > 0 + metadataResult.groupDescriptions.get('grails') == 'Core Properties' } def "escapeAsciidoc handles null and empty strings"() { @@ -347,4 +387,39 @@ class ConfigReportCommandSpec extends Specification { ConfigReportCommand.escapeAsciidoc('a|b') == 'a\\|b' } + private void setEnvVar(String key, String value) { + setEnvironmentVariable(key, value) + } + + private void clearEnvVar(String key) { + setEnvironmentVariable(key, null) + } + + private void setEnvironmentVariable(String key, String value) { + Map env = System.getenv() + Class envClass = env.getClass() + try { + java.lang.reflect.Field field = envClass.getDeclaredField('m') + field.setAccessible(true) + Map writable = (Map) field.get(env) + if (value == null) { + writable.remove(key) + } + else { + writable.put(key, value) + } + } + catch (NoSuchFieldException ignored) { + java.lang.reflect.Field field = envClass.getDeclaredField('delegate') + field.setAccessible(true) + Map writable = (Map) field.get(env) + if (value == null) { + writable.remove(key) + } + else { + writable.put(key, value) + } + } + } + } diff --git a/grails-data-hibernate5/core/src/main/resources/META-INF/spring-configuration-metadata.json b/grails-data-hibernate5/core/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000000..7a512604174 --- /dev/null +++ b/grails-data-hibernate5/core/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,28 @@ +{ + "groups": [ + { + "name": "hibernate.cache", + "description": "Hibernate" + } + ], + "properties": [ + { + "name": "hibernate.cache.queries", + "type": "java.lang.Boolean", + "description": "Whether to cache Hibernate queries.", + "defaultValue": false + }, + { + "name": "hibernate.cache.use_second_level_cache", + "type": "java.lang.Boolean", + "description": "Whether to enable Hibernate's second-level cache.", + "defaultValue": false + }, + { + "name": "hibernate.cache.use_query_cache", + "type": "java.lang.Boolean", + "description": "Whether to enable Hibernate's query cache.", + "defaultValue": false + } + ] +} diff --git a/grails-databinding/src/main/resources/META-INF/spring-configuration-metadata.json b/grails-databinding/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000000..44b3fab4fe7 --- /dev/null +++ b/grails-databinding/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,40 @@ +{ + "groups": [ + { + "name": "grails.databinding", + "description": "Data Binding" + } + ], + "properties": [ + { + "name": "grails.databinding.trimStrings", + "type": "java.lang.Boolean", + "description": "Whether to trim whitespace from String values during data binding.", + "defaultValue": true + }, + { + "name": "grails.databinding.convertEmptyStringsToNull", + "type": "java.lang.Boolean", + "description": "Whether empty String values are converted to null during data binding.", + "defaultValue": true + }, + { + "name": "grails.databinding.autoGrowCollectionLimit", + "type": "java.lang.Integer", + "description": "The maximum size to which indexed collections can auto-grow during data binding.", + "defaultValue": 256 + }, + { + "name": "grails.databinding.dateFormats", + "type": "java.util.List", + "description": "List of date format strings used to parse date values during data binding.", + "defaultValue": ["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"] + }, + { + "name": "grails.databinding.dateParsingLenient", + "type": "java.lang.Boolean", + "description": "Whether date parsing is lenient (accepting invalid dates like Feb 30).", + "defaultValue": false + } + ] +} diff --git a/grails-doc/build.gradle b/grails-doc/build.gradle index 4f7c1d9d879..c60dd977bbe 100644 --- a/grails-doc/build.gradle +++ b/grails-doc/build.gradle @@ -20,6 +20,9 @@ import grails.doc.git.FetchTagsTask import grails.doc.dropdown.CreateReleaseDropDownTask import grails.doc.gradle.PublishGuideTask +import groovy.json.JsonSlurper + +import java.util.zip.ZipFile plugins { id 'base' @@ -166,9 +169,316 @@ generateBomDocumentation.configure { Task it -> } } +def generateConfigReference = tasks.register('generateConfigReference') +generateConfigReference.configure { Task it -> + it.dependsOn( + project(':grails-core').tasks.named('jar'), + project(':grails-web-core').tasks.named('jar'), + project(':grails-gsp').tasks.named('jar'), + project(':grails-databinding').tasks.named('jar'), + project(':grails-data-hibernate5:core').tasks.named('jar') + ) + + it.description = 'Generates the Application Properties reference from configuration metadata.' + it.group = 'documentation' + + def configDir = project.layout.projectDirectory.dir('src/en/ref/Configuration') + def outputFile = project.layout.projectDirectory.file('src/en/ref/Configuration/Application Properties.adoc') + + it.inputs.files( + project(':grails-core').tasks.named('jar').flatMap { task -> task.archiveFile }, + project(':grails-web-core').tasks.named('jar').flatMap { task -> task.archiveFile }, + project(':grails-gsp').tasks.named('jar').flatMap { task -> task.archiveFile }, + project(':grails-databinding').tasks.named('jar').flatMap { task -> task.archiveFile }, + project(':grails-data-hibernate5:core').tasks.named('jar').flatMap { task -> task.archiveFile } + ) + it.outputs.file(outputFile) + + it.doFirst { + configDir.asFile.mkdirs() + } + + it.doLast { + List jarFiles = [ + project(':grails-core').tasks.named('jar').get().archiveFile.get().asFile, + project(':grails-web-core').tasks.named('jar').get().archiveFile.get().asFile, + project(':grails-gsp').tasks.named('jar').get().archiveFile.get().asFile, + project(':grails-databinding').tasks.named('jar').get().archiveFile.get().asFile, + project(':grails-data-hibernate5:core').tasks.named('jar').get().archiveFile.get().asFile + ] + + Map groupDescriptions = new LinkedHashMap() + List> properties = new ArrayList>() + JsonSlurper slurper = new JsonSlurper() + + jarFiles.each { File jarFile -> + if (!jarFile.exists()) { + return + } + ZipFile zipFile = new ZipFile(jarFile) + try { + def entry = zipFile.getEntry('META-INF/spring-configuration-metadata.json') + if (entry == null) { + return + } + String jsonText = zipFile.getInputStream(entry).getText('UTF-8') + Map jsonData = (Map) slurper.parseText(jsonText) + Object groupsObject = jsonData.get('groups') + if (groupsObject instanceof List) { + ((List) groupsObject).each { Object groupObject -> + if (groupObject instanceof Map) { + Map groupMap = (Map) groupObject + Object nameObject = groupMap.get('name') + Object descriptionObject = groupMap.get('description') + if (nameObject instanceof String && descriptionObject instanceof String) { + groupDescriptions.put((String) nameObject, (String) descriptionObject) + } + } + } + } + Object propertiesObject = jsonData.get('properties') + if (propertiesObject instanceof List) { + ((List) propertiesObject).each { Object propertyObject -> + if (!(propertyObject instanceof Map)) { + return + } + Map propertyMap = (Map) propertyObject + Object nameObject = propertyMap.get('name') + if (!(nameObject instanceof String)) { + return + } + String name = (String) nameObject + if (!(name.startsWith('grails.') || name.startsWith('dataSource.') || name.startsWith('hibernate.'))) { + return + } + String description = propertyMap.get('description') instanceof String ? (String) propertyMap.get('description') : '' + Object defaultValue = propertyMap.get('defaultValue') + String group = propertyMap.get('group') instanceof String ? (String) propertyMap.get('group') : null + properties.add([ + name : name, + description : description, + defaultValue: defaultValue, + group : group + ]) + } + } + } + finally { + zipFile.close() + } + } + + Set groupNames = groupDescriptions.keySet() + Closure fallbackGroup = { String name -> + int delimiter = name.lastIndexOf('.') + if (delimiter <= 0) { + return name + } + name.substring(0, delimiter) + } + Closure resolveGroup = { String name, String group -> + if (group) { + return group + } + if (groupNames == null || groupNames.isEmpty()) { + return fallbackGroup(name) + } + String match = groupNames.findAll { String groupName -> + name == groupName || name.startsWith("${groupName}.") + }.sort { String left, String right -> right.length() <=> left.length() } + .find { String groupName -> groupName } + match ?: fallbackGroup(name) + } + + Closure escapeAsciidoc = { String value -> + if (!value) { + return value + } + value.replace('|', '\\|') + } + + Closure formatDefaultValue = { Object value -> + if (value == null) { + return '' + } + if (value instanceof List) { + List values = (List) value + String joined = values.collect { Object item -> "\"${item?.toString()}\"" }.join(', ') + return "[${joined}]" + } + if (value instanceof Map) { + Map mapValue = (Map) value + if (mapValue.isEmpty()) { + return '{}' + } + return mapValue.toString() + } + value.toString() + } + + Map>> categories = new LinkedHashMap>>() + properties.each { Map property -> + String name = (String) property.get('name') + String resolvedGroup = resolveGroup(name, (String) property.get('group')) + String category = groupDescriptions.get(resolvedGroup) ?: resolvedGroup + if (!categories.containsKey(category)) { + categories.put(category, new ArrayList>()) + } + categories.get(category).add([ + name : name, + description : property.get('description') as String, + defaultValue: property.get('defaultValue') + ]) + } + + List preferredOrder = [ + 'Core Properties', + 'Web & Controllers', + 'CORS', + 'Views & GSP', + 'Content Negotiation & MIME Types', + 'Data Binding', + 'Internationalization', + 'Static Resources', + 'URL Mappings', + 'Scaffolding', + 'Development & Reloading', + 'Events', + 'JSON & Converters', + 'GORM', + 'DataSource', + 'Hibernate' + ] + List orderedCategories = new ArrayList() + preferredOrder.each { String category -> + if (categories.containsKey(category)) { + orderedCategories.add(category) + } + } + categories.keySet().findAll { String category -> !orderedCategories.contains(category) }.sort().each { String category -> + orderedCategories.add(category) + } + + def documentFile = outputFile.asFile + documentFile.withWriter('UTF-8') { writer -> + writer.writeLine('////') + writer.writeLine('Licensed to the Apache Software Foundation (ASF) under one') + writer.writeLine('or more contributor license agreements. See the NOTICE file') + writer.writeLine('distributed with this work for additional information') + writer.writeLine('regarding copyright ownership. The ASF licenses this file') + writer.writeLine('to you under the Apache License, Version 2.0 (the') + writer.writeLine('"License"); you may not use this file except in compliance') + writer.writeLine('with the License. You may obtain a copy of the License at') + writer.writeLine('') + writer.writeLine('https://www.apache.org/licenses/LICENSE-2.0') + writer.writeLine('') + writer.writeLine('Unless required by applicable law or agreed to in writing,') + writer.writeLine('software distributed under the License is distributed on an') + writer.writeLine('"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY') + writer.writeLine('KIND, either express or implied. See the License for the') + writer.writeLine('specific language governing permissions and limitations') + writer.writeLine('under the License.') + writer.writeLine('////') + writer.writeLine('') + writer.writeLine('== Application Properties') + writer.writeLine('') + writer.writeLine('A comprehensive reference of all configuration properties specific to Grails and its bundled modules. These properties are set in `grails-app/conf/application.yml` (or `application.groovy`).') + writer.writeLine('') + writer.writeLine('Since Grails is built on Spring Boot, all https://docs.spring.io/spring-boot/{springBootVersion}/appendix/application-properties/index.html[Spring Boot Common Application Properties] are also available (for example `server.port`, `logging.*`, `spring.datasource.hikari.*`, and `management.*`). This reference covers only Grails-specific properties.') + writer.writeLine('') + writer.writeLine('=== Environment-Specific Configuration') + writer.writeLine('') + writer.writeLine('Properties in `application.yml` (or `application.groovy`) are global by default - they apply to all environments. To override a property for a specific environment, nest it under an `environments` block:') + writer.writeLine('') + writer.writeLine('[source,yaml]') + writer.writeLine('----') + writer.writeLine('# Global (applies to all environments)') + writer.writeLine('grails:') + writer.writeLine(' serverURL: https://my.app.com') + writer.writeLine('') + writer.writeLine('# Environment-specific overrides') + writer.writeLine('environments:') + writer.writeLine(' development:') + writer.writeLine(' grails:') + writer.writeLine(' serverURL: http://localhost:8080') + writer.writeLine(' test:') + writer.writeLine(' grails:') + writer.writeLine(' serverURL: http://localhost:8080') + writer.writeLine(' production:') + writer.writeLine(' grails:') + writer.writeLine(' serverURL: https://my.app.com') + writer.writeLine('----') + writer.writeLine('') + writer.writeLine('Or in `application.groovy`:') + writer.writeLine('') + writer.writeLine('[source,groovy]') + writer.writeLine('----') + writer.writeLine("grails.serverURL = 'https://my.app.com'") + writer.writeLine('') + writer.writeLine('environments {') + writer.writeLine(' development {') + writer.writeLine(" grails.serverURL = 'http://localhost:8080'") + writer.writeLine(' }') + writer.writeLine(' test {') + writer.writeLine(" grails.serverURL = 'http://localhost:8080'") + writer.writeLine(' }') + writer.writeLine(' production {') + writer.writeLine(" grails.serverURL = 'https://my.app.com'") + writer.writeLine(' }') + writer.writeLine('}') + writer.writeLine('----') + writer.writeLine('') + + orderedCategories.each { String category -> + List> categoryProperties = categories.get(category) + if (categoryProperties == null || categoryProperties.isEmpty()) { + return + } + writer.writeLine("=== ${category}") + writer.writeLine('') + writer.writeLine('[cols="3,5,2", options="header"]') + writer.writeLine('|===') + writer.writeLine('| Property | Description | Default') + writer.writeLine('') + categoryProperties.sort { Map left, Map right -> + (left.get('name') as String) <=> (right.get('name') as String) + }.each { Map property -> + String name = property.get('name') as String + String description = property.get('description') as String + String defaultValue = formatDefaultValue(property.get('defaultValue')) + writer.writeLine("| `${name}`") + writer.writeLine("| ${escapeAsciidoc(description)}") + if (defaultValue) { + writer.writeLine("| `${escapeAsciidoc(defaultValue)}`") + } + else { + writer.writeLine('|') + } + writer.writeLine('') + } + writer.writeLine('|===') + writer.writeLine('') + } + + writer.writeLine('== Plugin References') + writer.writeLine('') + writer.writeLine('The following plugin documentation covers configuration properties that are not listed above:') + writer.writeLine('') + writer.writeLine('- Spring Security - https://apache.github.io/grails-spring-security/') + writer.writeLine('- Asset Pipeline - https://github.com/bertramdev/asset-pipeline') + writer.writeLine('- Database Migration - https://github.com/grails/grails-database-migration') + writer.writeLine('- Cache - https://github.com/grails/grails-cache') + writer.writeLine('- MongoDB - https://github.com/grails/grails-data-mapping') + writer.writeLine('') + } + + it.logger.lifecycle "Application Properties reference generated to: ${documentFile.absolutePath}" + } +} + def publishGuideTask = tasks.register('publishGuide', PublishGuideTask) publishGuideTask.configure { PublishGuideTask publish -> - publish.dependsOn([generateBomDocumentation]) + publish.dependsOn([generateBomDocumentation, generateConfigReference]) // No language setting because we want the English guide to be // generated with a 'en' in the path, but the source is in 'en' diff --git a/grails-doc/src/en/ref/Configuration/Application Properties.adoc b/grails-doc/src/en/ref/Configuration/Application Properties.adoc deleted file mode 100644 index 2c31b00d332..00000000000 --- a/grails-doc/src/en/ref/Configuration/Application Properties.adoc +++ /dev/null @@ -1,1440 +0,0 @@ -//// -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. -//// - -== Application Properties - -A comprehensive reference of all configuration properties specific to Grails and its bundled plugins. These properties are set in `grails-app/conf/application.yml` (or `application.groovy`). - -Since Grails is built on Spring Boot, all https://docs.spring.io/spring-boot/{springBootVersion}/appendix/application-properties/index.html[Spring Boot Common Application Properties] are also available (for example `server.port`, `logging.*`, `spring.datasource.hikari.*`, and `management.*`). This reference covers only Grails-specific properties. - -=== Environment-Specific Configuration - -Properties in `application.yml` (or `application.groovy`) are **global by default** - they apply to all environments. To override a property for a specific environment, nest it under an `environments` block: - -[source,yaml] ----- -# Global (applies to all environments) -grails: - serverURL: https://my.app.com - -# Environment-specific overrides -environments: - development: - grails: - serverURL: http://localhost:8080 - test: - grails: - serverURL: http://localhost:8080 - production: - grails: - serverURL: https://my.app.com ----- - -Or in `application.groovy`: - -[source,groovy] ----- -grails.serverURL = 'https://my.app.com' - -environments { - development { - grails.serverURL = 'http://localhost:8080' - } - test { - grails.serverURL = 'http://localhost:8080' - } - production { - grails.serverURL = 'https://my.app.com' - } -} ----- - -==== Default Environments - -Grails ships with three standard environments: - -[cols="2,2,5", options="header"] -|=== -| Environment | Short Name | Description - -| `development` -| `dev` -| Active during `grails run-app` and local development. Enables reloading and development-mode defaults. - -| `test` -| `test` -| Active during `grails test-app` and test execution. - -| `production` -| `prod` -| Active when the application is packaged and deployed (e.g., `java -jar`). The default when no environment is explicitly set in a packaged application. -|=== - -You can also define custom environments by name. Any environment name not matching the three defaults is treated as a custom environment: - -[source,yaml] ----- -environments: - staging: - grails: - serverURL: https://staging.my.app.com ----- - -Run with a custom environment using the `-Dgrails.env` system property or the `GRAILS_ENV` environment variable: - -[source,bash] ----- -java -Dgrails.env=staging -jar my-app.jar ----- - -Environment-specific values **override** global values. If a property is not set under the current environment block, the global value is used. This allows you to define sensible defaults globally and override only what differs per environment. - - -=== Core Properties - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.profile` -| The active Grails application profile (e.g., `web`, `rest-api`, `plugin`). -| Set by project template - -| `grails.codegen.defaultPackage` -| The default package used when generating artefacts with `grails create-*` commands. -| Set by project template - -| `grails.serverURL` -| The server URL used to generate absolute links (e.g., `\https://my.app.com`). Also used by redirects. -| _(derived from request)_ - -| `grails.enable.native2ascii` -| Whether to perform native2ascii conversion of i18n properties files. -| `true` - -| `grails.bootstrap.skip` -| Whether to skip execution of `BootStrap.groovy` classes on startup. -| `false` - -| `grails.spring.bean.packages` -| List of packages to scan for Spring beans. -| `[]` - -| `grails.spring.disable.aspectj.autoweaving` -| Whether to disable AspectJ auto-weaving. -| `false` - -| `grails.spring.placeholder.prefix` -| The prefix for property placeholder resolution. -| `${` - -| `grails.spring.transactionManagement.proxies` -| Whether to enable Spring proxy-based transaction management. Since `@Transactional` uses an AST transform, proxies are typically redundant. -| `false` - -| `grails.plugin.includes` -| List of plugin names to include in the plugin manager (all others excluded). -| `[]` _(all plugins)_ - -| `grails.plugin.excludes` -| List of plugin names to exclude from the plugin manager. -| `[]` -|=== - -=== Web & Controllers - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.controllers.defaultScope` -| The default scope for controllers (`singleton`, `prototype`, `session`). -| `singleton` - -| `grails.controllers.upload.location` -| The directory for temporary file uploads. -| `System.getProperty('java.io.tmpdir')` - -| `grails.controllers.upload.maxFileSize` -| Maximum file size for uploads (in bytes). -| `1048576` (1 MB) - -| `grails.controllers.upload.maxRequestSize` -| Maximum request size for multipart uploads (in bytes). -| `10485760` (10 MB) - -| `grails.controllers.upload.fileSizeThreshold` -| File size threshold (in bytes) above which uploads are written to disk. -| `0` - -| `grails.web.url.converter` -| The URL token converter strategy. Use `hyphenated` for hyphen-separated URLs. -| `camelCase` - -| `grails.web.linkGenerator.useCache` -| Whether to cache links generated by the link generator. -| `true` - -| `grails.web.servlet.path` -| The path the Grails dispatcher servlet is mapped to. -| `/+++*+++` - -| `grails.filter.encoding` -| The character encoding for the Grails character encoding filter. -| `UTF-8` - -| `grails.filter.forceEncoding` -| Whether to force the encoding filter to set the encoding on the response. -| `true` - -| `grails.exceptionresolver.logRequestParameters` -| Whether to log request parameters in exception stack traces. -| `true` - -| `grails.exceptionresolver.params.exclude` -| List of parameter names to mask (replace with `[*****]`) in exception stack traces. Typically used for `password`, `creditCard`, etc. -| `[]` - -| `grails.logging.stackTraceFiltererClass` -| Fully qualified class name of a custom `StackTraceFilterer` implementation. -| `org.grails.exceptions.reporting.DefaultStackTraceFilterer` -|=== - -=== CORS - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.cors.enabled` -| Whether CORS support is enabled. -| `false` - -| `grails.cors.filter` -| Whether CORS is handled via a servlet filter (`true`) or an interceptor (`false`). -| `true` - -| `grails.cors.allowedOrigins` -| List of allowed origins (e.g., `\http://localhost:5000`). Only applies when `grails.cors.enabled` is `true`. -| `['*']` - -| `grails.cors.allowedMethods` -| List of allowed HTTP methods. -| `['*']` - -| `grails.cors.allowedHeaders` -| List of allowed request headers. -| `['*']` - -| `grails.cors.exposedHeaders` -| List of response headers to expose to the client. -| `[]` - -| `grails.cors.maxAge` -| How long (in seconds) the preflight response can be cached. -| `1800` - -| `grails.cors.allowCredentials` -| Whether credentials (cookies, authorization headers) are supported. -| `false` - -| `grails.cors.mappings` -| Map of URL patterns to per-path CORS configuration. Defining any mapping disables the global `/**` mapping. -| `{}` _(global `/**` mapping)_ -|=== - -=== Views & GSP - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.views.default.codec` -| The default encoding codec for GSP output. Set to `html` to reduce XSS risk. Options: `none`, `html`, `base64`. -| `none` - -| `grails.views.gsp.encoding` -| The file encoding for GSP source files. -| `UTF-8` - -| `grails.views.gsp.htmlcodec` -| The HTML codec for GSP output (`xml` or `html`). -| `xml` - -| `grails.views.gsp.codecs.expression` -| The codec applied to GSP `${}` expressions. -| `html` - -| `grails.views.gsp.codecs.scriptlet` -| The codec applied to GSP `<% %>` scriptlet output. -| `html` - -| `grails.views.gsp.codecs.taglib` -| The codec applied to tag library output. -| `none` - -| `grails.views.gsp.codecs.staticparts` -| The codec applied to static HTML parts of GSP pages. -| `none` - -| `grails.views.gsp.layout.preprocess` -| Whether GSP layout preprocessing is enabled. Disabling allows Grails to parse rendered HTML but slows rendering. -| `true` - -| `grails.views.enable.jsessionid` -| Whether to include the `jsessionid` in rendered links. -| `false` - -| `grails.views.filteringCodecForContentType` -| Map of content types to encoding codecs. -| `{}` - -| `grails.gsp.disable.caching.resources` -| Whether to disable GSP resource caching. -| `false` - -| `grails.gsp.enable.reload` -| Whether to enable GSP reloading in production. -| `false` - -| `grails.gsp.view.dir` -| Custom directory for GSP view resolution. -| `grails-app/views` -|=== - -=== Content Negotiation & MIME Types - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.mime.types` -| Map of MIME type names to content type strings used for content negotiation. -| _(see web profile `application.yml`)_ - -| `grails.mime.file.extensions` -| Whether to use the file extension to determine the MIME type in content negotiation. -| `true` - -| `grails.mime.use.accept.header` -| Whether to use the `Accept` header for content negotiation. -| `true` - -| `grails.mime.disable.accept.header.userAgents` -| List of user agent substrings (e.g., `Gecko`, `WebKit`) for which `Accept` header processing is disabled. -| `[]` - -| `grails.mime.disable.accept.header.userAgentsXhr` -| When `true`, XHR requests also respect the `grails.mime.disable.accept.header.userAgents` setting. By default, XHR requests ignore user agent filtering. -| `false` - -| `grails.converters.encoding` -| The character encoding for converter output (JSON/XML). -| `UTF-8` -|=== - -=== Data Binding - -These properties configure Grails' data binding behavior. Defined via `@ConfigurationProperties("grails.databinding")`. - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.databinding.trimStrings` -| Whether to trim whitespace from String values during data binding. -| `true` - -| `grails.databinding.convertEmptyStringsToNull` -| Whether empty String values are converted to `null` during data binding. -| `true` - -| `grails.databinding.autoGrowCollectionLimit` -| The maximum size to which indexed collections can auto-grow during data binding. -| `256` - -| `grails.databinding.dateFormats` -| List of date format strings used to parse date values during data binding. -| `["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"]` - -| `grails.databinding.dateParsingLenient` -| Whether date parsing is lenient (accepting invalid dates like Feb 30). -| `false` -|=== - -=== Internationalization - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.i18n.cache.seconds` -| How long (in seconds) to cache resolved message bundles. Set to `-1` to cache indefinitely, `0` to disable caching. -| `-1` - -| `grails.i18n.filecache.seconds` -| How long (in seconds) to cache the message bundle file lookup. Set to `-1` to cache indefinitely. -| `-1` -|=== - -=== Static Resources - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.resources.enabled` -| Whether serving static files from `src/main/resources/public` is enabled. -| `true` - -| `grails.resources.pattern` -| The URL path pattern for serving static resources. -| `/static/**` - -| `grails.resources.cachePeriod` -| The cache period (in seconds) for static resource HTTP responses. -| `0` _(no caching)_ -|=== - -=== URL Mappings - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.urlmapping.cache.maxsize` -| The maximum size of the URL mapping cache. -| `1000` -|=== - -=== Scaffolding - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.scaffolding.templates.domainSuffix` -| The suffix appended to domain class names when generating scaffolding templates. -| `""` -|=== - -=== Development & Reloading - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.reload.includes` -| List of fully qualified class names to include in development reloading. When set, only these classes are reloaded. -| `[]` _(all project classes)_ - -| `grails.reload.excludes` -| List of fully qualified class names to exclude from development reloading. -| `[]` -|=== - -=== Events - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.events.spring` -| Whether to bridge GORM/Grails events to the Spring `ApplicationEventPublisher`, allowing `@EventListener` methods to receive domain events. -| `true` -|=== - -=== JSON & Converters - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.json.legacy.builder` -| Whether to use the legacy JSON builder. -| `false` - -| `grails.converters.json.domain.include.class` -| Whether to include the `class` property when marshalling domain objects to JSON. -| `false` - -| `grails.converters.xml.domain.include.class` -| Whether to include the `class` attribute when marshalling domain objects to XML. -| `false` -|=== - -=== GORM - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.gorm.failOnError` -| When `true`, `save()` throws `ValidationException` on validation failure instead of returning `null`. Can also be a list of package names to apply selectively. -| `false` - -| `grails.gorm.autoFlush` -| Whether to automatically flush the Hibernate session between queries. -| `false` - -| `grails.gorm.flushMode` -| The default Hibernate flush mode (`AUTO`, `COMMIT`, `MANUAL`). -| `AUTO` - -| `grails.gorm.markDirty` -| Whether to mark a domain instance as dirty on an explicit `save()` call. -| `true` - -| `grails.gorm.autowire` -| Whether to autowire Spring beans into domain class instances. -| `true` - -| `grails.gorm.default.mapping` -| A closure applied as the default mapping block for all domain classes. -| `{}` - -| `grails.gorm.default.constraints` -| A closure applied as the default constraints for all domain classes. -| `{}` - -| `grails.gorm.custom.types` -| Map of custom GORM types. -| `{}` - -| `grails.gorm.reactor.events` -| Whether to translate GORM events into Reactor events (disabled by default for performance). -| `false` - -| `grails.gorm.events.autoTimestampInsertOverwrite` -| Whether auto-timestamp (`dateCreated`) overwrites a user-provided value on insert. -| `true` -|=== - -==== Multi-Tenancy - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.gorm.multiTenancy.mode` -| The multi-tenancy mode: `DISCRIMINATOR`, `DATABASE`, `SCHEMA`, or `NONE`. -| `NONE` - -| `grails.gorm.multiTenancy.tenantResolverClass` -| Fully qualified class name of the `TenantResolver` implementation. -| _(required when mode is not NONE)_ -|=== - -=== DataSource - -Spring Boot auto-configures the connection pool. https://github.com/brettwooldridge/HikariCP[HikariCP] is the default and preferred pool. Pool-specific tuning is configured via `spring.datasource.hikari.*` - see the https://docs.spring.io/spring-boot/{springBootVersion}/reference/data/sql.html#data.sql.datasource.connection-pool[Spring Boot Connection Pool documentation]. - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `dataSource.driverClassName` -| The JDBC driver class name. -| `org.h2.Driver` - -| `dataSource.username` -| The database username. -| `sa` - -| `dataSource.password` -| The database password. -| `''` - -| `dataSource.url` -| The JDBC connection URL. -| `jdbc:h2:mem:devDb` (dev) - -| `dataSource.dbCreate` -| The schema generation strategy: `create-drop`, `create`, `update`, `validate`, or `none`. Use `none` in production with a migration tool. -| `create-drop` (dev), `none` (prod) - -| `dataSource.pooled` -| Whether to use a connection pool. -| `true` - -| `dataSource.logSql` -| Whether to log SQL statements to stdout. -| `false` - -| `dataSource.formatSql` -| Whether to format logged SQL for readability. -| `false` - -| `dataSource.dialect` -| The Hibernate dialect class name or class. -| _(auto-detected from driver)_ - -| `dataSource.readOnly` -| Whether the DataSource is read-only (calls `setReadOnly(true)` on connections). -| `false` - -| `dataSource.transactional` -| For additional (non-default) datasources: whether to include in the chained transaction manager. -| `true` - -| `dataSource.persistenceInterceptor` -| For additional datasources: whether to wire up the persistence interceptor (the default datasource is always wired). -| `false` - -| `dataSource.jmxExport` -| Whether to register JMX MBeans for the DataSource. -| `true` - -| `dataSource.type` -| The connection pool implementation class when multiple are on the classpath. -| `com.zaxxer.hikari.HikariDataSource` -|=== - -Additional datasources are configured under the `dataSources` key. Each named datasource supports the same properties as the default `dataSource`. - -[source,yaml] ----- -dataSources: - lookup: - pooled: true - jmxExport: true - driverClassName: org.h2.Driver - url: jdbc:h2:mem:simpleDb ----- - -=== Hibernate - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `hibernate.cache.queries` -| Whether to cache Hibernate queries. -| `false` - -| `hibernate.cache.use_second_level_cache` -| Whether to enable Hibernate's second-level cache. -| `false` - -| `hibernate.cache.use_query_cache` -| Whether to enable Hibernate's query cache. -| `false` -|=== - -NOTE: All standard {hibernate5Reference}#configurations[Hibernate configuration properties] are supported under the `hibernate` key. - -=== Database Migration Plugin - -These properties are read from the `grails.plugin.databasemigration` namespace. For multi-datasource setups, per-datasource configuration uses `grails.plugin.databasemigration.` (e.g., `grails.plugin.databasemigration.analytics.updateOnStart`). - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.plugin.databasemigration.updateOnStart` -| Whether to automatically apply pending migrations on application startup. -| `false` - -| `grails.plugin.databasemigration.updateAllOnStart` -| Whether to apply migrations for all datasources on startup (overrides per-datasource `updateOnStart`). -| `false` - -| `grails.plugin.databasemigration.updateOnStartFileName` -| The changelog filename to use when applying migrations on startup. -| `changelog.groovy` (default ds), `changelog-.groovy` (named ds) - -| `grails.plugin.databasemigration.dropOnStart` -| Whether to drop and recreate the schema before applying migrations on startup. -| `false` - -| `grails.plugin.databasemigration.updateOnStartContexts` -| List of Liquibase contexts to apply during startup migration. -| `[]` _(all contexts)_ - -| `grails.plugin.databasemigration.updateOnStartLabels` -| List of Liquibase labels to apply during startup migration. -| `[]` _(all labels)_ - -| `grails.plugin.databasemigration.updateOnStartDefaultSchema` -| The default schema to use when applying migrations on startup. -| _(database default schema)_ - -| `grails.plugin.databasemigration.databaseChangeLogTableName` -| Custom name for the Liquibase changelog tracking table. -| `DATABASECHANGELOG` - -| `grails.plugin.databasemigration.databaseChangeLogLockTableName` -| Custom name for the Liquibase lock table. -| `DATABASECHANGELOGLOCK` - -| `grails.plugin.databasemigration.changelogLocation` -| The directory containing migration changelog files. -| `grails-app/migrations` - -| `grails.plugin.databasemigration.changelogFileName` -| The default changelog filename for CLI commands. -| `changelog.groovy` (default ds), `changelog-.groovy` (named ds) - -| `grails.plugin.databasemigration.contexts` -| List of Liquibase contexts for CLI commands. -| `[]` _(all contexts)_ - -| `grails.plugin.databasemigration.excludeObjects` -| Comma-separated list of database object names to exclude from `dbm-gorm-diff` and `dbm-generate-changelog` output. Cannot be combined with `includeObjects`. -| `''` - -| `grails.plugin.databasemigration.includeObjects` -| Comma-separated list of database object names to include in diff/changelog output (all others excluded). Cannot be combined with `excludeObjects`. -| `''` - -| `grails.plugin.databasemigration.skipUpdateOnStartMainClasses` -| List of main class names that should skip auto-migration on startup (e.g., CLI command runners). -| `['grails.ui.command.GrailsApplicationContextCommandRunner']` -|=== - -=== Cache Plugin - -The Grails Cache plugin provides annotation-driven caching (`@Cacheable`, `@CacheEvict`, `@CachePut`). It is included as a default feature in generated Grails applications. - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.cache.enabled` -| Whether the cache plugin is enabled. -| `true` - -| `grails.cache.cleanAtStartup` -| Whether to clear all caches on application startup. -| `false` - -| `grails.cache.cacheManager` -| Fully qualified class name of the `CacheManager` implementation. -| `grails.plugin.cache.GrailsConcurrentMapCacheManager` - -| `grails.cache.clearAtStartup` -| Alias for `cleanAtStartup` (both are supported). -| `false` -|=== - -When using EHCache as the cache provider, additional properties are available: - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.cache.ehcache.ehcacheXmlLocation` -| Classpath location of the `ehcache.xml` configuration file. -| `classpath:ehcache.xml` - -| `grails.cache.ehcache.lockTimeout` -| Timeout (in milliseconds) for cache lock acquisition. -| `200` -|=== - -=== Asset Pipeline Plugin - -The https://github.com/wondrify/asset-pipeline[Asset Pipeline plugin] manages and processes static assets (JavaScript, CSS, images) in Grails applications. It is included as a default feature in generated web applications. These properties are set under the `grails.assets` namespace. - -NOTE: Build-time properties (minification, digests, gzip) can also be configured in `build.gradle` via the `assets {}` block. The `application.yml` values shown here apply at runtime. - -==== Runtime Behavior - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.assets.mapping` -| The URL path segment for serving assets (e.g., `/assets/+++*+++`). Must be one level deep - `foo` is valid, `foo/bar` is not. -| `assets` - -| `grails.assets.bundle` -| Whether assets are bundled in development mode. When `false`, individual files are loaded separately for easier debugging. -| `false` - -| `grails.assets.url` -| Base URL for assets. Useful for CDN integration (e.g., `\https://cdn.example.com/`). Can also be a `Closure` accepting `HttpServletRequest` for dynamic URL generation in `application.groovy`. -| _(derived from request)_ - -| `grails.assets.storagePath` -| Directory path to copy compiled assets on application startup (e.g., for CDN upload). -| _(none)_ - -| `grails.assets.useManifest` -| Whether to use the `manifest.properties` file for asset resolution in production. -| `true` - -| `grails.assets.skipNotFound` -| If `true`, missing assets pass through to the next filter instead of returning 404. -| `false` - -| `grails.assets.allowDebugParam` -| If `true`, allows `?_debugAssets=y` query parameter to force non-bundled mode in production for debugging. -| `false` - -| `grails.assets.cacheLocation` -| Directory for caching compiled assets during development. -| `build/assetCache` -|=== - -==== Minification & Optimization - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.assets.minifyJs` -| Whether to minify JavaScript using Google Closure Compiler. -| `true` - -| `grails.assets.minifyCss` -| Whether to minify CSS. -| `true` - -| `grails.assets.enableSourceMaps` -| Whether to generate source maps for minified JavaScript files (`.js.map`). -| `true` - -| `grails.assets.enableDigests` -| Whether to generate digest/fingerprinted filenames (e.g., `app-abc123.js`). -| `true` - -| `grails.assets.skipNonDigests` -| If `true`, only digested filenames are generated. Non-digested names are served via manifest mapping. Reduces storage by 50%. -| `true` - -| `grails.assets.enableGzip` -| Whether to generate gzipped versions of assets (`.gz` files). -| `true` - -| `grails.assets.excludesGzip` -| List of GLOB patterns for files to exclude from gzip compression (e.g., `['+++**+++/+++*+++.png', '+++**+++/+++*+++.jpg']`). -| `[]` - -| `grails.assets.minifyOptions` -| Map of options passed to Google Closure Compiler. Keys: `languageMode` (input), `targetLanguage` (output), `optimizationLevel` (`SIMPLE`, `ADVANCED`, `WHITESPACE_ONLY`). -| `{languageMode: 'ES5', targetLanguage: 'ES5', optimizationLevel: 'SIMPLE'}` -|=== - -==== File Inclusion & Exclusion - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.assets.excludes` -| List of GLOB patterns (or `regex:` prefixed) for files to exclude from compilation. Excluded files can still be included via `require` directives. -| `[]` - -| `grails.assets.includes` -| List of GLOB patterns to override excludes. Allows specific files to be compiled even if they match an exclude pattern. -| `[]` -|=== - -==== JavaScript Processing - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.assets.enableES6` -| Enable ES6+ transpilation via Babel/SWC. If not set, auto-detects ES6 syntax. -| _(auto-detect)_ - -| `grails.assets.commonJs` -| Whether to enable CommonJS module support (`require()` / `module.exports`). -| `true` - -| `grails.assets.nodeEnv` -| Value injected as `process.env.NODE_ENV` in JavaScript files (used by libraries like React). -| `development` -|=== - -=== Spring Security Plugin - -The https://grails-plugins.github.io/grails-spring-security-core/[Grails Spring Security plugin] provides comprehensive authentication and authorization support for Grails applications. Detailed documentation is available in the plugin's full reference guide. - -==== General Settings - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.plugin.springsecurity.active` -| Whether the security plugin is active. -| `true` - -| `grails.plugin.springsecurity.printStatusMessages` -| Whether to print startup and status messages to the console. -| `true` - -| `grails.plugin.springsecurity.rejectIfNoRule` -| Whether to reject requests if no matching security rule is found. -| `true` - -| `grails.plugin.springsecurity.securityConfigType` -| The security configuration strategy (`Annotation`, `Requestmap`, `InterceptUrlMap`). -| `Annotation` - -| `grails.plugin.springsecurity.roleHierarchy` -| The role hierarchy definition string. -| `''` - -| `grails.plugin.springsecurity.cacheUsers` -| Whether to cache user details in the user details service. -| `false` - -| `grails.plugin.springsecurity.useHttpSessionEventPublisher` -| Whether to register a `HttpSessionEventPublisher` bean. -| `false` - -| `grails.plugin.springsecurity.useSecurityEventListener` -| Whether to publish security events to the Grails event system. -| `false` -|=== - -==== User & Role Configuration - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.plugin.springsecurity.userLookup.userDomainClassName` -| The fully qualified name of the user domain class. -| `null` _(must be set)_ - -| `grails.plugin.springsecurity.userLookup.usernamePropertyName` -| The property name for the username in the user domain class. -| `username` - -| `grails.plugin.springsecurity.userLookup.enabledPropertyName` -| The property name for the enabled status in the user domain class. -| `enabled` - -| `grails.plugin.springsecurity.userLookup.passwordPropertyName` -| The property name for the password in the user domain class. -| `password` - -| `grails.plugin.springsecurity.userLookup.authoritiesPropertyName` -| The property name for the authorities collection in the user domain class. -| `authorities` - -| `grails.plugin.springsecurity.userLookup.accountExpiredPropertyName` -| The property name for the account expired status in the user domain class. -| `accountExpired` - -| `grails.plugin.springsecurity.userLookup.accountLockedPropertyName` -| The property name for the account locked status in the user domain class. -| `accountLocked` - -| `grails.plugin.springsecurity.userLookup.passwordExpiredPropertyName` -| The property name for the password expired status in the user domain class. -| `passwordExpired` - -| `grails.plugin.springsecurity.userLookup.authorityJoinClassName` -| The fully qualified name of the user-authority join domain class. -| `null` _(must be set)_ - -| `grails.plugin.springsecurity.userLookup.usernameIgnoreCase` -| Whether to ignore case when looking up users by username. -| `false` - -| `grails.plugin.springsecurity.authority.className` -| The fully qualified name of the authority (role) domain class. -| `null` _(must be set)_ - -| `grails.plugin.springsecurity.authority.nameField` -| The property name for the authority name in the authority domain class. -| `authority` - -| `grails.plugin.springsecurity.useRoleGroups` -| Whether to enable support for role groups. -| `false` -|=== - -==== Authentication - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.plugin.springsecurity.apf.filterProcessesUrl` -| The URL the authentication processing filter handles. -| `/login/authenticate` - -| `grails.plugin.springsecurity.apf.usernameParameter` -| The HTTP parameter name for the username in login requests. -| `username` - -| `grails.plugin.springsecurity.apf.passwordParameter` -| The HTTP parameter name for the password in login requests. -| `password` - -| `grails.plugin.springsecurity.apf.postOnly` -| Whether to restrict authentication requests to HTTP POST. -| `true` - -| `grails.plugin.springsecurity.apf.allowSessionCreation` -| Whether to allow the authentication filter to create a new HTTP session. -| `true` - -| `grails.plugin.springsecurity.apf.storeLastUsername` -| Whether to store the last used username in the session after a failed login. -| `false` -|=== - -==== Login & Logout - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.plugin.springsecurity.auth.loginFormUrl` -| The URL of the login form page. -| `/login/auth` - -| `grails.plugin.springsecurity.auth.forceHttps` -| Whether to force HTTPS for the login page. -| `false` - -| `grails.plugin.springsecurity.auth.ajaxLoginFormUrl` -| The URL of the AJAX login form. -| `/login/authAjax` - -| `grails.plugin.springsecurity.auth.useForward` -| Whether to use a forward instead of a redirect to the login page. -| `false` - -| `grails.plugin.springsecurity.successHandler.defaultTargetUrl` -| The default URL to redirect to after a successful login. -| `/` - -| `grails.plugin.springsecurity.successHandler.alwaysUseDefault` -| Whether to always redirect to the default target URL after login. -| `false` - -| `grails.plugin.springsecurity.successHandler.ajaxSuccessUrl` -| The URL used for AJAX success responses. -| `/login/ajaxSuccess` - -| `grails.plugin.springsecurity.successHandler.useReferer` -| Whether to redirect to the `Referer` header URL after login. -| `false` - -| `grails.plugin.springsecurity.failureHandler.defaultFailureUrl` -| The default URL to redirect to after a failed login. -| `/login/authfail?login_error=1` - -| `grails.plugin.springsecurity.failureHandler.ajaxAuthFailUrl` -| The URL used for AJAX failure responses. -| `/login/authfail?ajax=true` - -| `grails.plugin.springsecurity.failureHandler.useForward` -| Whether to use a forward for authentication failure. -| `false` - -| `grails.plugin.springsecurity.logout.afterLogoutUrl` -| The URL to redirect to after logging out. -| `/` - -| `grails.plugin.springsecurity.logout.filterProcessesUrl` -| The URL the logout filter handles. -| `/logoff` - -| `grails.plugin.springsecurity.logout.postOnly` -| Whether to restrict logout requests to HTTP POST. -| `true` - -| `grails.plugin.springsecurity.logout.invalidateHttpSession` -| Whether to invalidate the HTTP session on logout. -| `true` - -| `grails.plugin.springsecurity.logout.clearAuthentication` -| Whether to clear the authentication from the security context on logout. -| `true` - -| `grails.plugin.springsecurity.logout.redirectToReferer` -| Whether to redirect to the `Referer` header URL after logout. -| `false` - -| `grails.plugin.springsecurity.adh.errorPage` -| The URL of the access denied page. -| `/login/denied` - -| `grails.plugin.springsecurity.adh.ajaxErrorPage` -| The URL of the AJAX access denied page. -| `/login/ajaxDenied` - -| `grails.plugin.springsecurity.adh.useForward` -| Whether to use a forward for access denied errors. -| `true` -|=== - -==== Password Encoding - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.plugin.springsecurity.password.algorithm` -| The password hashing algorithm. -| `bcrypt` - -| `grails.plugin.springsecurity.password.encodeHashAsBase64` -| Whether to encode the hashed password as Base64. -| `false` - -| `grails.plugin.springsecurity.password.bcrypt.logrounds` -| The number of log rounds for the BCrypt algorithm. -| `10` _(4 in test)_ - -| `grails.plugin.springsecurity.password.hash.iterations` -| The number of hash iterations for algorithms that support it. -| `10000` _(1 in test)_ -|=== - -==== Remember Me - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.plugin.springsecurity.rememberMe.cookieName` -| The name of the remember-me cookie. -| `grails_remember_me` - -| `grails.plugin.springsecurity.rememberMe.alwaysRemember` -| Whether to always remember the user, even if the checkbox is not checked. -| `false` - -| `grails.plugin.springsecurity.rememberMe.tokenValiditySeconds` -| The validity period (in seconds) of the remember-me token. -| `1209600` _(14 days)_ - -| `grails.plugin.springsecurity.rememberMe.parameter` -| The HTTP parameter name for the remember-me checkbox. -| `remember-me` - -| `grails.plugin.springsecurity.rememberMe.key` -| The secret key used to sign remember-me cookies. -| `grailsRocks` - -| `grails.plugin.springsecurity.rememberMe.persistent` -| Whether to use persistent (database-backed) remember-me tokens. -| `false` - -| `grails.plugin.springsecurity.rememberMe.useSecureCookie` -| Whether to use the `Secure` flag on the remember-me cookie. -| `null` - -| `grails.plugin.springsecurity.rememberMe.persistentToken.domainClassName` -| The fully qualified name of the persistent token domain class. -| `null` -|=== - -==== URL Mapping & Access Control - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.plugin.springsecurity.controllerAnnotations.staticRules` -| Map of static URL rules for controller-based security. -| `[]` - -| `grails.plugin.springsecurity.interceptUrlMap` -| Map of URL patterns to security rules. -| `[]` - -| `grails.plugin.springsecurity.requestMap.className` -| The fully qualified name of the `Requestmap` domain class. -| `null` - -| `grails.plugin.springsecurity.requestMap.urlField` -| The property name for the URL in the `Requestmap` domain class. -| `url` - -| `grails.plugin.springsecurity.requestMap.configAttributeField` -| The property name for the config attribute in the `Requestmap` domain class. -| `configAttribute` - -| `grails.plugin.springsecurity.fii.rejectPublicInvocations` -| Whether to reject invocations that do not match any security rule. -| `true` - -| `grails.plugin.springsecurity.fii.alwaysReauthenticate` -| Whether to always re-authenticate on every request. -| `false` - -| `grails.plugin.springsecurity.fii.validateConfigAttributes` -| Whether to validate configuration attributes at startup. -| `true` - -| `grails.plugin.springsecurity.fii.observeOncePerRequest` -| Whether to ensure security checks are performed only once per request. -| `true` -|=== - -==== Session & Security Context - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.plugin.springsecurity.useSessionFixationPrevention` -| Whether to enable session fixation prevention. -| `true` - -| `grails.plugin.springsecurity.sessionFixationPrevention.migrate` -| Whether to migrate session attributes to the new session after login. -| `true` - -| `grails.plugin.springsecurity.scr.allowSessionCreation` -| Whether the security context repository is allowed to create a session. -| `true` - -| `grails.plugin.springsecurity.scr.disableUrlRewriting` -| Whether to disable URL rewriting for session IDs. -| `true` - -| `grails.plugin.springsecurity.scpf.forceEagerSessionCreation` -| Whether to force eager creation of the HTTP session. -| `false` - -| `grails.plugin.springsecurity.sch.strategyName` -| The security context holder strategy (`MODE_THREADLOCAL`, `MODE_INHERITABLETHREADLOCAL`). -| `MODE_THREADLOCAL` -|=== - -==== Additional Authentication Methods - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.plugin.springsecurity.useBasicAuth` -| Whether to enable HTTP Basic authentication. -| `false` - -| `grails.plugin.springsecurity.basic.realmName` -| The realm name used in HTTP Basic authentication. -| `Grails Realm` - -| `grails.plugin.springsecurity.useSwitchUserFilter` -| Whether to enable the switch user filter for user impersonation. -| `false` - -| `grails.plugin.springsecurity.switchUser.switchUserUrl` -| The URL used to initiate a user switch. -| `/login/impersonate` - -| `grails.plugin.springsecurity.switchUser.exitUserUrl` -| The URL used to exit a user switch and return to the original user. -| `/logout/impersonate` -|=== - -==== GSP Layouts - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.plugin.springsecurity.gsp.layoutAuth` -| The Sitemesh layout used for the authentication page. -| `main` - -| `grails.plugin.springsecurity.gsp.layoutDenied` -| The Sitemesh layout used for the access denied page. -| `main` -|=== - -==== Other - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.plugin.springsecurity.ajaxHeader` -| The HTTP header name used to identify AJAX requests. -| `X-Requested-With` - -| `grails.plugin.springsecurity.ipRestrictions` -| List of IP-based restriction rules. -| `[]` - -| `grails.plugin.springsecurity.portMapper.httpPort` -| The standard HTTP port used for redirecting between secure and insecure pages. -| `8080` - -| `grails.plugin.springsecurity.portMapper.httpsPort` -| The standard HTTPS port used for redirecting between secure and insecure pages. -| `8443` - -| `grails.plugin.springsecurity.dao.hideUserNotFoundExceptions` -| Whether to hide `UsernameNotFoundException` and instead throw `BadCredentialsException`. -| `true` - -| `grails.plugin.springsecurity.providerManager.eraseCredentialsAfterAuthentication` -| Whether to erase password credentials from the `Authentication` object after successful authentication. -| `true` - -| `grails.plugin.springsecurity.debug.useFilter` -| Whether to enable the Spring Security debug filter. -| `false` -|=== - -=== MongoDB GORM Plugin - -The MongoDB GORM plugin provides GORM support for the MongoDB document database. Properties under `grails.mongodb.options.*` use reflection to map to the `MongoClientSettings.Builder` class. - -==== Connection - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.mongodb.url` -| The MongoDB connection string. -| `mongodb://localhost/test` - -| `grails.mongodb.host` -| The MongoDB server host. -| `localhost` - -| `grails.mongodb.port` -| The MongoDB server port. -| `27017` - -| `grails.mongodb.databaseName` -| The name of the MongoDB database. -| _(application name)_ - -| `grails.mongodb.username` -| The database username. -| _(none)_ - -| `grails.mongodb.password` -| The database password. -| _(none)_ -|=== - -==== Behavior - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.mongodb.stateless` -| Whether the GORM implementation is stateless (disabling persistence context). -| `false` - -| `grails.mongodb.decimalType` -| Whether to use the `Decimal128` type for `BigDecimal` properties. -| `false` - -| `grails.mongodb.codecs` -| List of custom MongoDB codec classes. -| `[]` - -| `grails.mongodb.default.mapping` -| A closure applied as the default mapping block for MongoDB domain classes. -| `{}` -|=== - -==== Connection Pool - -These properties are set under `grails.mongodb.options.connectionPoolSettings.*`. - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.mongodb.options.connectionPoolSettings.maxSize` -| The maximum number of connections in the pool. -| `100` - -| `grails.mongodb.options.connectionPoolSettings.minSize` -| The minimum number of connections in the pool. -| `0` - -| `grails.mongodb.options.connectionPoolSettings.maxWaitTime` -| The maximum time (in milliseconds) a thread will wait for a connection. -| `120000` - -| `grails.mongodb.options.connectionPoolSettings.maxConnectionLifeTime` -| The maximum life time (in milliseconds) of a pooled connection. -| `0` _(unlimited)_ - -| `grails.mongodb.options.connectionPoolSettings.maxConnectionIdleTime` -| The maximum idle time (in milliseconds) of a pooled connection. -| `0` _(unlimited)_ -|=== - -==== Client Options - -These properties are set under `grails.mongodb.options.*`. - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.mongodb.options.readPreference` -| The read preference strategy. -| _(none)_ - -| `grails.mongodb.options.writeConcern` -| The write concern strategy. -| _(none)_ - -| `grails.mongodb.options.readConcern` -| The read concern strategy. -| _(none)_ - -| `grails.mongodb.options.retryWrites` -| Whether to retry write operations on failure. -| _(none)_ - -| `grails.mongodb.options.retryReads` -| Whether to retry read operations on failure. -| _(none)_ - -| `grails.mongodb.options.applicationName` -| The name of the application (used for logging and monitoring). -| _(none)_ -|=== - -==== SSL - -These properties are set under `grails.mongodb.options.sslSettings.*`. - -[cols="3,5,2", options="header"] -|=== -| Property | Description | Default - -| `grails.mongodb.options.sslSettings.enabled` -| Whether SSL is enabled for connections. -| `false` - -| `grails.mongodb.options.sslSettings.invalidHostNameAllowed` -| Whether invalid hostnames are allowed in SSL certificates. -| `false` -|=== - - - diff --git a/grails-gsp/src/main/resources/META-INF/spring-configuration-metadata.json b/grails-gsp/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000000..3be85ef3f7b --- /dev/null +++ b/grails-gsp/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,100 @@ +{ + "groups": [ + { + "name": "grails.views", + "description": "Views & GSP" + }, + { + "name": "grails.views.gsp", + "description": "Views & GSP" + }, + { + "name": "grails.views.gsp.codecs", + "description": "Views & GSP" + }, + { + "name": "grails.gsp", + "description": "Views & GSP" + } + ], + "properties": [ + { + "name": "grails.views.default.codec", + "type": "java.lang.String", + "description": "The default encoding codec for GSP output where html reduces XSS risk and options are none, html, base64.", + "defaultValue": "none" + }, + { + "name": "grails.views.gsp.encoding", + "type": "java.lang.String", + "description": "The file encoding for GSP source files.", + "defaultValue": "UTF-8" + }, + { + "name": "grails.views.gsp.htmlcodec", + "type": "java.lang.String", + "description": "The HTML codec for GSP output (xml or html).", + "defaultValue": "xml" + }, + { + "name": "grails.views.gsp.codecs.expression", + "type": "java.lang.String", + "description": "The codec applied to GSP ${} expressions.", + "defaultValue": "html" + }, + { + "name": "grails.views.gsp.codecs.scriptlet", + "type": "java.lang.String", + "description": "The codec applied to GSP <% %> scriptlet output.", + "defaultValue": "html" + }, + { + "name": "grails.views.gsp.codecs.taglib", + "type": "java.lang.String", + "description": "The codec applied to tag library output.", + "defaultValue": "none" + }, + { + "name": "grails.views.gsp.codecs.staticparts", + "type": "java.lang.String", + "description": "The codec applied to static HTML parts of GSP pages.", + "defaultValue": "none" + }, + { + "name": "grails.views.gsp.layout.preprocess", + "type": "java.lang.Boolean", + "description": "Whether GSP layout preprocessing is enabled, where disabling allows Grails to parse rendered HTML but slows rendering.", + "defaultValue": true + }, + { + "name": "grails.views.enable.jsessionid", + "type": "java.lang.Boolean", + "description": "Whether to include the jsessionid in rendered links.", + "defaultValue": false + }, + { + "name": "grails.views.filteringCodecForContentType", + "type": "java.util.Map", + "description": "Map of content types to encoding codecs.", + "defaultValue": {} + }, + { + "name": "grails.gsp.disable.caching.resources", + "type": "java.lang.Boolean", + "description": "Whether to disable GSP resource caching.", + "defaultValue": false + }, + { + "name": "grails.gsp.enable.reload", + "type": "java.lang.Boolean", + "description": "Whether to enable GSP reloading in production.", + "defaultValue": false + }, + { + "name": "grails.gsp.view.dir", + "type": "java.lang.String", + "description": "Custom directory for GSP view resolution.", + "defaultValue": "grails-app/views" + } + ] +} 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 9fe8e0bce32..c8b347f3a6c 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 @@ -38,7 +38,7 @@ import spock.lang.Specification *
  • {@code @ConfigurationProperties} - Type-safe configuration beans
  • * * - *

    The hybrid report uses curated property metadata (from {@code config-properties.yml}) + *

    The hybrid report uses curated property metadata (from {@code spring-configuration-metadata.json}) * 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. diff --git a/grails-web-core/src/main/resources/META-INF/spring-configuration-metadata.json b/grails-web-core/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000000..1d3a24f8386 --- /dev/null +++ b/grails-web-core/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,248 @@ +{ + "groups": [ + { + "name": "grails.controllers", + "description": "Web & Controllers" + }, + { + "name": "grails.web", + "description": "Web & Controllers" + }, + { + "name": "grails.filter", + "description": "Web & Controllers" + }, + { + "name": "grails.exceptionresolver", + "description": "Web & Controllers" + }, + { + "name": "grails.logging", + "description": "Web & Controllers" + }, + { + "name": "grails.cors", + "description": "CORS" + }, + { + "name": "grails.mime", + "description": "Content Negotiation & MIME Types" + }, + { + "name": "grails.converters", + "description": "Content Negotiation & MIME Types" + }, + { + "name": "grails.resources", + "description": "Static Resources" + }, + { + "name": "grails.urlmapping", + "description": "URL Mappings" + }, + { + "name": "grails.scaffolding", + "description": "Scaffolding" + } + ], + "properties": [ + { + "name": "grails.controllers.defaultScope", + "type": "java.lang.String", + "description": "The default scope for controllers (singleton, prototype, session).", + "defaultValue": "singleton" + }, + { + "name": "grails.controllers.upload.location", + "type": "java.lang.String", + "description": "The directory for temporary file uploads.", + "defaultValue": "System.getProperty('java.io.tmpdir')" + }, + { + "name": "grails.controllers.upload.maxFileSize", + "type": "java.lang.Integer", + "description": "Maximum file size for uploads (in bytes).", + "defaultValue": 1048576 + }, + { + "name": "grails.controllers.upload.maxRequestSize", + "type": "java.lang.Integer", + "description": "Maximum request size for multipart uploads (in bytes).", + "defaultValue": 10485760 + }, + { + "name": "grails.controllers.upload.fileSizeThreshold", + "type": "java.lang.Integer", + "description": "File size threshold (in bytes) above which uploads are written to disk.", + "defaultValue": 0 + }, + { + "name": "grails.web.url.converter", + "type": "java.lang.String", + "description": "The URL token converter strategy, use hyphenated for hyphen-separated URLs.", + "defaultValue": "camelCase" + }, + { + "name": "grails.web.linkGenerator.useCache", + "type": "java.lang.Boolean", + "description": "Whether to cache links generated by the link generator.", + "defaultValue": true + }, + { + "name": "grails.web.servlet.path", + "type": "java.lang.String", + "description": "The path the Grails dispatcher servlet is mapped to.", + "defaultValue": "/**" + }, + { + "name": "grails.filter.encoding", + "type": "java.lang.String", + "description": "The character encoding for the Grails character encoding filter.", + "defaultValue": "UTF-8" + }, + { + "name": "grails.filter.forceEncoding", + "type": "java.lang.Boolean", + "description": "Whether to force the encoding filter to set the encoding on the response.", + "defaultValue": true + }, + { + "name": "grails.exceptionresolver.logRequestParameters", + "type": "java.lang.Boolean", + "description": "Whether to log request parameters in exception stack traces.", + "defaultValue": true + }, + { + "name": "grails.exceptionresolver.params.exclude", + "type": "java.util.List", + "description": "List of parameter names to mask (replace with [*****]) in exception stack traces, typically used for password and creditCard.", + "defaultValue": [] + }, + { + "name": "grails.logging.stackTraceFiltererClass", + "type": "java.lang.String", + "description": "Fully qualified class name of a custom StackTraceFilterer implementation.", + "defaultValue": "org.grails.exceptions.reporting.DefaultStackTraceFilterer" + }, + { + "name": "grails.cors.enabled", + "type": "java.lang.Boolean", + "description": "Whether CORS support is enabled.", + "defaultValue": false + }, + { + "name": "grails.cors.filter", + "type": "java.lang.Boolean", + "description": "Whether CORS is handled via a servlet filter (true) or an interceptor (false).", + "defaultValue": true + }, + { + "name": "grails.cors.allowedOrigins", + "type": "java.util.List", + "description": "List of allowed origins (e.g., http://localhost:5000), only applies when grails.cors.enabled is true.", + "defaultValue": ["*"] + }, + { + "name": "grails.cors.allowedMethods", + "type": "java.util.List", + "description": "List of allowed HTTP methods.", + "defaultValue": ["*"] + }, + { + "name": "grails.cors.allowedHeaders", + "type": "java.util.List", + "description": "List of allowed request headers.", + "defaultValue": ["*"] + }, + { + "name": "grails.cors.exposedHeaders", + "type": "java.util.List", + "description": "List of response headers to expose to the client.", + "defaultValue": [] + }, + { + "name": "grails.cors.maxAge", + "type": "java.lang.Integer", + "description": "How long (in seconds) the preflight response can be cached.", + "defaultValue": 1800 + }, + { + "name": "grails.cors.allowCredentials", + "type": "java.lang.Boolean", + "description": "Whether credentials (cookies, authorization headers) are supported.", + "defaultValue": false + }, + { + "name": "grails.cors.mappings", + "type": "java.util.Map", + "description": "Map of URL patterns to per-path CORS configuration where defining any mapping disables the global /** mapping.", + "defaultValue": {} + }, + { + "name": "grails.mime.types", + "type": "java.util.Map", + "description": "Map of MIME type names to content type strings used for content negotiation.", + "defaultValue": "see web profile application.yml" + }, + { + "name": "grails.mime.file.extensions", + "type": "java.lang.Boolean", + "description": "Whether to use the file extension to determine the MIME type in content negotiation.", + "defaultValue": true + }, + { + "name": "grails.mime.use.accept.header", + "type": "java.lang.Boolean", + "description": "Whether to use the Accept header for content negotiation.", + "defaultValue": true + }, + { + "name": "grails.mime.disable.accept.header.userAgents", + "type": "java.util.List", + "description": "List of user agent substrings (e.g., Gecko, WebKit) for which Accept header processing is disabled.", + "defaultValue": [] + }, + { + "name": "grails.mime.disable.accept.header.userAgentsXhr", + "type": "java.lang.Boolean", + "description": "When true, XHR requests also respect the grails.mime.disable.accept.header.userAgents setting, while by default XHR requests ignore user agent filtering.", + "defaultValue": false + }, + { + "name": "grails.converters.encoding", + "type": "java.lang.String", + "description": "The character encoding for converter output (JSON or XML).", + "defaultValue": "UTF-8" + }, + { + "name": "grails.resources.enabled", + "type": "java.lang.Boolean", + "description": "Whether serving static files from src/main/resources/public is enabled.", + "defaultValue": true + }, + { + "name": "grails.resources.pattern", + "type": "java.lang.String", + "description": "The URL path pattern for serving static resources.", + "defaultValue": "/static/**" + }, + { + "name": "grails.resources.cachePeriod", + "type": "java.lang.Integer", + "description": "The cache period (in seconds) for static resource HTTP responses.", + "defaultValue": 0 + }, + { + "name": "grails.urlmapping.cache.maxsize", + "type": "java.lang.Integer", + "description": "The maximum size of the URL mapping cache.", + "defaultValue": 1000 + }, + { + "name": "grails.scaffolding.templates.domainSuffix", + "type": "java.lang.String", + "description": "The suffix appended to domain class names when generating scaffolding templates.", + "defaultValue": "" + } + ] +} From f4ad3e6da168c4074458a40018d10622b0d049ed Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 26 Feb 2026 13:11:37 -0500 Subject: [PATCH 08/11] Fix project path for grails-data-hibernate5-core in doc generation task Use the correct Gradle project path :grails-data-hibernate5-core instead of :grails-data-hibernate5:core which does not exist as a project. Assisted-by: Claude Code --- grails-doc/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/grails-doc/build.gradle b/grails-doc/build.gradle index c60dd977bbe..51008c07d05 100644 --- a/grails-doc/build.gradle +++ b/grails-doc/build.gradle @@ -176,7 +176,7 @@ generateConfigReference.configure { Task it -> project(':grails-web-core').tasks.named('jar'), project(':grails-gsp').tasks.named('jar'), project(':grails-databinding').tasks.named('jar'), - project(':grails-data-hibernate5:core').tasks.named('jar') + project(':grails-data-hibernate5-core').tasks.named('jar') ) it.description = 'Generates the Application Properties reference from configuration metadata.' @@ -190,7 +190,7 @@ generateConfigReference.configure { Task it -> project(':grails-web-core').tasks.named('jar').flatMap { task -> task.archiveFile }, project(':grails-gsp').tasks.named('jar').flatMap { task -> task.archiveFile }, project(':grails-databinding').tasks.named('jar').flatMap { task -> task.archiveFile }, - project(':grails-data-hibernate5:core').tasks.named('jar').flatMap { task -> task.archiveFile } + project(':grails-data-hibernate5-core').tasks.named('jar').flatMap { task -> task.archiveFile } ) it.outputs.file(outputFile) @@ -204,7 +204,7 @@ generateConfigReference.configure { Task it -> project(':grails-web-core').tasks.named('jar').get().archiveFile.get().asFile, project(':grails-gsp').tasks.named('jar').get().archiveFile.get().asFile, project(':grails-databinding').tasks.named('jar').get().archiveFile.get().asFile, - project(':grails-data-hibernate5:core').tasks.named('jar').get().archiveFile.get().asFile + project(':grails-data-hibernate5-core').tasks.named('jar').get().archiveFile.get().asFile ] Map groupDescriptions = new LinkedHashMap() From 82144a98e56d8ab378a4578d7957a6919455734a Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 26 Feb 2026 13:25:12 -0500 Subject: [PATCH 09/11] fix: remove unnecessary Groovy imports flagged by CodeNarc Remove java.util.Enumeration, java.util.Locale, and java.net.URL imports that are auto-imported in Groovy and cause UnnecessaryGroovyImport CodeNarc violations. Assisted-by: Claude Code --- .../main/groovy/grails/dev/commands/ConfigReportCommand.groovy | 3 --- 1 file changed, 3 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 5a669171115..674947df353 100644 --- a/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy +++ b/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy @@ -22,9 +22,6 @@ import groovy.json.JsonSlurper import groovy.transform.CompileStatic import groovy.util.logging.Slf4j -import java.util.Enumeration -import java.util.Locale -import java.net.URL import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.env.EnumerablePropertySource From d0669b3319f6d522b7e817ee2d6f4e2f589b1260 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 26 Feb 2026 15:23:51 -0500 Subject: [PATCH 10/11] feat: add configuration metadata for Cache, DB Migration, MongoDB, and JSON Views Add spring-configuration-metadata.json files for in-repo plugin modules: - grails-cache: 6 properties (grails.cache.*) - grails-data-hibernate5-dbmigration: 15 properties (grails.plugin.databasemigration.*) - grails-data-mongodb: 23 properties (grails.mongodb.*) - grails-views-gson: 9 properties (grails.views.json.*) Update grails-gsp metadata descriptions to match original documentation. Update generateConfigReference Gradle task to include all new modules and fix Plugin References with direct anchor links to config sections. Fix consecutive blank lines CodeNarc violation in ConfigReportCommand. Assisted-by: Claude Code --- .../spring-configuration-metadata.json | 46 +++++ .../dev/commands/ConfigReportCommand.groovy | 1 - .../spring-configuration-metadata.json | 106 ++++++++++++ .../spring-configuration-metadata.json | 160 ++++++++++++++++++ grails-doc/build.gradle | 30 +++- .../spring-configuration-metadata.json | 4 +- .../spring-configuration-metadata.json | 67 ++++++++ 7 files changed, 402 insertions(+), 12 deletions(-) create mode 100644 grails-cache/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 grails-data-hibernate5/dbmigration/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 grails-data-mongodb/grails-plugin/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 grails-views-gson/src/main/resources/META-INF/spring-configuration-metadata.json diff --git a/grails-cache/src/main/resources/META-INF/spring-configuration-metadata.json b/grails-cache/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000000..1e7a631d854 --- /dev/null +++ b/grails-cache/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,46 @@ +{ + "groups": [ + { + "name": "grails.cache", + "description": "Cache Plugin" + } + ], + "properties": [ + { + "name": "grails.cache.enabled", + "type": "java.lang.Boolean", + "description": "Whether the cache plugin is enabled.", + "defaultValue": true + }, + { + "name": "grails.cache.clearAtStartup", + "type": "java.lang.Boolean", + "description": "Whether to clear all caches when the application starts.", + "defaultValue": false + }, + { + "name": "grails.cache.cacheManager", + "type": "java.lang.String", + "description": "The cache manager implementation class name. Use `GrailsConcurrentLinkedMapCacheManager` for bounded caches with maxCapacity support.", + "defaultValue": "GrailsConcurrentMapCacheManager" + }, + { + "name": "grails.cache.caches", + "type": "java.util.Map", + "description": "Map of cache-specific configurations keyed by cache name, each supporting a `maxCapacity` setting (used by GrailsConcurrentLinkedMapCacheManager).", + "defaultValue": {} + }, + { + "name": "grails.cache.ehcache.ehcacheXmlLocation", + "type": "java.lang.String", + "description": "Location of the Ehcache XML configuration file on the classpath.", + "defaultValue": "classpath:ehcache.xml" + }, + { + "name": "grails.cache.ehcache.lockTimeout", + "type": "java.lang.Integer", + "description": "The timeout in milliseconds for acquiring a lock on a cache element.", + "defaultValue": 200 + } + ] +} 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 674947df353..83e9373a201 100644 --- a/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy +++ b/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy @@ -22,7 +22,6 @@ import groovy.json.JsonSlurper import groovy.transform.CompileStatic import groovy.util.logging.Slf4j - import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.env.EnumerablePropertySource import org.springframework.core.env.PropertySource diff --git a/grails-data-hibernate5/dbmigration/src/main/resources/META-INF/spring-configuration-metadata.json b/grails-data-hibernate5/dbmigration/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000000..302af8c095f --- /dev/null +++ b/grails-data-hibernate5/dbmigration/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,106 @@ +{ + "groups": [ + { + "name": "grails.plugin.databasemigration", + "description": "Database Migration Plugin" + }, + { + "name": "grails.plugin.databasemigration.startup", + "description": "Database Migration Plugin - Startup" + } + ], + "properties": [ + { + "name": "grails.plugin.databasemigration.updateOnStart", + "type": "java.lang.Boolean", + "description": "Whether to run changesets from the specified file at application startup.", + "defaultValue": false + }, + { + "name": "grails.plugin.databasemigration.updateAllOnStart", + "type": "java.lang.Boolean", + "description": "Whether to run changesets at startup for all configured datasources.", + "defaultValue": false + }, + { + "name": "grails.plugin.databasemigration.updateOnStartFileName", + "type": "java.lang.String", + "description": "The changelog file name to run at startup. For named datasources uses `changelog-.groovy`.", + "defaultValue": "changelog.groovy" + }, + { + "name": "grails.plugin.databasemigration.dropOnStart", + "type": "java.lang.Boolean", + "description": "Whether to drop all database tables before auto-running migrations at startup.", + "defaultValue": false + }, + { + "name": "grails.plugin.databasemigration.updateOnStartContexts", + "type": "java.util.List", + "description": "Liquibase contexts to activate when running migrations at startup. Empty means all contexts.", + "defaultValue": [] + }, + { + "name": "grails.plugin.databasemigration.updateOnStartLabels", + "type": "java.util.List", + "description": "Liquibase labels to filter changesets when running migrations at startup. Empty means all labels.", + "defaultValue": [] + }, + { + "name": "grails.plugin.databasemigration.updateOnStartDefaultSchema", + "type": "java.lang.String", + "description": "The default database schema to use when running migrations at startup.", + "defaultValue": null + }, + { + "name": "grails.plugin.databasemigration.databaseChangeLogTableName", + "type": "java.lang.String", + "description": "The name of the Liquibase changelog tracking table.", + "defaultValue": "DATABASECHANGELOG" + }, + { + "name": "grails.plugin.databasemigration.databaseChangeLogLockTableName", + "type": "java.lang.String", + "description": "The name of the Liquibase lock table used to prevent concurrent migrations.", + "defaultValue": "DATABASECHANGELOGLOCK" + }, + { + "name": "grails.plugin.databasemigration.changelogLocation", + "type": "java.lang.String", + "description": "The directory containing changelog files.", + "defaultValue": "grails-app/migrations" + }, + { + "name": "grails.plugin.databasemigration.changelogFileName", + "type": "java.lang.String", + "description": "The name of the main changelog file. For named datasources uses `changelog-.groovy`.", + "defaultValue": "changelog.groovy" + }, + { + "name": "grails.plugin.databasemigration.contexts", + "type": "java.lang.String", + "description": "Comma-delimited list of Liquibase contexts to use for all operations.", + "defaultValue": null + }, + { + "name": "grails.plugin.databasemigration.excludeObjects", + "type": "java.lang.String", + "description": "Comma-delimited list of database objects to ignore in diff and generate operations.", + "defaultValue": null + }, + { + "name": "grails.plugin.databasemigration.includeObjects", + "type": "java.lang.String", + "description": "Comma-delimited list of database objects to include in diff and generate operations (all others excluded).", + "defaultValue": null + }, + { + "name": "grails.plugin.databasemigration.skipUpdateOnStartMainClasses", + "type": "java.util.List", + "description": "List of main class names for which startup migrations are skipped, preventing migrations when running CLI commands.", + "defaultValue": [ + "grails.ui.command.GrailsApplicationContextCommandRunner" + ] + } + ] +} diff --git a/grails-data-mongodb/grails-plugin/src/main/resources/META-INF/spring-configuration-metadata.json b/grails-data-mongodb/grails-plugin/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000000..998fb14858b --- /dev/null +++ b/grails-data-mongodb/grails-plugin/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,160 @@ +{ + "groups": [ + { + "name": "grails.mongodb", + "description": "MongoDB GORM Plugin" + }, + { + "name": "grails.mongodb.options", + "description": "MongoDB GORM Plugin - Client Options" + }, + { + "name": "grails.mongodb.options.connectionPoolSettings", + "description": "MongoDB GORM Plugin - Connection Pool" + }, + { + "name": "grails.mongodb.options.sslSettings", + "description": "MongoDB GORM Plugin - SSL" + } + ], + "properties": [ + { + "name": "grails.mongodb.url", + "type": "java.lang.String", + "description": "The MongoDB connection URL. Supports the full MongoDB connection string format.", + "defaultValue": "mongodb://localhost/test" + }, + { + "name": "grails.mongodb.host", + "type": "java.lang.String", + "description": "The MongoDB server hostname. Ignored when `url` or `connectionString` is set.", + "defaultValue": "localhost" + }, + { + "name": "grails.mongodb.port", + "type": "java.lang.Integer", + "description": "The MongoDB server port. Ignored when `url` or `connectionString` is set.", + "defaultValue": 27017 + }, + { + "name": "grails.mongodb.databaseName", + "type": "java.lang.String", + "description": "The MongoDB database name.", + "defaultValue": null + }, + { + "name": "grails.mongodb.username", + "type": "java.lang.String", + "description": "The username for MongoDB authentication.", + "defaultValue": null + }, + { + "name": "grails.mongodb.password", + "type": "java.lang.String", + "description": "The password for MongoDB authentication.", + "defaultValue": null + }, + { + "name": "grails.mongodb.stateless", + "type": "java.lang.Boolean", + "description": "Whether to use stateless mode with no session-level caching of entities.", + "defaultValue": false + }, + { + "name": "grails.mongodb.decimalType", + "type": "java.lang.Boolean", + "description": "Whether to use Decimal128 for BigDecimal values instead of Double.", + "defaultValue": false + }, + { + "name": "grails.mongodb.codecs", + "type": "java.util.List", + "description": "List of custom MongoDB Codec classes to register for BSON serialization.", + "defaultValue": [] + }, + { + "name": "grails.mongodb.default.mapping", + "type": "groovy.lang.Closure", + "description": "A closure applied as the default GORM mapping block for all MongoDB domain classes.", + "defaultValue": {} + }, + { + "name": "grails.mongodb.options.connectionPoolSettings.maxSize", + "type": "java.lang.Integer", + "description": "Maximum number of connections in the connection pool.", + "defaultValue": 100 + }, + { + "name": "grails.mongodb.options.connectionPoolSettings.minSize", + "type": "java.lang.Integer", + "description": "Minimum number of connections in the connection pool.", + "defaultValue": 0 + }, + { + "name": "grails.mongodb.options.connectionPoolSettings.maxWaitTime", + "type": "java.lang.Long", + "description": "Maximum time in milliseconds to wait for a connection from the pool.", + "defaultValue": 120000 + }, + { + "name": "grails.mongodb.options.connectionPoolSettings.maxConnectionLifeTime", + "type": "java.lang.Long", + "description": "Maximum lifetime in milliseconds of a pooled connection (0 for unlimited).", + "defaultValue": 0 + }, + { + "name": "grails.mongodb.options.connectionPoolSettings.maxConnectionIdleTime", + "type": "java.lang.Long", + "description": "Maximum idle time in milliseconds before a pooled connection is closed (0 for unlimited).", + "defaultValue": 0 + }, + { + "name": "grails.mongodb.options.readPreference", + "type": "java.lang.String", + "description": "The MongoDB read preference (e.g., primary, secondary, nearest).", + "defaultValue": null + }, + { + "name": "grails.mongodb.options.writeConcern", + "type": "java.lang.String", + "description": "The MongoDB write concern (e.g., majority, w1, journaled).", + "defaultValue": null + }, + { + "name": "grails.mongodb.options.readConcern", + "type": "java.lang.String", + "description": "The MongoDB read concern level (e.g., local, majority, linearizable).", + "defaultValue": null + }, + { + "name": "grails.mongodb.options.retryWrites", + "type": "java.lang.Boolean", + "description": "Whether to retry write operations on transient network errors.", + "defaultValue": null + }, + { + "name": "grails.mongodb.options.retryReads", + "type": "java.lang.Boolean", + "description": "Whether to retry read operations on transient network errors.", + "defaultValue": null + }, + { + "name": "grails.mongodb.options.applicationName", + "type": "java.lang.String", + "description": "Application name sent to MongoDB for server logs and profiling.", + "defaultValue": null + }, + { + "name": "grails.mongodb.options.sslSettings.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable TLS/SSL for connections to MongoDB.", + "defaultValue": false + }, + { + "name": "grails.mongodb.options.sslSettings.invalidHostNameAllowed", + "type": "java.lang.Boolean", + "description": "Whether to allow connections to MongoDB servers with invalid hostnames in TLS certificates.", + "defaultValue": false + } + ] +} diff --git a/grails-doc/build.gradle b/grails-doc/build.gradle index 51008c07d05..2d3165e6347 100644 --- a/grails-doc/build.gradle +++ b/grails-doc/build.gradle @@ -176,7 +176,11 @@ generateConfigReference.configure { Task it -> project(':grails-web-core').tasks.named('jar'), project(':grails-gsp').tasks.named('jar'), project(':grails-databinding').tasks.named('jar'), - project(':grails-data-hibernate5-core').tasks.named('jar') + project(':grails-data-hibernate5-core').tasks.named('jar'), + project(':grails-cache').tasks.named('jar'), + project(':grails-data-hibernate5-dbmigration').tasks.named('jar'), + project(':grails-data-mongodb').tasks.named('jar'), + project(':grails-views-gson').tasks.named('jar') ) it.description = 'Generates the Application Properties reference from configuration metadata.' @@ -190,7 +194,11 @@ generateConfigReference.configure { Task it -> project(':grails-web-core').tasks.named('jar').flatMap { task -> task.archiveFile }, project(':grails-gsp').tasks.named('jar').flatMap { task -> task.archiveFile }, project(':grails-databinding').tasks.named('jar').flatMap { task -> task.archiveFile }, - project(':grails-data-hibernate5-core').tasks.named('jar').flatMap { task -> task.archiveFile } + project(':grails-data-hibernate5-core').tasks.named('jar').flatMap { task -> task.archiveFile }, + project(':grails-cache').tasks.named('jar').flatMap { task -> task.archiveFile }, + project(':grails-data-hibernate5-dbmigration').tasks.named('jar').flatMap { task -> task.archiveFile }, + project(':grails-data-mongodb').tasks.named('jar').flatMap { task -> task.archiveFile }, + project(':grails-views-gson').tasks.named('jar').flatMap { task -> task.archiveFile } ) it.outputs.file(outputFile) @@ -204,7 +212,11 @@ generateConfigReference.configure { Task it -> project(':grails-web-core').tasks.named('jar').get().archiveFile.get().asFile, project(':grails-gsp').tasks.named('jar').get().archiveFile.get().asFile, project(':grails-databinding').tasks.named('jar').get().archiveFile.get().asFile, - project(':grails-data-hibernate5-core').tasks.named('jar').get().archiveFile.get().asFile + project(':grails-data-hibernate5-core').tasks.named('jar').get().archiveFile.get().asFile, + project(':grails-cache').tasks.named('jar').get().archiveFile.get().asFile, + project(':grails-data-hibernate5-dbmigration').tasks.named('jar').get().archiveFile.get().asFile, + project(':grails-data-mongodb').tasks.named('jar').get().archiveFile.get().asFile, + project(':grails-views-gson').tasks.named('jar').get().archiveFile.get().asFile ] Map groupDescriptions = new LinkedHashMap() @@ -347,7 +359,10 @@ generateConfigReference.configure { Task it -> 'JSON & Converters', 'GORM', 'DataSource', - 'Hibernate' + 'Hibernate', + 'Database Migration Plugin', + 'Cache Plugin', + 'MongoDB GORM Plugin' ] List orderedCategories = new ArrayList() preferredOrder.each { String category -> @@ -464,11 +479,8 @@ generateConfigReference.configure { Task it -> writer.writeLine('') writer.writeLine('The following plugin documentation covers configuration properties that are not listed above:') writer.writeLine('') - writer.writeLine('- Spring Security - https://apache.github.io/grails-spring-security/') - writer.writeLine('- Asset Pipeline - https://github.com/bertramdev/asset-pipeline') - writer.writeLine('- Database Migration - https://github.com/grails/grails-database-migration') - writer.writeLine('- Cache - https://github.com/grails/grails-cache') - writer.writeLine('- MongoDB - https://github.com/grails/grails-data-mapping') + writer.writeLine('- https://apache.github.io/grails-spring-security/snapshot/spring-security-core/index.html#_configuration_properties[Spring Security Core Configuration Properties]') + writer.writeLine('- https://github.com/bertramdev/asset-pipeline#configuration[Asset Pipeline Configuration Properties]') writer.writeLine('') } diff --git a/grails-gsp/src/main/resources/META-INF/spring-configuration-metadata.json b/grails-gsp/src/main/resources/META-INF/spring-configuration-metadata.json index 3be85ef3f7b..98ee551a046 100644 --- a/grails-gsp/src/main/resources/META-INF/spring-configuration-metadata.json +++ b/grails-gsp/src/main/resources/META-INF/spring-configuration-metadata.json @@ -21,7 +21,7 @@ { "name": "grails.views.default.codec", "type": "java.lang.String", - "description": "The default encoding codec for GSP output where html reduces XSS risk and options are none, html, base64.", + "description": "The default encoding codec for GSP output. Set to `html` to reduce XSS risk. Options: `none`, `html`, `base64`.", "defaultValue": "none" }, { @@ -63,7 +63,7 @@ { "name": "grails.views.gsp.layout.preprocess", "type": "java.lang.Boolean", - "description": "Whether GSP layout preprocessing is enabled, where disabling allows Grails to parse rendered HTML but slows rendering.", + "description": "Whether GSP layout preprocessing is enabled. Disabling allows Grails to parse rendered HTML but slows rendering.", "defaultValue": true }, { diff --git a/grails-views-gson/src/main/resources/META-INF/spring-configuration-metadata.json b/grails-views-gson/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000000..62e7e7a143a --- /dev/null +++ b/grails-views-gson/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,67 @@ +{ + "groups": [ + { + "name": "grails.views.json", + "description": "JSON Views" + } + ], + "properties": [ + { + "name": "grails.views.json.mimeTypes", + "type": "java.util.List", + "description": "MIME types handled by JSON views.", + "defaultValue": [ + "application/json", + "application/hal+json" + ] + }, + { + "name": "grails.views.json.generator.escapeUnicode", + "type": "java.lang.Boolean", + "description": "Whether to escape Unicode characters in JSON output.", + "defaultValue": false + }, + { + "name": "grails.views.json.generator.dateFormat", + "type": "java.lang.String", + "description": "The date format pattern for JSON serialization.", + "defaultValue": "yyyy-MM-dd'T'HH:mm:ss.SSSX" + }, + { + "name": "grails.views.json.generator.timeZone", + "type": "java.lang.String", + "description": "The time zone for JSON date serialization.", + "defaultValue": "GMT" + }, + { + "name": "grails.views.json.generator.locale", + "type": "java.lang.String", + "description": "The locale for JSON output formatting.", + "defaultValue": "en/US" + }, + { + "name": "grails.views.json.compileStatic", + "type": "java.lang.Boolean", + "description": "Whether JSON views are statically compiled.", + "defaultValue": true + }, + { + "name": "grails.views.json.encoding", + "type": "java.lang.String", + "description": "The character encoding for JSON views.", + "defaultValue": "UTF-8" + }, + { + "name": "grails.views.json.prettyPrint", + "type": "java.lang.Boolean", + "description": "Whether to pretty-print JSON output with indentation.", + "defaultValue": false + }, + { + "name": "grails.views.json.allowResourceExpansion", + "type": "java.lang.Boolean", + "description": "Whether to allow HAL resource expansion in JSON views.", + "defaultValue": true + } + ] +} From 6cdb4ce348d7b19413d77dffd298c37c893e430c Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 26 Feb 2026 16:48:19 -0500 Subject: [PATCH 11/11] Fix GSP metadata location and update plugin reference links - Move spring-configuration-metadata.json from grails-gsp/src/ to grails-gsp/plugin/src/ to match the :grails-gsp project directory (grails-gsp/plugin). This was preventing 13 Views & GSP properties from appearing in the generated doc. - Update Spring Security link to correct published docs path - Update Asset Pipeline link to GitHub Pages URL Assisted-by: Claude Code --- grails-doc/build.gradle | 4 ++-- .../resources/META-INF/spring-configuration-metadata.json | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename grails-gsp/{ => plugin}/src/main/resources/META-INF/spring-configuration-metadata.json (100%) diff --git a/grails-doc/build.gradle b/grails-doc/build.gradle index 2d3165e6347..4a442fdfdee 100644 --- a/grails-doc/build.gradle +++ b/grails-doc/build.gradle @@ -479,8 +479,8 @@ generateConfigReference.configure { Task it -> writer.writeLine('') writer.writeLine('The following plugin documentation covers configuration properties that are not listed above:') writer.writeLine('') - writer.writeLine('- https://apache.github.io/grails-spring-security/snapshot/spring-security-core/index.html#_configuration_properties[Spring Security Core Configuration Properties]') - writer.writeLine('- https://github.com/bertramdev/asset-pipeline#configuration[Asset Pipeline Configuration Properties]') + writer.writeLine('- https://apache.github.io/grails-spring-security/snapshot/core-plugin/guide/index.html#_configuration_properties[Spring Security Core Configuration Properties]') + writer.writeLine('- https://wondrify.github.io/asset-pipeline/[Asset Pipeline Configuration Properties]') writer.writeLine('') } diff --git a/grails-gsp/src/main/resources/META-INF/spring-configuration-metadata.json b/grails-gsp/plugin/src/main/resources/META-INF/spring-configuration-metadata.json similarity index 100% rename from grails-gsp/src/main/resources/META-INF/spring-configuration-metadata.json rename to grails-gsp/plugin/src/main/resources/META-INF/spring-configuration-metadata.json