This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch feat/gradle-managed-version-overrides
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit ca495ab5287aeba5d2300a997a6c24145e866bc4
Author: James Fredley <[email protected]>
AuthorDate: Thu Feb 26 11:25:11 2026 -0500

    feat: replace Spring Dependency Management plugin with Gradle platform and 
lightweight BOM property overrides
    
    Replace the Spring Dependency Management Gradle plugin with Gradle's
    native platform() support plus a lightweight BomManagedVersions utility
    that preserves the ability to override BOM-managed dependency versions
    via project properties (ext[] or gradle.properties).
    
    This allows Grails to standardize on Gradle platforms - the modern
    dependency management solution - while retaining the one feature
    Gradle platforms lack: property-based version overrides from BOMs.
    
    Changes:
    - Add BomManagedVersions: parses BOM POM XML to extract property-to-
      artifact mappings, applies version overrides via eachDependency()
    - Update GrailsGradlePlugin to use platform() + BomManagedVersions
      instead of Spring DM plugin
    - Deprecate GrailsExtension.springDependencyManagement flag
    - Remove Spring DM plugin from plugins/build.gradle dependency
    - Remove Spring DM plugin from example projects
    - Update documentation to reflect Gradle platform approach
    - Add unit tests (BomManagedVersionsSpec) and functional test
      (BomPlatformFunctionalSpec)
    
    Note: build-logic/docs-core/ExtractDependenciesTask still uses Spring
    DM's shaded Maven model classes and should be addressed in a follow-up.
    
    Assisted-by: Claude Code <[email protected]>
---
 grails-bom/build.gradle                            |   2 +-
 .../examples/spring-boot-app/build.gradle          |   1 -
 .../gradleBuild/gradleDependencies.adoc            |  26 +-
 grails-gradle/plugins/build.gradle                 |   1 -
 .../gradle/plugin/core/BomManagedVersions.groovy   | 354 +++++++++++++++++++++
 .../gradle/plugin/core/GrailsExtension.groovy      |   7 +-
 .../gradle/plugin/core/GrailsGradlePlugin.groovy   |  55 ++--
 .../plugin/core/BomManagedVersionsSpec.groovy      |  97 ++++++
 .../plugin/core/BomPlatformFunctionalSpec.groovy   |  45 +++
 .../src/test/resources/test-poms/test-bom.pom      |  51 +++
 .../test-projects/bom-platform-basic/build.gradle  |  16 +
 .../bom-platform-basic/gradle.properties           |   1 +
 .../grails-app/conf/application.yml                |   2 +
 .../bom-platform-basic/settings.gradle             |   1 +
 .../templates/grailsCentralPublishing.gradle       |   2 +-
 .../gsp-spring-boot/app/build.gradle               |   1 -
 16 files changed, 624 insertions(+), 38 deletions(-)

diff --git a/grails-bom/build.gradle b/grails-bom/build.gradle
index 260239fced..cd38ef236e 100644
--- a/grails-bom/build.gradle
+++ b/grails-bom/build.gradle
@@ -210,7 +210,7 @@ ext {
                     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
+                        // overridden by project properties (gradle.properties 
or ext['property.name'])
                         dep.version[0].value = 
extractedConstraint.versionPropertyReference
 
                         // Add an entry in the <properties> node with the 
actual version number
diff --git a/grails-data-graphql/examples/spring-boot-app/build.gradle 
b/grails-data-graphql/examples/spring-boot-app/build.gradle
index 6c113e1be8..0c64d390e2 100644
--- a/grails-data-graphql/examples/spring-boot-app/build.gradle
+++ b/grails-data-graphql/examples/spring-boot-app/build.gradle
@@ -30,7 +30,6 @@ buildscript {
 apply plugin: 'groovy'
 apply plugin: 'idea'
 apply plugin: 'org.springframework.boot'
-apply plugin: 'io.spring.dependency-management'
 
 dependencies {
        implementation platform(project(':grails-bom'))
diff --git 
a/grails-doc/src/en/guide/commandLine/gradleBuild/gradleDependencies.adoc 
b/grails-doc/src/en/guide/commandLine/gradleBuild/gradleDependencies.adoc
index 31a1ffe6b3..348689f4a0 100644
--- a/grails-doc/src/en/guide/commandLine/gradleBuild/gradleDependencies.adoc
+++ b/grails-doc/src/en/guide/commandLine/gradleBuild/gradleDependencies.adoc
@@ -60,13 +60,22 @@ dependencies {
 
 Note that version numbers are not present in the majority of the dependencies.
 
-This is thanks to the Spring dependency management plugin which automatically 
configures `grails-bom` as a Maven BOM via the Grails Gradle Plugin.  This 
defines the default dependency versions for most commonly used dependencies and 
plugins.
+This is thanks to Gradle's platform support which automatically imports 
`grails-bom` as a managed dependency platform via the Grails Gradle Plugin.  
This defines the default dependency versions for most commonly used 
dependencies and plugins.
+
+To override a managed version, set the corresponding property in 
`gradle.properties` or `build.gradle`:
+[source,groovy]
+----
+// gradle.properties
+slf4j.version=1.7.36
+
+// or build.gradle
+ext['slf4j.version'] = '1.7.36'
+----
 
 For a Grails App, applying `org.apache.grails.gradle.grails-web` will 
automatically configure the `grails-bom`.  No other steps required.
 
-For Plugins and Projects which do not use 
`org.apache.grails.gradle.grails-web`, you can apply the `grails-bom` in one of 
the following two ways.
+For Plugins and Projects which do not use 
`org.apache.grails.gradle.grails-web`, you can apply the `grails-bom` using 
Gradle Platforms:
 
-build.gradle, using Gradle Platforms:
 [source,groovy]
 ----
 dependencies {
@@ -74,14 +83,3 @@ dependencies {
     //...
 }
 ----
-
-build.gradle, using Spring dependency management plugin:
-[source,groovy]
-----
-dependencyManagement {
-    imports {
-        mavenBom 'org.apache.grails:grails-bom:{GrailsVersion}'
-    }
-    applyMavenExclusions false
-}
-----
diff --git a/grails-gradle/plugins/build.gradle 
b/grails-gradle/plugins/build.gradle
index cedf995b7a..88dff223b8 100644
--- a/grails-gradle/plugins/build.gradle
+++ b/grails-gradle/plugins/build.gradle
@@ -55,7 +55,6 @@ dependencies {
     implementation "${gradleBomDependencies['grails-publish-plugin']}"
     implementation 'org.springframework.boot:spring-boot-gradle-plugin'
     implementation 'org.springframework.boot:spring-boot-loader-tools'
-    implementation 'io.spring.gradle:dependency-management-plugin'
 
     // Testing - Gradle TestKit is auto-added by java-gradle-plugin
     testImplementation('org.spockframework:spock-core') { transitive = false }
diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/BomManagedVersions.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/BomManagedVersions.groovy
new file mode 100644
index 0000000000..b1f708478c
--- /dev/null
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/BomManagedVersions.groovy
@@ -0,0 +1,354 @@
+/*
+ *  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.gradle.plugin.core
+
+import groovy.transform.CompileStatic
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.DependencyResolveDetails
+import org.gradle.api.logging.Logger
+import org.gradle.api.logging.Logging
+import org.w3c.dom.Document
+import org.w3c.dom.Element
+import org.w3c.dom.NodeList
+
+import javax.xml.parsers.DocumentBuilderFactory
+
+/**
+ * Lightweight replacement for the Spring Dependency Management plugin's
+ * version property override feature.
+ *
+ * <p>Parses BOM POM files to build a mapping of Maven property names
+ * (e.g., {@code slf4j.version}) to the artifacts they control. At
+ * dependency resolution time, checks whether the user has overridden
+ * any of these properties via {@code ext['property.name']} in
+ * {@code build.gradle} or via {@code gradle.properties}, and applies
+ * those overrides using Gradle's {@code 
ResolutionStrategy.eachDependency()}.</p>
+ *
+ * <p>Gradle's native {@code platform()} mechanism handles the base
+ * BOM import and default version management. This class only adds the
+ * one feature Gradle lacks: property-based version customization
+ * (see <a href="https://github.com/gradle/gradle/issues/9160";>Gradle 
#9160</a>).</p>
+ *
+ * @since 8.0
+ */
+@CompileStatic
+class BomManagedVersions {
+
+    private static final Logger LOG = Logging.getLogger(BomManagedVersions)
+
+    private final Map<String, String> versionOverrides = new LinkedHashMap<>()
+
+    /**
+     * Resolves a BOM, parses its POM chain, and determines which managed
+     * dependency versions need to be overridden based on project properties.
+     *
+     * @param project the Gradle project (used for artifact resolution and 
property lookup)
+     * @param bomCoordinates the BOM coordinates in {@code 
group:artifact:version} format
+     * @return a BomManagedVersions instance containing any version overrides 
to apply
+     */
+    static BomManagedVersions resolve(Project project, String bomCoordinates) {
+        BomManagedVersions instance = new BomManagedVersions()
+
+        String[] parts = bomCoordinates.split(':')
+        if (parts.length != 3) {
+            LOG.warn('Invalid BOM coordinates: {}', bomCoordinates)
+            return instance
+        }
+
+        Map<String, String> bomProperties = new LinkedHashMap<>()
+        Map<String, List<String>> propertyToArtifacts = new LinkedHashMap<>()
+
+        processBom(project, parts[0], parts[1], parts[2], bomProperties, 
propertyToArtifacts, new HashSet<String>())
+
+        for (Map.Entry<String, List<String>> entry : 
propertyToArtifacts.entrySet()) {
+            String propertyName = entry.key
+            if (project.hasProperty(propertyName)) {
+                String overrideVersion = 
project.property(propertyName).toString()
+                String defaultVersion = bomProperties.get(propertyName)
+
+                if (overrideVersion != defaultVersion) {
+                    for (String artifactKey : entry.value) {
+                        instance.versionOverrides.put(artifactKey, 
overrideVersion)
+                    }
+                    LOG.lifecycle(
+                        'Grails BOM version override: {} = {} (BOM default: 
{})',
+                        propertyName, overrideVersion, defaultVersion ?: 
'unknown'
+                    )
+                }
+            }
+        }
+
+        if (!instance.versionOverrides.isEmpty()) {
+            LOG.info('Grails BOM: {} version override(s) will be applied', 
instance.versionOverrides.size())
+        }
+
+        return instance
+    }
+
+    /**
+     * Applies version overrides to a Gradle configuration's resolution 
strategy.
+     *
+     * @param configuration the configuration to apply overrides to
+     */
+    void applyTo(Configuration configuration) {
+        if (versionOverrides.isEmpty()) {
+            return
+        }
+
+        Map<String, String> overrides = this.versionOverrides
+        configuration.resolutionStrategy.eachDependency { 
DependencyResolveDetails details ->
+            String key = 
"${details.requested.group}:${details.requested.name}" as String
+            String override = overrides.get(key)
+            if (override != null) {
+                details.useVersion(override)
+                details.because('Grails BOM version override via project 
property')
+            }
+        }
+    }
+
+    /**
+     * Returns whether any version overrides were detected.
+     */
+    boolean hasOverrides() {
+        return !versionOverrides.isEmpty()
+    }
+
+    /**
+     * Returns an unmodifiable view of the version overrides.
+     * Keys are {@code group:artifact}, values are the override version 
strings.
+     */
+    Map<String, String> getOverrides() {
+        return Collections.unmodifiableMap(versionOverrides)
+    }
+
+    /**
+     * Parses a BOM POM file and extracts the property-to-artifact mapping.
+     * This method does not follow imported BOMs recursively - it only 
processes
+     * the given file. Intended for testing and direct POM inspection.
+     *
+     * @param pomFile the BOM POM file to parse
+     * @param bomProperties output map to receive property name to default 
value mappings
+     * @param propertyToArtifacts output map to receive property name to 
artifact coordinate mappings
+     */
+    static void parseBomFile(File pomFile, Map<String, String> bomProperties, 
Map<String, List<String>> propertyToArtifacts) {
+        Document doc = parseXml(pomFile)
+        if (doc == null) {
+            return
+        }
+        extractProperties(doc, bomProperties)
+
+        NodeList depMgmtNodes = 
doc.getElementsByTagName('dependencyManagement')
+        if (depMgmtNodes.length == 0) {
+            return
+        }
+        Element depMgmt = (Element) depMgmtNodes.item(0)
+        NodeList dependenciesNodes = 
depMgmt.getElementsByTagName('dependencies')
+        if (dependenciesNodes.length == 0) {
+            return
+        }
+        Element dependenciesElement = (Element) dependenciesNodes.item(0)
+        NodeList depNodes = 
dependenciesElement.getElementsByTagName('dependency')
+
+        for (int i = 0; i < depNodes.length; i++) {
+            Element dep = (Element) depNodes.item(i)
+            String depGroupId = getChildText(dep, 'groupId')
+            String depArtifactId = getChildText(dep, 'artifactId')
+            String depVersion = getChildText(dep, 'version')
+
+            if (!depGroupId || !depArtifactId || !depVersion) {
+                continue
+            }
+
+            if (depVersion.contains('${')) {
+                String propertyName = extractPropertyName(depVersion)
+                if (propertyName) {
+                    String artifactKey = "${depGroupId}:${depArtifactId}" as 
String
+                    propertyToArtifacts.computeIfAbsent(propertyName) { new 
ArrayList<String>() }.add(artifactKey)
+                }
+            }
+        }
+    }
+
+    private static void processBom(
+        Project project, String group, String artifact, String version,
+        Map<String, String> bomProperties,
+        Map<String, List<String>> propertyToArtifacts,
+        Set<String> processed
+    ) {
+        String bomKey = "${group}:${artifact}:${version}" as String
+        if (!processed.add(bomKey)) {
+            return
+        }
+
+        File pomFile = resolvePomFile(project, group, artifact, version)
+        if (pomFile == null) {
+            return
+        }
+
+        Document doc = parseXml(pomFile)
+        if (doc == null) {
+            return
+        }
+
+        extractProperties(doc, bomProperties)
+        processManagedDependencies(doc, project, bomProperties, 
propertyToArtifacts, processed)
+    }
+
+    private static File resolvePomFile(Project project, String group, String 
artifact, String version) {
+        try {
+            Configuration detached = 
project.configurations.detachedConfiguration(
+                
project.dependencies.create("${group}:${artifact}:${version}@pom" as String)
+            )
+            detached.transitive = false
+            return detached.singleFile
+        }
+        catch (Exception e) {
+            LOG.info('Could not resolve BOM POM: {}:{}:{} - {}', group, 
artifact, version, e.message)
+            return null
+        }
+    }
+
+    private static Document parseXml(File pomFile) {
+        try {
+            DocumentBuilderFactory factory = 
DocumentBuilderFactory.newInstance()
+            factory.setNamespaceAware(false)
+            factory.setValidating(false)
+            
factory.setFeature('http://apache.org/xml/features/nonvalidating/load-external-dtd',
 false)
+            
factory.setFeature('http://xml.org/sax/features/external-general-entities', 
false)
+            
factory.setFeature('http://xml.org/sax/features/external-parameter-entities', 
false)
+            return factory.newDocumentBuilder().parse(pomFile)
+        }
+        catch (Exception e) {
+            LOG.warn('Failed to parse BOM POM: {} - {}', pomFile.name, 
e.message)
+            return null
+        }
+    }
+
+    private static void extractProperties(Document doc, Map<String, String> 
bomProperties) {
+        NodeList propertiesNodes = doc.getElementsByTagName('properties')
+        if (propertiesNodes.length == 0) {
+            return
+        }
+
+        Element propertiesElement = (Element) propertiesNodes.item(0)
+        NodeList children = propertiesElement.childNodes
+        for (int i = 0; i < children.length; i++) {
+            if (children.item(i) instanceof Element) {
+                Element prop = (Element) children.item(i)
+                String name = prop.tagName
+                String value = prop.textContent?.trim()
+                if (name && value) {
+                    bomProperties.put(name, value)
+                }
+            }
+        }
+    }
+
+    private static void processManagedDependencies(
+        Document doc, Project project,
+        Map<String, String> bomProperties,
+        Map<String, List<String>> propertyToArtifacts,
+        Set<String> processed
+    ) {
+        NodeList depMgmtNodes = 
doc.getElementsByTagName('dependencyManagement')
+        if (depMgmtNodes.length == 0) {
+            return
+        }
+
+        Element depMgmt = (Element) depMgmtNodes.item(0)
+        NodeList dependenciesNodes = 
depMgmt.getElementsByTagName('dependencies')
+        if (dependenciesNodes.length == 0) {
+            return
+        }
+
+        Element dependenciesElement = (Element) dependenciesNodes.item(0)
+        NodeList depNodes = 
dependenciesElement.getElementsByTagName('dependency')
+
+        for (int i = 0; i < depNodes.length; i++) {
+            Element dep = (Element) depNodes.item(i)
+            String depGroupId = getChildText(dep, 'groupId')
+            String depArtifactId = getChildText(dep, 'artifactId')
+            String depVersion = getChildText(dep, 'version')
+            String depScope = getChildText(dep, 'scope')
+
+            if (!depGroupId || !depArtifactId) {
+                continue
+            }
+
+            if ('import' == depScope) {
+                String resolvedVersion = interpolateProperties(depVersion, 
bomProperties)
+                if (resolvedVersion) {
+                    processBom(project, depGroupId, depArtifactId, 
resolvedVersion,
+                        bomProperties, propertyToArtifacts, processed)
+                }
+                continue
+            }
+
+            if (depVersion && depVersion.contains('${')) {
+                String propertyName = extractPropertyName(depVersion)
+                if (propertyName) {
+                    String artifactKey = "${depGroupId}:${depArtifactId}" as 
String
+                    propertyToArtifacts.computeIfAbsent(propertyName) { new 
ArrayList<String>() }.add(artifactKey)
+                }
+            }
+        }
+    }
+
+    private static String extractPropertyName(String versionStr) {
+        if (versionStr == null) {
+            return null
+        }
+        int start = versionStr.indexOf('${')
+        int end = versionStr.indexOf('}', start)
+        if (start >= 0 && end > start) {
+            return versionStr.substring(start + 2, end)
+        }
+        return null
+    }
+
+    private static String interpolateProperties(String value, Map<String, 
String> properties) {
+        if (value == null || !value.contains('${')) {
+            return value
+        }
+
+        String result = value
+        int maxIterations = 10
+        while (result.contains('${') && maxIterations-- > 0) {
+            String propertyName = extractPropertyName(result)
+            if (propertyName == null) {
+                break
+            }
+            String resolved = properties.get(propertyName)
+            if (resolved == null) {
+                break
+            }
+            result = result.replace("\${${propertyName}}" as String, resolved)
+        }
+        return result
+    }
+
+    private static String getChildText(Element parent, String childTagName) {
+        NodeList children = parent.getElementsByTagName(childTagName)
+        if (children.length == 0) {
+            return null
+        }
+        return children.item(0).textContent?.trim()
+    }
+}
diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy
index c216885913..82ce70830c 100644
--- 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy
@@ -83,8 +83,13 @@ class GrailsExtension {
     List<String> starImports = []
 
     /**
-     * Whether the spring dependency management plugin should be applied by 
default
+     * @deprecated The Spring Dependency Management plugin has been replaced 
with Gradle's native
+     * {@code platform()} support plus lightweight property-based version 
overrides.
+     * This property is no longer used. Set version overrides in {@code 
gradle.properties}
+     * or via {@code ext['property.name']} instead.
+     * @see BomManagedVersions
      */
+    @Deprecated
     boolean springDependencyManagement = true
 
     /**
diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
index bdfcc62c31..72ac323722 100644
--- 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
@@ -26,8 +26,6 @@ import grails.util.GrailsNameUtils
 import grails.util.Metadata
 import groovy.transform.CompileDynamic
 import groovy.transform.CompileStatic
-import io.spring.gradle.dependencymanagement.DependencyManagementPlugin
-import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
 import org.apache.grails.gradle.common.PropertyFileUtils
 import org.apache.tools.ant.filters.EscapeUnicode
 import org.apache.tools.ant.filters.ReplaceTokens
@@ -355,17 +353,45 @@ ${importStatements}
     protected void applyDefaultPlugins(Project project) {
         applySpringBootPlugin(project)
 
-        project.afterEvaluate {
-            GrailsExtension ge = project.extensions.getByType(GrailsExtension)
-            if (ge.springDependencyManagement) {
-                Plugin dependencyManagementPlugin = 
project.plugins.findPlugin(DependencyManagementPlugin)
-                if (dependencyManagementPlugin == null) {
-                    project.plugins.apply(DependencyManagementPlugin)
-                }
+        applyGrailsBom(project)
+    }
 
-                DependencyManagementExtension dme = 
project.extensions.findByType(DependencyManagementExtension)
+    /**
+     * Applies the Grails BOM as a Gradle platform and configures 
property-based
+     * version overrides. This replaces the Spring Dependency Management 
plugin with
+     * a lightweight mechanism that:
+     * <ol>
+     *   <li>Imports {@code grails-bom} via Gradle's native {@code platform()} 
support</li>
+     *   <li>Parses the BOM POM chain to discover which Maven properties 
control which artifact versions</li>
+     *   <li>Checks project properties ({@code gradle.properties} or {@code 
ext['property.name']}) for overrides</li>
+     *   <li>Applies any overrides via {@code 
ResolutionStrategy.eachDependency()}</li>
+     * </ol>
+     *
+     * <p>Usage: to override a version managed by the Grails or Spring Boot 
BOM, set the
+     * corresponding property in {@code gradle.properties} or {@code 
build.gradle}:</p>
+     * <pre>
+     * // gradle.properties
+     * slf4j.version=1.7.36
+     *
+     * // or build.gradle
+     * ext['slf4j.version'] = '1.7.36'
+     * </pre>
+     *
+     * @see BomManagedVersions
+     * @since 8.0
+     */
+    protected void applyGrailsBom(Project project) {
+        String grailsVersion = (project.findProperty('grailsVersion') ?: 
BuildSettings.grailsVersion) as String
+        String bomCoordinates = 
"org.apache.grails:grails-bom:${grailsVersion}" as String
+
+        project.dependencies.add('implementation', 
project.dependencies.platform(bomCoordinates))
 
-                applyBomImport(dme, project)
+        project.afterEvaluate {
+            BomManagedVersions managedVersions = 
BomManagedVersions.resolve(project, bomCoordinates)
+            if (managedVersions.hasOverrides()) {
+                project.configurations.configureEach { Configuration conf ->
+                    managedVersions.applyTo(conf)
+                }
             }
         }
     }
@@ -377,13 +403,6 @@ ${importStatements}
         }
     }
 
-    @CompileDynamic
-    private void applyBomImport(DependencyManagementExtension dme, project) {
-        dme.imports({
-            
mavenBom("org.apache.grails:grails-bom:${project.properties['grailsVersion']}")
-        })
-    }
-
     protected String getDefaultProfile() {
         'web'
     }
diff --git 
a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomManagedVersionsSpec.groovy
 
b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomManagedVersionsSpec.groovy
new file mode 100644
index 0000000000..6953c1ba3e
--- /dev/null
+++ 
b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomManagedVersionsSpec.groovy
@@ -0,0 +1,97 @@
+/*
+ *  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.gradle.plugin.core
+
+import spock.lang.Specification
+
+/**
+ * Tests for the Grails BOM platform integration that replaced the
+ * Spring Dependency Management plugin.
+ *
+ * <p>Verifies that {@link BomManagedVersions} correctly parses BOM POM
+ * files, extracts property-to-artifact mappings, and that the Grails
+ * Gradle plugin applies {@code grails-bom} as a Gradle
+ * {@code platform()} dependency.</p>
+ *
+ * @since 8.0
+ * @see BomManagedVersions
+ * @see GrailsGradlePlugin#applyGrailsBom
+ */
+class BomManagedVersionsSpec extends Specification {
+
+    def "parseBomFile extracts properties from BOM POM"() {
+        given:
+        File pomFile = new 
File(getClass().getClassLoader().getResource('test-poms/test-bom.pom').toURI())
+        Map<String, String> bomProperties = [:]
+        Map<String, List<String>> propertyToArtifacts = [:]
+
+        when:
+        BomManagedVersions.parseBomFile(pomFile, bomProperties, 
propertyToArtifacts)
+
+        then:
+        bomProperties['jackson.version'] == '2.15.0'
+        bomProperties['slf4j.version'] == '2.0.9'
+        bomProperties['groovy.version'] == '4.0.30'
+    }
+
+    def "parseBomFile maps property references to artifact coordinates"() {
+        given:
+        File pomFile = new 
File(getClass().getClassLoader().getResource('test-poms/test-bom.pom').toURI())
+        Map<String, String> bomProperties = [:]
+        Map<String, List<String>> propertyToArtifacts = [:]
+
+        when:
+        BomManagedVersions.parseBomFile(pomFile, bomProperties, 
propertyToArtifacts)
+
+        then: "jackson.version maps to all three jackson artifacts"
+        propertyToArtifacts['jackson.version'].containsAll([
+            'com.fasterxml.jackson.core:jackson-databind',
+            'com.fasterxml.jackson.core:jackson-core',
+            'com.fasterxml.jackson.core:jackson-annotations'
+        ])
+
+        and: "slf4j.version maps to slf4j-api"
+        propertyToArtifacts['slf4j.version'] == ['org.slf4j:slf4j-api']
+
+        and: "groovy.version maps to groovy"
+        propertyToArtifacts['groovy.version'] == ['org.apache.groovy:groovy']
+    }
+
+    def "parseBomFile ignores dependencies with hardcoded versions"() {
+        given:
+        File pomFile = new 
File(getClass().getClassLoader().getResource('test-poms/test-bom.pom').toURI())
+        Map<String, String> bomProperties = [:]
+        Map<String, List<String>> propertyToArtifacts = [:]
+
+        when:
+        BomManagedVersions.parseBomFile(pomFile, bomProperties, 
propertyToArtifacts)
+
+        then: "hardcoded-version artifact is not in any property mapping"
+        
!propertyToArtifacts.values().flatten().contains('org.example:hardcoded-version')
+    }
+
+    def "BomManagedVersions with no overrides reports hasOverrides false"() {
+        given:
+        def instance = new BomManagedVersions()
+
+        expect:
+        !instance.hasOverrides()
+        instance.overrides.isEmpty()
+    }
+}
diff --git 
a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy
 
b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy
new file mode 100644
index 0000000000..4e1a5ba3d6
--- /dev/null
+++ 
b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy
@@ -0,0 +1,45 @@
+/*
+ *  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.gradle.plugin.core
+
+/**
+ * Functional tests for the Gradle platform-based BOM integration.
+ *
+ * <p>Uses Gradle TestKit to verify that the Grails Gradle plugin correctly
+ * applies {@code grails-bom} as a Gradle {@code platform()} dependency
+ * and no longer depends on the Spring Dependency Management plugin.</p>
+ *
+ * @since 8.0
+ * @see BomManagedVersions
+ * @see GrailsGradlePlugin#applyGrailsBom
+ */
+class BomPlatformFunctionalSpec extends GradleSpecification {
+
+    def "plugin applies grails-bom as Gradle platform and does not apply 
Spring DM plugin"() {
+        given:
+        setupTestResourceProject('bom-platform-basic')
+
+        when:
+        def result = executeTask('inspectBomSetup')
+
+        then:
+        result.output.contains('HAS_PLATFORM_BOM=true')
+        result.output.contains('HAS_SPRING_DM=false')
+    }
+}
diff --git a/grails-gradle/plugins/src/test/resources/test-poms/test-bom.pom 
b/grails-gradle/plugins/src/test/resources/test-poms/test-bom.pom
new file mode 100644
index 0000000000..cda1a266ad
--- /dev/null
+++ b/grails-gradle/plugins/src/test/resources/test-poms/test-bom.pom
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.test</groupId>
+    <artifactId>test-bom</artifactId>
+    <version>1.0.0</version>
+    <packaging>pom</packaging>
+
+    <properties>
+        <jackson.version>2.15.0</jackson.version>
+        <slf4j.version>2.0.9</slf4j.version>
+        <groovy.version>4.0.30</groovy.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>com.fasterxml.jackson.core</groupId>
+                <artifactId>jackson-databind</artifactId>
+                <version>${jackson.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.fasterxml.jackson.core</groupId>
+                <artifactId>jackson-core</artifactId>
+                <version>${jackson.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.fasterxml.jackson.core</groupId>
+                <artifactId>jackson-annotations</artifactId>
+                <version>${jackson.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.slf4j</groupId>
+                <artifactId>slf4j-api</artifactId>
+                <version>${slf4j.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.groovy</groupId>
+                <artifactId>groovy</artifactId>
+                <version>${groovy.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.example</groupId>
+                <artifactId>hardcoded-version</artifactId>
+                <version>1.0.0</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+</project>
diff --git 
a/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/build.gradle
 
b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/build.gradle
new file mode 100644
index 0000000000..9b58b70062
--- /dev/null
+++ 
b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/build.gradle
@@ -0,0 +1,16 @@
+plugins {
+    id 'org.apache.grails.gradle.grails-app'
+}
+
+tasks.register('inspectBomSetup') {
+    doLast {
+        def implDeps = configurations.implementation.allDependencies
+        def hasPlatform = implDeps.any { dep ->
+            dep.group == 'org.apache.grails' && dep.name == 'grails-bom'
+        }
+        println "HAS_PLATFORM_BOM=${hasPlatform}"
+
+        def hasSpringDm = 
project.plugins.findPlugin('io.spring.dependency-management') != null
+        println "HAS_SPRING_DM=${hasSpringDm}"
+    }
+}
diff --git 
a/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/gradle.properties
 
b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/gradle.properties
new file mode 100644
index 0000000000..35c332fb87
--- /dev/null
+++ 
b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/gradle.properties
@@ -0,0 +1 @@
+grailsVersion=__PROJECT_VERSION__
diff --git 
a/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/grails-app/conf/application.yml
 
b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/grails-app/conf/application.yml
new file mode 100644
index 0000000000..4706b4393f
--- /dev/null
+++ 
b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/grails-app/conf/application.yml
@@ -0,0 +1,2 @@
+grails:
+    profile: web
diff --git 
a/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/settings.gradle
 
b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/settings.gradle
new file mode 100644
index 0000000000..b2a1c27a42
--- /dev/null
+++ 
b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'test-bom-platform'
diff --git a/grails-profiles/plugin/templates/grailsCentralPublishing.gradle 
b/grails-profiles/plugin/templates/grailsCentralPublishing.gradle
index e9f5c8cafe..e7cbc1abf4 100644
--- a/grails-profiles/plugin/templates/grailsCentralPublishing.gradle
+++ b/grails-profiles/plugin/templates/grailsCentralPublishing.gradle
@@ -7,7 +7,7 @@ publishing {
 
                 // simply remove dependencies without a version
                 // version-less dependencies are handled with 
dependencyManagement
-                // see 
https://github.com/spring-gradle-plugins/dependency-management-plugin/issues/8 
for more complete solutions
+                // remove version-less dependencies since versions are managed 
by the Grails BOM platform
                 pomNode.dependencies.dependency.findAll {
                     it.version.text().isEmpty()
                 }.each {
diff --git a/grails-test-examples/gsp-spring-boot/app/build.gradle 
b/grails-test-examples/gsp-spring-boot/app/build.gradle
index c3187fecfa..81338eab00 100644
--- a/grails-test-examples/gsp-spring-boot/app/build.gradle
+++ b/grails-test-examples/gsp-spring-boot/app/build.gradle
@@ -21,7 +21,6 @@ plugins {
     id 'java'
     id 'war'
     id 'org.springframework.boot'
-    id 'io.spring.dependency-management'
     id "groovy"
 }
 


Reply via email to