jdaugherty commented on code in PR #15568:
URL: https://github.com/apache/grails-core/pull/15568#discussion_r3196289258


##########
grails-bom/hibernate5-micronaut/build.gradle:
##########
@@ -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.
+ */
+
+import org.apache.grails.gradle.tasks.bom.ExtractDependenciesTask
+import org.apache.grails.gradle.tasks.bom.ExtractedDependencyConstraint
+import org.apache.grails.gradle.tasks.bom.PropertyNameCalculator
+
+buildscript {
+    apply from: rootProject.layout.projectDirectory.file('dependencies.gradle')
+}
+
+plugins {
+    id 'java-platform'
+    id 'org.apache.grails.buildsrc.publish'
+    id 'org.apache.grails.buildsrc.sbom'
+}
+
+version = projectVersion
+group = 'org.apache.grails'
+
+javaPlatform {
+    allowDependencies()
+}
+
+ext {
+    isReleaseBuild = System.getenv('GRAILS_PUBLISH_RELEASE') == 'true'
+    isPublishedExternal = 
System.getenv().containsKey('NEXUS_PUBLISH_STAGING_PROFILE_ID')
+    // TODO: It should be possible to pull these build names using 
includedBuild, but I haven't found a way to do so
+    gradleBuildProjects = [
+            'grails-gradle-plugins':'org.apache.grails',
+            'grails-gradle-model':'org.apache.grails.gradle',
+            'grails-gradle-common':'org.apache.grails.gradle',
+            'grails-gradle-tasks':'org.apache.grails',
+    ]
+}
+
+// Register the Micronaut platform in combinedPlatforms/combinedVersions so
+// PropertyNameCalculator (used by extractConstraints and pomCustomization) can
+// resolve a property name for the micronaut-platform constraint.
+project.ext.combinedPlatforms = combinedPlatforms + ['micronaut-platform': 
"io.micronaut.platform:micronaut-platform:$micronautPlatformVersion".toString()]
+project.ext.combinedVersions = combinedVersions + 
['micronaut-platform.version': micronautPlatformVersion as String]
+
+// Coordinates we override via customBomDependencies — these must be excluded 
from the
+// inherited platform chain so they don't conflict with our strictly-versioned 
overrides
+// when consumers apply this BOM via enforcedPlatform.
+def overriddenModules = customBomDependencies.values().collect { String coord 
->
+    def parts = coord.split(':')
+    [group: parts[0], module: parts[1]]
+}
+Set<String> overriddenCoords = overriddenModules.collect { 
"${it.group}:${it.module}".toString() } as Set
+
+dependencies {
+    api(platform(project(':grails-base-bom'))) {
+        overriddenModules.each { ovr ->
+            exclude group: ovr.group, module: ovr.module
+        }
+    }
+
+    // Re-export the Micronaut platform so consumers inherit Micronaut's 
managed versions
+    // transitively. Exclude Groovy since we declare the required version 
explicitly via
+    // customBomDependencies. Exclude Spock since we manage that version 
ourselves.
+    
api(platform("io.micronaut.platform:micronaut-platform:$micronautPlatformVersion"))
 {
+        exclude group: 'org.apache.groovy'
+        exclude group: 'org.spockframework'
+    }
+
+    constraints {
+        // Re-declare base BOM constraints directly so enforcedPlatform() 
consumers
+        // get forced versions. Constraints inherited via platform() are not 
enforced
+        // by enforcedPlatform — only direct constraints are.
+        // Skip entries we override below in customBomDependencies to avoid 
conflicting
+        // strictly constraints under enforcedPlatform.
+        gradleBomDependencies.values().each { String coord ->
+            def parts = coord.split(':')
+            String key = parts[0] + ':' + parts[1]
+            if (key in overriddenCoords) {
+                return
+            }
+            api coord
+        }
+        bomDependencies.values().each { String coord ->
+            def parts = coord.split(':')
+            String key = parts[0] + ':' + parts[1]
+            if (key in overriddenCoords) return
+            api coord
+        }
+        for (def entry : bomPlatformDependencies.entrySet()) {
+            api entry.value
+        }
+        // Re-declare the Micronaut platform as a constraint for 
enforcedPlatform support
+        api 
"io.micronaut.platform:micronaut-platform:$micronautPlatformVersion"
+        for (def entry : customBomDependencies.entrySet()) {
+            def parts = entry.value.split(':')
+            if (parts.length == 3) {
+                api("${parts[0]}:${parts[1]}") {
+                    version {
+                        strictly parts[2]
+                    }
+                }
+            } else {
+                api entry.value
+            }
+        }
+    }
+}
+
+configurations.register('bomDependencies').configure {
+    it.canBeResolved = true
+    it.transitive = true
+    it.extendsFrom(configurations.named('api').get())
+}
+
+tasks.register('extractConstraints', ExtractDependenciesTask).configure { 
ExtractDependenciesTask it ->
+    it.captureProjectServices(project.dependencies, project.configurations)
+    it.configuration = configurations.named('bomDependencies')
+    it.configurationName = 'bomDependencies'
+    it.destination = 
project.layout.buildDirectory.file('grails-hibernate5-micronaut-bom-constraints.adoc')
+    it.platformDefinitions = combinedPlatforms
+    it.definitions = combinedDependencies
+    it.projectName = project.name
+    it.versions = combinedVersions
+    // Micronaut's platform imports many sub-BOMs (micronaut-*-bom, netty-bom, 
etc.) that are
+    // not explicitly registered in dependencies.gradle. Auto-register them so 
extractConstraints
+    // can document their versions without requiring manual entries for every 
transitive platform.
+    // this is required because the micronaut bom format uses gradle modules 
instead of a pom like spring boot
+    it.autoRegisterTransitivePlatforms = true
+    rootProject.subprojects.each { p ->
+        evaluationDependsOn(p.path)
+    }
+    it.projectArtifactIds.set(project.provider {
+        Map<String, String> artifactIdMappings = [:]
+
+        rootProject.subprojects.each { p ->
+            artifactIdMappings[p.name] = p.findProperty('pomArtifactId') ?: 
p.name
+        }
+
+        for (Map.Entry<String, String> dependency : 
project.ext.gradleBuildProjects.entrySet()) {
+            artifactIdMappings[dependency.key] = dependency.key
+        }
+
+        artifactIdMappings
+    })
+    it.forcedGroupPrefixes.set(['org.apache.grails.profiles': 
'grails-profile'])
+    it.projectCoordinateProperties.set(project.provider {
+        Map<String, String> projectCoordinates = [:]
+
+        rootProject.subprojects.each { p ->
+            String artifactId = p.findProperty('pomArtifactId') as String ?: 
p.name
+            String baseVersionName = artifactId.replaceAll('[.]', '-')
+            projectCoordinates["${p.group}:${ artifactId}:${p.version}" as 
String] = baseVersionName
+        }
+
+        for (Map.Entry<String, String> dependency : 
project.ext.gradleBuildProjects.entrySet()) {
+            
projectCoordinates["${dependency.value}:${dependency.key}:${project.version}" 
as String] = dependency.key
+        }
+
+        projectCoordinates
+    })
+
+    
it.dependsOn(project.tasks.named('generateMetadataFileForMavenPublication'), 
project.tasks.named('generatePomFileForMavenPublication'))
+}
+
+def validateNoSnapshotDependencies = 
tasks.register('validateNoSnapshotDependencies')
+validateNoSnapshotDependencies.configure { Task it ->
+    it.group = 'publishing'
+    it.description = 'Validates that no snapshot dependencies are present in 
the project when performing a release.'
+
+    it.doLast {
+        configurations.each { config ->
+            config.allDependencies.each { dep ->
+                if (dep.version && dep.version.contains('-SNAPSHOT')) {
+                    throw new GradleException("Releases cannot have a snapshot 
dependency: ${dep.group}:${dep.name} (${dep.version})")
+                }
+            }
+        }
+    }
+}
+
+if (ext.isReleaseBuild && ext.isPublishedExternal) {
+    project.afterEvaluate {
+        tasks.named('generateMetadataFileForMavenPublication').configure {
+            dependsOn(validateNoSnapshotDependencies)
+        }
+        tasks.named('generatePomFileForMavenPublication').configure {
+            dependsOn(validateNoSnapshotDependencies)
+        }
+    }
+}
+
+ext {
+    pomDescription = 'Grails Hibernate 5 Micronaut BOM (Bill of Materials) for 
Grails projects integrating with Micronaut and Hibernate 5. Layers Hibernate 5 
dependency management on top of grails-micronaut-bom; consume as 
enforcedPlatform.'
+    pomCustomization = { xml ->
+        def root = xml.asNode()
+
+        def propertiesNode = root.properties ? root.properties[0] : 
root.appendNode('properties')
+
+        def depMgmt = root.dependencyManagement?.getAt(0)
+        def deps = depMgmt?.dependencies?.getAt(0)
+        if (deps) {
+            PropertyNameCalculator propertyNameCalculator = new 
PropertyNameCalculator(combinedPlatforms, combinedDependencies, 
combinedVersions)
+            
propertyNameCalculator.addForcedGroupPrefix('org.apache.grails.profiles', 
'grails-profile')
+            propertyNameCalculator.addProjects(rootProject.subprojects)
+            for (String gradleArtifactId : project.ext.gradleBuildProjects) {
+                propertyNameCalculator.addProject('org.apache.grails.gradle', 
gradleArtifactId, project.version as String, gradleArtifactId)
+            }
+
+            Map<String, String> pomProperties = [:]
+            deps.dependency.each { dep ->
+                String groupId = dep.groupId.text().trim()
+                String artifactId = dep.artifactId.text().trim()
+                boolean isBom = dep.scope.text().trim() == 'import'
+
+                String inlineVersion = dep.version.text().trim()
+                if (inlineVersion == 'null') {
+                    inlineVersion = null
+                }
+
+                if (inlineVersion) {
+                    ExtractedDependencyConstraint extractedConstraint = 
propertyNameCalculator.calculate(groupId, artifactId, inlineVersion, isBom)
+                    if (extractedConstraint?.versionPropertyReference) {
+                        // use the property reference instead of the hard 
coded version so that it can be
+                        // overriden by the spring boot dependency management 
plugin

Review Comment:
   Typo: `overriden` → `overridden` (same typo appears in 
`hibernate7-micronaut/build.gradle` line 238).



##########
grails-bom/hibernate5-micronaut/build.gradle:
##########
@@ -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.
+ */
+
+import org.apache.grails.gradle.tasks.bom.ExtractDependenciesTask
+import org.apache.grails.gradle.tasks.bom.ExtractedDependencyConstraint
+import org.apache.grails.gradle.tasks.bom.PropertyNameCalculator
+
+buildscript {
+    apply from: rootProject.layout.projectDirectory.file('dependencies.gradle')
+}
+
+plugins {
+    id 'java-platform'
+    id 'org.apache.grails.buildsrc.publish'
+    id 'org.apache.grails.buildsrc.sbom'
+}
+
+version = projectVersion
+group = 'org.apache.grails'
+
+javaPlatform {
+    allowDependencies()
+}
+
+ext {
+    isReleaseBuild = System.getenv('GRAILS_PUBLISH_RELEASE') == 'true'
+    isPublishedExternal = 
System.getenv().containsKey('NEXUS_PUBLISH_STAGING_PROFILE_ID')
+    // TODO: It should be possible to pull these build names using 
includedBuild, but I haven't found a way to do so
+    gradleBuildProjects = [
+            'grails-gradle-plugins':'org.apache.grails',
+            'grails-gradle-model':'org.apache.grails.gradle',
+            'grails-gradle-common':'org.apache.grails.gradle',
+            'grails-gradle-tasks':'org.apache.grails',
+    ]
+}
+
+// Register the Micronaut platform in combinedPlatforms/combinedVersions so
+// PropertyNameCalculator (used by extractConstraints and pomCustomization) can
+// resolve a property name for the micronaut-platform constraint.
+project.ext.combinedPlatforms = combinedPlatforms + ['micronaut-platform': 
"io.micronaut.platform:micronaut-platform:$micronautPlatformVersion".toString()]
+project.ext.combinedVersions = combinedVersions + 
['micronaut-platform.version': micronautPlatformVersion as String]
+
+// Coordinates we override via customBomDependencies — these must be excluded 
from the
+// inherited platform chain so they don't conflict with our strictly-versioned 
overrides
+// when consumers apply this BOM via enforcedPlatform.
+def overriddenModules = customBomDependencies.values().collect { String coord 
->
+    def parts = coord.split(':')
+    [group: parts[0], module: parts[1]]
+}
+Set<String> overriddenCoords = overriddenModules.collect { 
"${it.group}:${it.module}".toString() } as Set
+
+dependencies {
+    api(platform(project(':grails-base-bom'))) {
+        overriddenModules.each { ovr ->
+            exclude group: ovr.group, module: ovr.module
+        }
+    }
+
+    // Re-export the Micronaut platform so consumers inherit Micronaut's 
managed versions
+    // transitively. Exclude Groovy since we declare the required version 
explicitly via
+    // customBomDependencies. Exclude Spock since we manage that version 
ourselves.
+    
api(platform("io.micronaut.platform:micronaut-platform:$micronautPlatformVersion"))
 {
+        exclude group: 'org.apache.groovy'
+        exclude group: 'org.spockframework'
+    }
+
+    constraints {
+        // Re-declare base BOM constraints directly so enforcedPlatform() 
consumers
+        // get forced versions. Constraints inherited via platform() are not 
enforced
+        // by enforcedPlatform — only direct constraints are.
+        // Skip entries we override below in customBomDependencies to avoid 
conflicting
+        // strictly constraints under enforcedPlatform.
+        gradleBomDependencies.values().each { String coord ->
+            def parts = coord.split(':')
+            String key = parts[0] + ':' + parts[1]
+            if (key in overriddenCoords) {
+                return
+            }
+            api coord
+        }
+        bomDependencies.values().each { String coord ->
+            def parts = coord.split(':')
+            String key = parts[0] + ':' + parts[1]
+            if (key in overriddenCoords) return
+            api coord
+        }
+        for (def entry : bomPlatformDependencies.entrySet()) {
+            api entry.value
+        }
+        // Re-declare the Micronaut platform as a constraint for 
enforcedPlatform support
+        api 
"io.micronaut.platform:micronaut-platform:$micronautPlatformVersion"
+        for (def entry : customBomDependencies.entrySet()) {
+            def parts = entry.value.split(':')
+            if (parts.length == 3) {
+                api("${parts[0]}:${parts[1]}") {
+                    version {
+                        strictly parts[2]
+                    }
+                }
+            } else {
+                api entry.value
+            }
+        }
+    }
+}
+
+configurations.register('bomDependencies').configure {
+    it.canBeResolved = true
+    it.transitive = true
+    it.extendsFrom(configurations.named('api').get())
+}
+
+tasks.register('extractConstraints', ExtractDependenciesTask).configure { 
ExtractDependenciesTask it ->
+    it.captureProjectServices(project.dependencies, project.configurations)
+    it.configuration = configurations.named('bomDependencies')
+    it.configurationName = 'bomDependencies'
+    it.destination = 
project.layout.buildDirectory.file('grails-hibernate5-micronaut-bom-constraints.adoc')
+    it.platformDefinitions = combinedPlatforms
+    it.definitions = combinedDependencies
+    it.projectName = project.name
+    it.versions = combinedVersions
+    // Micronaut's platform imports many sub-BOMs (micronaut-*-bom, netty-bom, 
etc.) that are
+    // not explicitly registered in dependencies.gradle. Auto-register them so 
extractConstraints
+    // can document their versions without requiring manual entries for every 
transitive platform.
+    // this is required because the micronaut bom format uses gradle modules 
instead of a pom like spring boot
+    it.autoRegisterTransitivePlatforms = true
+    rootProject.subprojects.each { p ->
+        evaluationDependsOn(p.path)
+    }
+    it.projectArtifactIds.set(project.provider {
+        Map<String, String> artifactIdMappings = [:]
+
+        rootProject.subprojects.each { p ->
+            artifactIdMappings[p.name] = p.findProperty('pomArtifactId') ?: 
p.name
+        }
+
+        for (Map.Entry<String, String> dependency : 
project.ext.gradleBuildProjects.entrySet()) {
+            artifactIdMappings[dependency.key] = dependency.key
+        }
+
+        artifactIdMappings
+    })
+    it.forcedGroupPrefixes.set(['org.apache.grails.profiles': 
'grails-profile'])
+    it.projectCoordinateProperties.set(project.provider {
+        Map<String, String> projectCoordinates = [:]
+
+        rootProject.subprojects.each { p ->
+            String artifactId = p.findProperty('pomArtifactId') as String ?: 
p.name
+            String baseVersionName = artifactId.replaceAll('[.]', '-')
+            projectCoordinates["${p.group}:${ artifactId}:${p.version}" as 
String] = baseVersionName

Review Comment:
   Stray space in GString: `"${p.group}:${ artifactId}:${p.version}"` — `${ 
artifactId}` has a leading space inside the braces. Should be `${artifactId}`. 
Same issue on line 166 of `hibernate7-micronaut/build.gradle`.



##########
grails-bom/hibernate5-micronaut/build.gradle:
##########
@@ -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.
+ */
+
+import org.apache.grails.gradle.tasks.bom.ExtractDependenciesTask
+import org.apache.grails.gradle.tasks.bom.ExtractedDependencyConstraint
+import org.apache.grails.gradle.tasks.bom.PropertyNameCalculator
+
+buildscript {
+    apply from: rootProject.layout.projectDirectory.file('dependencies.gradle')
+}
+
+plugins {
+    id 'java-platform'
+    id 'org.apache.grails.buildsrc.publish'
+    id 'org.apache.grails.buildsrc.sbom'
+}
+
+version = projectVersion
+group = 'org.apache.grails'
+
+javaPlatform {
+    allowDependencies()
+}
+
+ext {
+    isReleaseBuild = System.getenv('GRAILS_PUBLISH_RELEASE') == 'true'
+    isPublishedExternal = 
System.getenv().containsKey('NEXUS_PUBLISH_STAGING_PROFILE_ID')
+    // TODO: It should be possible to pull these build names using 
includedBuild, but I haven't found a way to do so
+    gradleBuildProjects = [
+            'grails-gradle-plugins':'org.apache.grails',
+            'grails-gradle-model':'org.apache.grails.gradle',
+            'grails-gradle-common':'org.apache.grails.gradle',
+            'grails-gradle-tasks':'org.apache.grails',
+    ]
+}
+
+// Register the Micronaut platform in combinedPlatforms/combinedVersions so
+// PropertyNameCalculator (used by extractConstraints and pomCustomization) can
+// resolve a property name for the micronaut-platform constraint.
+project.ext.combinedPlatforms = combinedPlatforms + ['micronaut-platform': 
"io.micronaut.platform:micronaut-platform:$micronautPlatformVersion".toString()]
+project.ext.combinedVersions = combinedVersions + 
['micronaut-platform.version': micronautPlatformVersion as String]
+
+// Coordinates we override via customBomDependencies — these must be excluded 
from the
+// inherited platform chain so they don't conflict with our strictly-versioned 
overrides
+// when consumers apply this BOM via enforcedPlatform.
+def overriddenModules = customBomDependencies.values().collect { String coord 
->
+    def parts = coord.split(':')
+    [group: parts[0], module: parts[1]]
+}
+Set<String> overriddenCoords = overriddenModules.collect { 
"${it.group}:${it.module}".toString() } as Set
+
+dependencies {
+    api(platform(project(':grails-base-bom'))) {
+        overriddenModules.each { ovr ->
+            exclude group: ovr.group, module: ovr.module
+        }
+    }
+
+    // Re-export the Micronaut platform so consumers inherit Micronaut's 
managed versions
+    // transitively. Exclude Groovy since we declare the required version 
explicitly via
+    // customBomDependencies. Exclude Spock since we manage that version 
ourselves.
+    
api(platform("io.micronaut.platform:micronaut-platform:$micronautPlatformVersion"))
 {
+        exclude group: 'org.apache.groovy'
+        exclude group: 'org.spockframework'
+    }
+
+    constraints {
+        // Re-declare base BOM constraints directly so enforcedPlatform() 
consumers
+        // get forced versions. Constraints inherited via platform() are not 
enforced
+        // by enforcedPlatform — only direct constraints are.
+        // Skip entries we override below in customBomDependencies to avoid 
conflicting
+        // strictly constraints under enforcedPlatform.
+        gradleBomDependencies.values().each { String coord ->
+            def parts = coord.split(':')
+            String key = parts[0] + ':' + parts[1]
+            if (key in overriddenCoords) {
+                return
+            }
+            api coord
+        }
+        bomDependencies.values().each { String coord ->
+            def parts = coord.split(':')
+            String key = parts[0] + ':' + parts[1]
+            if (key in overriddenCoords) return
+            api coord
+        }
+        for (def entry : bomPlatformDependencies.entrySet()) {
+            api entry.value
+        }
+        // Re-declare the Micronaut platform as a constraint for 
enforcedPlatform support
+        api 
"io.micronaut.platform:micronaut-platform:$micronautPlatformVersion"
+        for (def entry : customBomDependencies.entrySet()) {
+            def parts = entry.value.split(':')
+            if (parts.length == 3) {
+                api("${parts[0]}:${parts[1]}") {
+                    version {
+                        strictly parts[2]
+                    }
+                }
+            } else {
+                api entry.value
+            }
+        }
+    }
+}
+
+configurations.register('bomDependencies').configure {
+    it.canBeResolved = true
+    it.transitive = true
+    it.extendsFrom(configurations.named('api').get())
+}
+
+tasks.register('extractConstraints', ExtractDependenciesTask).configure { 
ExtractDependenciesTask it ->
+    it.captureProjectServices(project.dependencies, project.configurations)
+    it.configuration = configurations.named('bomDependencies')
+    it.configurationName = 'bomDependencies'
+    it.destination = 
project.layout.buildDirectory.file('grails-hibernate5-micronaut-bom-constraints.adoc')
+    it.platformDefinitions = combinedPlatforms
+    it.definitions = combinedDependencies
+    it.projectName = project.name
+    it.versions = combinedVersions
+    // Micronaut's platform imports many sub-BOMs (micronaut-*-bom, netty-bom, 
etc.) that are
+    // not explicitly registered in dependencies.gradle. Auto-register them so 
extractConstraints
+    // can document their versions without requiring manual entries for every 
transitive platform.
+    // this is required because the micronaut bom format uses gradle modules 
instead of a pom like spring boot
+    it.autoRegisterTransitivePlatforms = true
+    rootProject.subprojects.each { p ->
+        evaluationDependsOn(p.path)
+    }
+    it.projectArtifactIds.set(project.provider {
+        Map<String, String> artifactIdMappings = [:]
+
+        rootProject.subprojects.each { p ->
+            artifactIdMappings[p.name] = p.findProperty('pomArtifactId') ?: 
p.name
+        }
+
+        for (Map.Entry<String, String> dependency : 
project.ext.gradleBuildProjects.entrySet()) {
+            artifactIdMappings[dependency.key] = dependency.key
+        }
+
+        artifactIdMappings
+    })
+    it.forcedGroupPrefixes.set(['org.apache.grails.profiles': 
'grails-profile'])
+    it.projectCoordinateProperties.set(project.provider {
+        Map<String, String> projectCoordinates = [:]
+
+        rootProject.subprojects.each { p ->
+            String artifactId = p.findProperty('pomArtifactId') as String ?: 
p.name
+            String baseVersionName = artifactId.replaceAll('[.]', '-')
+            projectCoordinates["${p.group}:${ artifactId}:${p.version}" as 
String] = baseVersionName
+        }
+
+        for (Map.Entry<String, String> dependency : 
project.ext.gradleBuildProjects.entrySet()) {
+            
projectCoordinates["${dependency.value}:${dependency.key}:${project.version}" 
as String] = dependency.key
+        }
+
+        projectCoordinates
+    })
+
+    
it.dependsOn(project.tasks.named('generateMetadataFileForMavenPublication'), 
project.tasks.named('generatePomFileForMavenPublication'))
+}
+
+def validateNoSnapshotDependencies = 
tasks.register('validateNoSnapshotDependencies')
+validateNoSnapshotDependencies.configure { Task it ->
+    it.group = 'publishing'
+    it.description = 'Validates that no snapshot dependencies are present in 
the project when performing a release.'
+
+    it.doLast {
+        configurations.each { config ->
+            config.allDependencies.each { dep ->
+                if (dep.version && dep.version.contains('-SNAPSHOT')) {
+                    throw new GradleException("Releases cannot have a snapshot 
dependency: ${dep.group}:${dep.name} (${dep.version})")
+                }
+            }
+        }
+    }
+}
+
+if (ext.isReleaseBuild && ext.isPublishedExternal) {
+    project.afterEvaluate {
+        tasks.named('generateMetadataFileForMavenPublication').configure {
+            dependsOn(validateNoSnapshotDependencies)
+        }
+        tasks.named('generatePomFileForMavenPublication').configure {
+            dependsOn(validateNoSnapshotDependencies)
+        }
+    }
+}
+
+ext {
+    pomDescription = 'Grails Hibernate 5 Micronaut BOM (Bill of Materials) for 
Grails projects integrating with Micronaut and Hibernate 5. Layers Hibernate 5 
dependency management on top of grails-micronaut-bom; consume as 
enforcedPlatform.'
+    pomCustomization = { xml ->
+        def root = xml.asNode()
+
+        def propertiesNode = root.properties ? root.properties[0] : 
root.appendNode('properties')
+
+        def depMgmt = root.dependencyManagement?.getAt(0)
+        def deps = depMgmt?.dependencies?.getAt(0)
+        if (deps) {
+            PropertyNameCalculator propertyNameCalculator = new 
PropertyNameCalculator(combinedPlatforms, combinedDependencies, 
combinedVersions)
+            
propertyNameCalculator.addForcedGroupPrefix('org.apache.grails.profiles', 
'grails-profile')
+            propertyNameCalculator.addProjects(rootProject.subprojects)
+            for (String gradleArtifactId : project.ext.gradleBuildProjects) {
+                propertyNameCalculator.addProject('org.apache.grails.gradle', 
gradleArtifactId, project.version as String, gradleArtifactId)
+            }
+
+            Map<String, String> pomProperties = [:]
+            deps.dependency.each { dep ->
+                String groupId = dep.groupId.text().trim()
+                String artifactId = dep.artifactId.text().trim()
+                boolean isBom = dep.scope.text().trim() == 'import'
+
+                String inlineVersion = dep.version.text().trim()
+                if (inlineVersion == 'null') {
+                    inlineVersion = null
+                }
+
+                if (inlineVersion) {
+                    ExtractedDependencyConstraint extractedConstraint = 
propertyNameCalculator.calculate(groupId, artifactId, inlineVersion, isBom)
+                    if (extractedConstraint?.versionPropertyReference) {
+                        // use the property reference instead of the hard 
coded version so that it can be
+                        // overriden by the spring boot dependency management 
plugin
+                        dep.version[0].value = 
extractedConstraint.versionPropertyReference
+
+                        // Add an entry in the <properties> node with the 
actual version number
+                        
pomProperties.put(extractedConstraint.versionPropertyName, inlineVersion)
+                    }
+                } else if (!inlineVersion) {
+                    throw new GradleException("Dependency $groupId:$artifactId 
does not have a version.")
+                }
+            }
+
+            for (Map.Entry<String, String> property : 
pomProperties.entrySet()) {
+                propertiesNode.appendNode(property.key, property.value)
+            }
+        }
+    }
+}

Review Comment:
   `hibernate5-micronaut/build.gradle` and `hibernate7-micronaut/build.gradle` 
are 254-line near-identical copies of each other (and closely mirror 
`grails-bom/micronaut/build.gradle`). The only real differences are the 
`pomDescription` string and the BOM project referenced. Consider extracting the 
shared boilerplate into a convention plugin or shared Gradle script to reduce 
3× duplication and make future maintenance easier.



##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextConfiguration.java:
##########
@@ -0,0 +1,421 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.orm.hibernate.cfg;
+
+import java.io.IOException;
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.sql.DataSource;
+
+import jakarta.annotation.Nullable;
+import jakarta.persistence.Embeddable;
+import jakarta.persistence.Entity;
+import jakarta.persistence.MappedSuperclass;
+
+import org.hibernate.HibernateException;
+import org.hibernate.MappingException;
+import org.hibernate.SessionFactory;
+import org.hibernate.boot.registry.BootstrapServiceRegistry;
+import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder;
+import org.hibernate.boot.registry.StandardServiceRegistry;
+import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
+import 
org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
+import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
+import org.hibernate.boot.spi.AdditionalMappingContributor;
+import org.hibernate.cfg.AvailableSettings;
+import org.hibernate.cfg.BytecodeSettings;
+import org.hibernate.cfg.Configuration;
+import org.hibernate.cfg.Environment;
+import org.hibernate.cfg.JdbcSettings;
+import org.hibernate.context.spi.CurrentSessionContext;
+import org.hibernate.internal.util.config.ConfigurationHelper;
+import org.hibernate.service.ServiceRegistry;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternUtils;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.core.type.filter.AnnotationTypeFilter;
+import org.springframework.core.type.filter.TypeFilter;
+import org.springframework.util.ClassUtils;
+
+import org.grails.datastore.gorm.GormEntity;
+import org.grails.datastore.gorm.jdbc.connections.DataSourceSettings;
+import org.grails.datastore.mapping.core.connections.ConnectionSource;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.orm.hibernate.EventListenerIntegrator;
+import org.grails.orm.hibernate.GrailsSessionContext;
+import org.grails.orm.hibernate.HibernateEventListeners;
+import org.grails.orm.hibernate.MetadataIntegrator;
+import org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder;
+import org.grails.orm.hibernate.cfg.domainbinding.util.NamingStrategyProvider;
+import org.grails.orm.hibernate.proxy.GrailsBytecodeProvider;
+
+/**
+ * A Configuration that uses a MappingContext to configure Hibernate
+ *
+ * @since 5.0
+ */
+@SuppressWarnings({"rawtypes", "PMD.UseProperClassLoader", 
"PMD.DataflowAnomalyAnalysis", "PMD.CloseResource"})
+public class HibernateMappingContextConfiguration extends Configuration
+        implements ApplicationContextAware, Serializable {
+
+    @Serial
+    private static final long serialVersionUID = -7115087342689305517L;
+
+    private static final String RESOURCE_PATTERN = "/**/*.class";
+
+    private static final TypeFilter[] ENTITY_TYPE_FILTERS = new TypeFilter[] {
+        new AnnotationTypeFilter(Entity.class, false),
+        new AnnotationTypeFilter(Embeddable.class, false),
+        new AnnotationTypeFilter(MappedSuperclass.class, false)
+    };
+    private static final String FALSE_LITERAL = "false";
+    private final Class<? extends CurrentSessionContext> currentSessionContext 
= GrailsSessionContext.class;
+    //    private MetadataContributor metadataContributor;
+    private final Set<Class> additionalClasses = new HashSet<>();
+    protected String sessionFactoryBeanName = "sessionFactory";
+    protected String dataSourceName = ConnectionSource.DEFAULT;
+    protected transient HibernateMappingContext hibernateMappingContext;
+    private transient HibernateEventListeners hibernateEventListeners;
+    private Map<String, Object> eventListeners;
+    private transient ServiceRegistry serviceRegistry;
+    private transient ResourcePatternResolver resourcePatternResolver = new 
PathMatchingResourcePatternResolver();
+    private transient NamingStrategyProvider namingStrategyProvider = new 
NamingStrategyProvider();
+    protected GrailsBytecodeProvider bytecodeProvider;
+
+    public void setBytecodeProvider(GrailsBytecodeProvider bytecodeProvider) {
+        this.bytecodeProvider = bytecodeProvider;
+    }
+
+    public NamingStrategyProvider getNamingStrategyProvider() {
+        return namingStrategyProvider;
+    }
+
+    public void setNamingStrategyProvider(NamingStrategyProvider 
namingStrategyProvider) {
+        this.namingStrategyProvider = namingStrategyProvider;
+    }
+
+    public MappingCacheHolder getMappingCacheHolder() {
+        return hibernateMappingContext != null ? 
hibernateMappingContext.getMappingCacheHolder() : null;
+    }
+
+    public void setHibernateMappingContext(HibernateMappingContext 
hibernateMappingContext) {
+        this.hibernateMappingContext = hibernateMappingContext;
+    }
+
+    @Override
+    public void setApplicationContext(@Nullable ApplicationContext 
applicationContext) throws BeansException {
+        resourcePatternResolver = 
ResourcePatternUtils.getResourcePatternResolver(applicationContext);
+        String dsName = ConnectionSource.DEFAULT.equals(dataSourceName) ? 
"dataSource" : "dataSource_" + dataSourceName;
+        Properties properties = getProperties();
+
+        if (applicationContext != null) {
+            if 
(!properties.containsKey(JdbcSettings.JAKARTA_NON_JTA_DATASOURCE) && 
applicationContext.containsBean(dsName)) {
+                properties.put(JdbcSettings.JAKARTA_NON_JTA_DATASOURCE, 
applicationContext.getBean(dsName));
+            }
+            properties.put(Environment.CURRENT_SESSION_CONTEXT_CLASS, 
currentSessionContext.getName());
+            properties.put(
+                    "hibernate.enhancer.bytecodeprovider.instance",
+                getGrailsBytecodeProvider());
+            properties.put("hibernate.bytecode.allow_enhancement_as_proxy", 
FALSE_LITERAL);
+            properties.put("hibernate.bytecode.enhancement_metadata_cache", 
FALSE_LITERAL);
+            properties.put("hibernate.enhancer.enableLazyInitialization", 
FALSE_LITERAL);
+            properties.put("hibernate.enhancer.enableDirtyTracking", 
FALSE_LITERAL);
+            properties.put("hibernate.enhancer.enableAssociationManagement", 
FALSE_LITERAL);
+            ClassLoader classLoader = applicationContext.getClassLoader();
+            if (classLoader != null) {
+                properties.put(AvailableSettings.CLASSLOADERS, classLoader);
+            }
+        }
+    }
+
+    protected GrailsBytecodeProvider getGrailsBytecodeProvider() {
+        return bytecodeProvider != null ? bytecodeProvider : new 
GrailsBytecodeProvider();
+    }
+
+    /**
+     * Set the target SQL {@link DataSource}
+     *
+     * @param connectionSource The data source to use
+     */
+    public void setDataSourceConnectionSource(ConnectionSource<DataSource, 
DataSourceSettings> connectionSource) {
+        this.dataSourceName = connectionSource.getName();
+        DataSource source = connectionSource.getSource();
+        getProperties().put(JdbcSettings.JAKARTA_NON_JTA_DATASOURCE, source);
+        getProperties().put(Environment.CURRENT_SESSION_CONTEXT_CLASS, 
GrailsSessionContext.class.getName());
+        setBytecodeProvider(getGrailsBytecodeProvider());
+        final ClassLoader contextClassLoader = 
Thread.currentThread().getContextClassLoader();
+        if (contextClassLoader != null &&
+                
contextClassLoader.getClass().getSimpleName().equalsIgnoreCase("RestartClassLoader"))
 {
+            getProperties().put(AvailableSettings.CLASSLOADERS, 
contextClassLoader);
+        } else {
+            getProperties()
+                    .put(
+                            AvailableSettings.CLASSLOADERS,
+                            connectionSource.getClass().getClassLoader());
+        }
+    }
+
+    /**
+     * Add the given annotated classes in a batch.
+     *
+     * @return Configuration
+     * @see #addAnnotatedClass
+     * @see #scanPackages
+     */
+    @Override
+    public Configuration addAnnotatedClasses(Class... annotatedClasses) {
+        for (Class<?> annotatedClass : annotatedClasses) {
+            addAnnotatedClass(annotatedClass);
+        }
+        return this;
+    }
+
+    @Override
+    public Configuration addAnnotatedClass(Class annotatedClass) {
+        additionalClasses.add(annotatedClass);
+        return super.addAnnotatedClass(annotatedClass);
+    }
+
+    @Override
+    public HibernateMappingContextConfiguration addPackages(String... 
annotatedPackages) {
+        for (String annotatedPackage : annotatedPackages) {
+            addPackage(annotatedPackage);
+        }
+        return this;
+    }
+
+    /**
+     * Perform Spring-based scanning for entity classes, registering them as 
annotated classes with
+     * this {@code Configuration}.
+     *
+     * @param packagesToScan one or more Java package names
+     * @throws HibernateException if scanning fails for any reason
+     */
+    public void scanPackages(String... packagesToScan) throws 
HibernateException {
+        try {
+            MetadataReaderFactory readerFactory = new 
CachingMetadataReaderFactory(resourcePatternResolver);
+            for (String pkg : packagesToScan) {
+                String pattern = 
ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
+                        ClassUtils.convertClassNameToResourcePath(pkg) +
+                        RESOURCE_PATTERN;
+                Resource[] resources = 
resourcePatternResolver.getResources(pattern);
+                for (Resource resource : resources) {
+                    if (resource.isReadable()) {
+                        MetadataReader reader = 
readerFactory.getMetadataReader(resource);
+                        String className = 
reader.getClassMetadata().getClassName();
+                        if (matchesFilter(reader, readerFactory)) {
+                            ClassLoader classLoader = 
resourcePatternResolver.getClassLoader();
+                            Class<?> loadedClass = classLoader != null ?
+                                    classLoader.loadClass(className) :
+                                    ClassUtils.forName(className, null);
+                            addAnnotatedClasses(loadedClass);
+                        }
+                    }
+                }
+            }
+        } catch (IOException ex) {
+            throw new MappingException("Failed to scan classpath for unlisted 
classes", ex);
+        } catch (ClassNotFoundException ex) {
+            throw new MappingException("Failed to load annotated classes from 
classpath", ex);
+        }
+    }
+
+    /**
+     * Check whether any of the configured entity type filters matches the 
current class descriptor
+     * contained in the metadata reader.
+     */
+    protected boolean matchesFilter(MetadataReader reader, 
MetadataReaderFactory readerFactory) throws IOException {
+        for (TypeFilter filter : ENTITY_TYPE_FILTERS) {
+            if (filter.match(reader, readerFactory)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void setSessionFactoryBeanName(String name) {
+        sessionFactoryBeanName = name;
+    }
+
+    public void setDataSourceName(String name) {
+        dataSourceName = name;
+    }
+
+    /* (non-Javadoc)
+     * @see org.hibernate.cfg.Configuration#buildSessionFactory()
+     */
+    @Override
+    public SessionFactory buildSessionFactory() throws HibernateException {
+        // 1. FORCE the custom bytecode provider instance right before 
bootstrap
+        // This bypasses the ServiceLoader and ensures your 
GrailsBytecodeProvider is used.
+        GrailsBytecodeProvider bytecodeProvider = getGrailsBytecodeProvider();
+        getProperties()
+                .put(
+                        BytecodeSettings.BYTECODE_PROVIDER_INSTANCE,
+                        bytecodeProvider);
+
+        // set the class loader to load Groovy classes
+
+        // work around for HHH-2624
+        SessionFactory sessionFactory;
+
+        Object classLoaderObject = 
getProperties().get(AvailableSettings.CLASSLOADERS);
+        ClassLoader appClassLoader;
+
+        if (classLoaderObject instanceof ClassLoader) {
+            appClassLoader = (ClassLoader) classLoaderObject;
+        } else {
+            appClassLoader = getClass().getClassLoader();
+        }
+
+        ConfigurationHelper.resolvePlaceHolders(getProperties());
+
+        final GrailsDomainBinder domainBinder = new GrailsDomainBinder(
+                dataSourceName,
+                sessionFactoryBeanName,
+                hibernateMappingContext,
+                namingStrategyProvider,
+                hibernateMappingContext.getMappingCacheHolder());
+
+        List<Class> annotatedClasses = new ArrayList<>();
+        for (PersistentEntity persistentEntity : 
hibernateMappingContext.getPersistentEntities()) {
+            Class<?> javaClass = persistentEntity.getJavaClass();
+            if (javaClass.isAnnotationPresent(Entity.class)) {
+                annotatedClasses.add(javaClass);
+            }
+        }
+
+        if (!additionalClasses.isEmpty()) {
+            for (Class additionalClass : additionalClasses) {
+                if (GormEntity.class.isAssignableFrom(additionalClass)) {
+                    
hibernateMappingContext.addPersistentEntity(additionalClass);
+                }
+            }
+        }
+
+        addAnnotatedClasses(annotatedClasses.toArray(new Class[0]));
+
+        ClassLoaderService classLoaderService = new 
ClassLoaderServiceImpl(appClassLoader) {
+            @Override
+            public <S> Collection<S> loadJavaServices(Class<S> 
serviceContract) {
+                // Ensure Grails contributes mappings for GORM entities even 
if they lack JPA @Entity
+                if 
(AdditionalMappingContributor.class.isAssignableFrom(serviceContract)) {
+                    // Include the GrailsDomainBinder first, then any other 
contributors
+                    // discovered by the parent classloader (e.g., Envers 
AdditionalMappingContributorImpl).
+                    // Without this, Envers' AdditionalMappingContributor 
would be excluded,
+                    // preventing EnversService from being initialized before 
EnversIntegrator runs.
+                    Collection<S> parentContributors = 
super.loadJavaServices(serviceContract);
+                    @SuppressWarnings("unchecked")
+                    S grailsBinder = (S) domainBinder;
+                    List<S> allContributors = new 
ArrayList<>(parentContributors.size() + 1);
+                    allContributors.add(grailsBinder);
+                    allContributors.addAll(parentContributors);
+                    return allContributors;

Review Comment:
   Good fix. Previously `Collections.singletonList(domainBinder)` silently 
excluded all other `AdditionalMappingContributor` implementations (including 
Envers' `AdditionalMappingContributorImpl`), causing `EnversService` to be 
uninitialized before `EnversIntegrator` ran. Prepending the Grails binder 
before the parent contributors is the correct ordering.



##########
grails-doc/src/en/guide/upgrading/upgrading80x.adoc:
##########
@@ -267,6 +267,36 @@ This affects any code path that goes through Micronaut's 
HTTP client, filters, o
 
 NOTE: The Grails Forge generator enforces this requirement: selecting any 
Micronaut feature with a JDK version below 25 will fail with 
`IllegalArgumentException` at generation time.
 
+===== 7.4 Hibernate-Specific Micronaut BOMs

Review Comment:
   Section is numbered `7.4` — please verify that sections `7.1`–`7.3` exist 
earlier in the document, otherwise the numbering skips. If this is the only or 
first sub-section of section 7, it should be numbered `7.1`.



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:
##########
@@ -482,10 +488,12 @@ ${importStatements}
         }
 
         throw new GradleException(
-                "Project '${project.name}' uses Micronaut but does not apply 
grails-micronaut-bom as an enforcedPlatform. " +
+                "Project '${project.name}' uses Micronaut but does not apply a 
Micronaut BOM as an enforcedPlatform. " +

Review Comment:
   The error message uses single-quoted strings, so `$grailsVersion` is printed 
literally. That's intentional as example code, but it may confuse users who try 
to copy-paste it. Consider using `<grailsVersion>` as the placeholder instead, 
which is unambiguous as a fill-in token.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to