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

jdaugherty pushed a commit to branch grails-geb
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit da2d80f0be360eaf64c50c2c07e1df50fc84b479
Author: James Daugherty <[email protected]>
AuthorDate: Sun Apr 20 01:15:23 2025 -0400

    Convert ExtractDependencies into a defined class
---
 dependencies.gradle                                |   6 +
 grails-bom/build.gradle                            | 222 +-----------------
 grails-gradle/docs-core/build.gradle               |   1 +
 .../gradle/tasks/bom/CoordinateHolder.groovy       |  17 ++
 .../tasks/bom/CoordinateVersionHolder.groovy       |  25 ++
 .../tasks/bom/ExtractDependenciesTask.groovy       | 259 +++++++++++++++++++++
 .../tasks/bom/ExtractedDependencyConstraint.groovy |  25 ++
 .../gradle/tasks/bom/PropertyNameCalculator.groovy |  71 ++++++
 8 files changed, 415 insertions(+), 211 deletions(-)

diff --git a/dependencies.gradle b/dependencies.gradle
index 79f8a5b3e2..f7007d77f4 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -119,4 +119,10 @@ ext {
             'rxjava2'                        : 
"io.reactivex.rxjava2:rxjava:${bomDependencyVersions['rxjava2.version']}",
             'rxjava3'                        : 
"io.reactivex.rxjava3:rxjava:${bomDependencyVersions['rxjava3.version']}",
     ]
+
+    // Because pom exclusions aren't properly supported by gradle, we can't 
inherit the grails-gradle-bom
+    // this requires copying the platforms selectively to the grails-bom.  Due 
this via these combinations
+    combinedPlatforms = ['spring-boot-bom': 
gradleBomPlatformDependencies['spring-boot-bom']] + bomPlatformDependencies
+    combinedDependencies = gradleBomDependencies + bomDependencies
+    combinedVersions = gradleBomDependencyVersions + bomDependencyVersions
 }
diff --git a/grails-bom/build.gradle b/grails-bom/build.gradle
index 6ff1bdbc5b..9f3523d25a 100644
--- a/grails-bom/build.gradle
+++ b/grails-bom/build.gradle
@@ -1,4 +1,5 @@
-import 
io.spring.gradle.dependencymanagement.org.apache.maven.model.io.xpp3.MavenXpp3Reader
+import org.apache.grails.gradle.tasks.bom.ExtractDependenciesTask
+import org.apache.grails.gradle.tasks.bom.PropertyNameCalculator
 
 // Copyright 2024 the original author or authors.
 buildscript {
@@ -63,217 +64,15 @@ configurations.register('bomDependencies').configure {
     it.extendsFrom(configurations.named('api').get())
 }
 
-String resolveMavenProperty(String errorDescription, String dynamicVersion, 
Map properties, int maxIterations = 10) {
-    def dynamicPattern = ~/\$\{([^}]+)\}/
-    String expandedVersion = dynamicVersion
-
-    int iterations = 0
-    while ((expandedVersion =~ dynamicPattern).find() && iterations < 
maxIterations) {
-        expandedVersion = expandedVersion.replaceAll(dynamicPattern) { String 
fullMatch, String propName ->
-            String replacement = properties[propName] as String
-            return replacement ? replacement : fullMatch
-        }
-        iterations++
-    }
-
-    if ((expandedVersion =~ dynamicPattern).find()) {
-        logger.warn("Reached max iterations for ${errorDescription} while 
resolving properties in: ${dynamicVersion}")
-    }
-
-    expandedVersion
-}
-
-
-Closure determinePossibleKey = { Map.Entry<String, String> found, Map versions 
->
-    String possibleKey = found.key.toString()
-    while (possibleKey != null) {
-        String propertyName = "${possibleKey}.version" as String
-        if (versions.containsKey(propertyName)) {
-            return propertyName
-        }
-
-        int lastIndex = possibleKey.lastIndexOf('-')
-        possibleKey = lastIndex > 0 ? possibleKey.substring(0, lastIndex) : 
null
-    }
-}
-
-Closure determinePropertyName = { String groupId, String artifactId, String 
version, boolean isBom ->
-    Map<String, String> toSearch = isBom ? bomPlatformDependencies : 
bomDependencies
-    Map<String, String> toSearchGradle = isBom ? gradleBomPlatformDependencies 
: gradleBomDependencies
-
-    Map.Entry<String, String> found = toSearch.entrySet().find { 
"$groupId:$artifactId:$version" as String == it.value as String }
-    if (!found) {
-        found = toSearchGradle.entrySet().find { 
"$groupId:$artifactId:$version" as String == it.value as String }
-        if (!found) {
-            return null
-        }
-    }
-
-    Map<String, String> versions = bomDependencyVersions
-
-    String propertyName = determinePossibleKey(found, versions)
-    if (!propertyName) {
-        propertyName = determinePossibleKey(found, gradleBomDependencyVersions)
-    }
-
-    if (propertyName) {
-        return propertyName
-    }
-
-    throw new GradleException("Could not determine artifact property key for 
$groupId:$artifactId:$version")
-}
-
-//TODO: This really should be a gradle task that we export so others can use.
-tasks.register('extractConstraints').configure { Task it ->
-    it.inputs.files(configurations.named('bomDependencies'))
-    
it.outputs.file(project.layout.buildDirectory.file('grails-bom-constraints.adoc'))
+tasks.register('extractConstraints', ExtractDependenciesTask).configure { 
ExtractDependenciesTask it ->
+    it.configuration =configurations.named('bomDependencies')
+    it.configurationName = 'bomDependencies'
+    it.destination = 
project.layout.buildDirectory.file('grails-bom-constraints.adoc')
+    it.platformDefinitions = combinedPlatforms
+    it.definitions = combinedDependencies
+    it.versions = combinedVersions
 
     
it.dependsOn(project.tasks.named('generateMetadataFileForMavenPublication'), 
project.tasks.named('generatePomFileForMavenPublication'))
-    it.doLast {
-        def conf = configurations.detachedConfiguration()
-        conf.transitive = true
-
-        Map<String, Tuple5<String, String, String, String, String>> 
bomConstraints = [:]
-        Configuration apiConfiguration = 
configurations.named('bomDependencies').get()
-
-        // Explicit constraints
-        apiConfiguration.getAllDependencyConstraints().all { constraint ->
-
-            def groupId = constraint.module.group as String
-            def artifactId = constraint.module.name as String
-            def artifactVersion = constraint.version as String
-            def propertyName = determinePropertyName(groupId, artifactId, 
artifactVersion, false)
-            bomConstraints.put("${module.group}:${module.name}" as String,
-                    new Tuple5<>(
-                            groupId,
-                            artifactId,
-                            artifactVersion,
-                            propertyName ? "\${${propertyName}}" : '' as 
String,
-                            'grails-bom'
-                    )
-            )
-        }
-
-        // Build an exclusion list so parsing the pom we can exclude any 
dependencies as they are matched
-        Map<String, List<Tuple2<String, String>>> allExclusions = 
[:].withDefault { [] }
-        apiConfiguration.allDependencies.each { dep ->
-            if (dep instanceof org.gradle.api.artifacts.ModuleDependency) {
-                String coordinates = "${dep.group}:${dep.name}" as String
-
-                dep.excludeRules.each { exclusion ->
-                    Tuple2<String, String> exclude = new 
Tuple2<>(exclusion.group, exclusion.module)
-                    allExclusions.get(coordinates).add(exclude)
-                }
-            }
-        }
-
-        // Find inherited libraries via platform() usage
-        apiConfiguration.incoming.resolutionResult.allDependencies.each { 
ResolvedDependencyResult dep ->
-            // Any non-constraint via api dependency should *always* be a 
platform dependency, so expand each of those
-            String bomGroup = dep.requested.group
-            String bomName = dep.requested.module
-            String bomVersion = dep.requested.version
-
-            String bomCoordinates = "${bomGroup}:${bomName}" as String
-            List<Tuple2<String, String>> excluded = 
allExclusions.get(bomCoordinates)
-
-            // fetch the BOM as a pom file so it can be expanded
-            def propertyName = determinePropertyName(bomGroup, bomName, 
bomVersion, true)
-            bomConstraints.put(bomCoordinates, new Tuple5<>(bomGroup, bomName, 
bomVersion, propertyName ? "\${${propertyName}}" : '' as String, bomName))
-            populatePlatformDependencies(bomGroup, bomName, bomVersion, 
bomConstraints, excluded)
-        }
-
-        List lines = []
-        bomConstraints.values().sort { a, b -> a.v1 <=> b.v1 ?: a.v2 <=> b.v2 
}.withIndex().each {
-            int position = it.v2 + 1
-            String groupId = it.v1.v1
-            String artifactId = it.v1.v2
-            String artifactVersion = it.v1.v3
-            String propertyName = it.v1.v4
-            String bomName = it.v1.v5
-            lines << "| ${position} | ${groupId} | ${artifactId} | 
${artifactVersion} | ${propertyName ? propertyName == '${project.version}' ? '' 
: propertyName : ''} | ${bomName} "
-        }
-
-        
project.layout.buildDirectory.file('grails-bom-constraints.adoc').get().asFile.withWriter
 { writer ->
-            writer.writeLine '[cols="1,1,1,1,1,1", options="header"]'
-            writer.writeLine '|==='
-            writer.writeLine '| Index | Group | Artifact | Version | Property 
Name | Source'
-            lines.each { line ->
-                writer.writeLine(line)
-            }
-            writer.writeLine '|==='
-        }
-    }
-}
-
-private Properties populatePlatformDependencies(String bomGroup, String 
bomName, String bomVersion,
-                                                Map<String, Tuple5<String, 
String, String, String, String>> bomConstraints,
-                                                List<Tuple2<String, String>> 
excluded, boolean error = true, int level = 0) {
-    def bomDependency = 
dependencies.create("${bomGroup}:${bomName}:${bomVersion}@pom")
-    def bomConf = configurations.detachedConfiguration(bomDependency)
-    File bomPomFile = bomConf.singleFile
-
-    def reader = new MavenXpp3Reader()
-    def model = reader.read(new FileReader(bomPomFile))
-
-    Properties versionProperties = new Properties()
-    if (model.parent) {
-        // Need to populate the parent bom if it's present first
-        populatePlatformDependencies(model.parent.groupId, 
model.parent.artifactId, model.parent.version, bomConstraints, excluded, false, 
level + 1)?.entrySet()?.each {
-            versionProperties.put(it.key, it.value)
-        }
-    }
-    model.properties.entrySet().each {
-        versionProperties.put(it.key, it.value)
-    }
-    versionProperties.put('project.groupId', bomGroup)
-    versionProperties.put('project.version', bomVersion)
-
-    if (model.dependencyManagement && model.dependencyManagement.dependencies) 
{
-        model.dependencyManagement.dependencies.each { depItem ->
-            String baseCoordinates = 
"${depItem.groupId}:${depItem.artifactId}" as String
-            String artifactId = resolveMavenProperty(baseCoordinates, 
depItem.artifactId, versionProperties)
-            String groupId = resolveMavenProperty(baseCoordinates, 
depItem.groupId, versionProperties)
-
-            String resolvedCoordinates = "${groupId}:${artifactId}" as String
-            if (!bomConstraints.containsKey(resolvedCoordinates)) {
-                boolean isExcluded = excluded.any {
-                    if (it.v1 && it.v2) {
-                        return resolvedCoordinates == "${it.v1}:${it.v2}" as 
String
-                    }
-
-                    if (it.v1 && !it.v2) {
-                        return depItem.groupId == it.v1
-                    }
-
-                    if (!it.v1 && it.v2) {
-                        return depItem.artifactId == it.v2
-                    }
-
-                    false
-                }
-                if (!isExcluded) {
-
-                    String resolvedVersion = 
resolveMavenProperty(resolvedCoordinates, depItem.version, versionProperties)
-                    String propertyName = depItem.version.contains('$') ? 
depItem.version : null
-                    if (depItem.scope == 'import') {
-                        bomConstraints.put(resolvedCoordinates, new 
Tuple5<>(groupId, artifactId, resolvedVersion, propertyName, bomName))
-                        populatePlatformDependencies(groupId, artifactId, 
resolvedVersion, bomConstraints, excluded, error, level + 1)
-                    } else {
-                        bomConstraints.put(resolvedCoordinates, new 
Tuple5<>(groupId, artifactId, resolvedVersion, propertyName, bomName))
-                    }
-                }
-            }
-        }
-    } else {
-        if (error) {
-            // only the boms we directly include need to error since we expect 
a dependency management;
-            // parent boms are sometimes use to share properties so we need to 
not error on these cases
-            throw new GradleException("BOM 
${bomGroup}:${bomName}:${bomVersion} has no dependencyManagement section.")
-        }
-    }
-
-    return versionProperties
 }
 
 tasks.register('validateNoSnapshotDependencies').configure { Task it ->
@@ -318,8 +117,9 @@ ext {
                     inlineVersion = null
                 }
 
+                PropertyNameCalculator propertyNameCalculator = new 
PropertyNameCalculator(combinedPlatforms, combinedDependencies, 
combinedVersions)
                 if (inlineVersion) {
-                    String propertyName = determinePropertyName(groupId, 
artifactId, inlineVersion, isBom)
+                    String propertyName = 
propertyNameCalculator.calculate(groupId, artifactId, inlineVersion, isBom)
                     if (propertyName) {
                         // Replace the version in the pom with a property 
reference
                         String propertyReference = "\${${propertyName}}"
diff --git a/grails-gradle/docs-core/build.gradle 
b/grails-gradle/docs-core/build.gradle
index 7c436ea5ac..ecc4d27e89 100644
--- a/grails-gradle/docs-core/build.gradle
+++ b/grails-gradle/docs-core/build.gradle
@@ -30,6 +30,7 @@ dependencies {
 
     api 'org.asciidoctor:asciidoctorj'
     implementation 'org.xhtmlrenderer:flying-saucer-pdf-openpdf'
+    implementation 'org.springframework.boot:spring-boot-gradle-plugin'
 
     runtimeOnly 'org.slf4j:slf4j-api'
 
diff --git 
a/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/CoordinateHolder.groovy
 
b/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/CoordinateHolder.groovy
new file mode 100644
index 0000000000..b6299a14f2
--- /dev/null
+++ 
b/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/CoordinateHolder.groovy
@@ -0,0 +1,17 @@
+package org.apache.grails.gradle.tasks.bom
+
+import groovy.transform.CompileStatic
+import groovy.transform.EqualsAndHashCode
+import groovy.transform.ToString
+
+@EqualsAndHashCode(includes = ['groupId', 'artifactId'])
+@CompileStatic
+@ToString
+class CoordinateHolder {
+    String groupId
+    String artifactId
+
+    String getCoordinatesWithoutVersion() {
+        "${groupId}:${artifactId}" as String
+    }
+}
\ No newline at end of file
diff --git 
a/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/CoordinateVersionHolder.groovy
 
b/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/CoordinateVersionHolder.groovy
new file mode 100644
index 0000000000..8cc7d1d922
--- /dev/null
+++ 
b/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/CoordinateVersionHolder.groovy
@@ -0,0 +1,25 @@
+package org.apache.grails.gradle.tasks.bom
+
+import groovy.transform.CompileStatic
+import groovy.transform.EqualsAndHashCode
+import groovy.transform.ToString
+import org.gradle.api.GradleException
+
+@EqualsAndHashCode(includes = ['version'], callSuper = true)
+@CompileStatic
+@ToString
+class CoordinateVersionHolder extends CoordinateHolder {
+    String version
+
+    CoordinateHolder toCoordinateHolder() {
+        new CoordinateHolder(groupId: groupId, artifactId: artifactId)
+    }
+
+    String getCoordinates() {
+        if (!version) {
+            throw new GradleException("Constraint does not have a version: 
${this}")
+        }
+
+        "${groupId}:${artifactId}:${version}" as String
+    }
+}
\ No newline at end of file
diff --git 
a/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy
 
b/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy
new file mode 100644
index 0000000000..a44d9c2614
--- /dev/null
+++ 
b/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy
@@ -0,0 +1,259 @@
+package org.apache.grails.gradle.tasks.bom
+
+import io.spring.gradle.dependencymanagement.org.apache.maven.model.Model
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.NamedDomainObjectProvider
+import org.gradle.api.artifacts.*
+import org.gradle.api.artifacts.component.ModuleComponentSelector
+import org.gradle.api.artifacts.result.DependencyResult
+import org.gradle.api.artifacts.result.ResolvedDependencyResult
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.MapProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.*
+import 
io.spring.gradle.dependencymanagement.org.apache.maven.model.io.xpp3.MavenXpp3Reader
+
+/**
+ * Grails Bom files define their dependencies in a series of maps, this task 
takes those maps and generates an
+ * asciidoc file containing all of the resolve dependencies and their versions 
in the bom.
+ */
+abstract class ExtractDependenciesTask extends DefaultTask {
+    @InputFiles
+    @Classpath
+    abstract ConfigurableFileCollection getDependencyArtifacts()
+
+    @OutputFile
+    abstract RegularFileProperty getDestination()
+
+    @Input
+    abstract MapProperty<String,String> getVersions()
+
+    @Input
+    abstract Property<String> getConfigurationName()
+
+    @Input
+    abstract MapProperty<String, String> getPlatformDefinitions()
+
+    @Input
+    abstract MapProperty<String, String> getDefinitions()
+
+    void setConfiguration(NamedDomainObjectProvider<Configuration> config) {
+        dependencyArtifacts.from(config)
+        configurationName.set(config.name)
+    }
+
+    ExtractDependenciesTask() {
+        doFirst {
+            if(!project.pluginManager.hasPlugin('java-platform')) {
+                throw new GradleException("The 'java-platform' plugin must be 
applied to the project to use this task.")
+            }
+        }
+    }
+
+    @TaskAction
+    void generate() {
+        File outputFile = destination.get().asFile
+        outputFile.parentFile.mkdirs()
+
+        Map<CoordinateHolder, ExtractedDependencyConstraint> constraints = [:]
+        PropertyNameCalculator propertyNameCalculator = new 
PropertyNameCalculator(
+                getPlatformDefinitions().get(),
+                getDefinitions().get(),
+                getVersions().get()
+        )
+
+        Configuration configuration = 
project.configurations.named(configurationName.get()).get()
+        if(!configuration.canBeResolved) {
+            throw new GradleException("The configuration ${configuration.name} 
must be resolvable to use this task.")
+        }
+
+        populateExplicitConstraints(configuration, constraints, 
propertyNameCalculator)
+
+        Map<CoordinateHolder, List<CoordinateHolder>> exclusions = 
determineExclusions(configuration)
+        populateInheritedConstraints(configuration, exclusions, constraints, 
propertyNameCalculator)
+
+        List<String> lines = generateAsciiDoc(constraints)
+        destination.get().asFile.withWriter { writer ->
+            writer.writeLine '[cols="1,1,1,1,1,1", options="header"]'
+            writer.writeLine '|==='
+            writer.writeLine '| Index | Group | Artifact | Version | Property 
Name | Source'
+            lines.each { line ->
+                writer.writeLine(line)
+            }
+            writer.writeLine '|==='
+        }
+    }
+
+    private List<String> generateAsciiDoc(Map<CoordinateHolder, 
ExtractedDependencyConstraint> constraints) {
+        List lines = []
+        constraints.values().sort { ExtractedDependencyConstraint a, 
ExtractedDependencyConstraint b -> a.groupId <=> b.groupId ?: a.artifactId <=> 
b.artifactId }.withIndex().each {
+            int position = it.v2 + 1
+            lines << "| ${position} | ${it.v1.groupId} | ${it.v1.artifactId} | 
${it.v1.version} | ${it.v1.versionProperty ?: ''} | ${it.v1.source} "
+        }
+        lines
+    }
+
+    private populateExplicitConstraints(Configuration configuration,
+                                        Map<CoordinateHolder, 
ExtractedDependencyConstraint> constraints,
+                                        PropertyNameCalculator 
propertyNameCalculator) {
+        configuration.getAllDependencyConstraints().all { DependencyConstraint 
constraint ->
+            String groupId = constraint.module.group as String
+            String artifactId = constraint.module.name as String
+            String artifactVersion = constraint.version as String
+
+            ExtractedDependencyConstraint extractConstraint = 
propertyNameCalculator.calculate(groupId, artifactId, artifactVersion, false) 
?: new ExtractedDependencyConstraint(groupId: groupId, artifactId: artifactId, 
version: artifactVersion)
+            extractConstraint.source = project.name
+            constraints.put(new CoordinateHolder(groupId: 
extractConstraint.groupId, artifactId: extractConstraint.artifactId), 
extractConstraint)
+        }
+    }
+
+    private Map<CoordinateHolder, List<CoordinateHolder>> 
determineExclusions(Configuration configuration) {
+        Map<CoordinateHolder, List<CoordinateHolder>> exclusions = 
[:].withDefault { [] }
+        for (Dependency dep  : configuration.allDependencies) {
+            if (dep instanceof ModuleDependency) {
+                CoordinateHolder foundCoordinate = new 
CoordinateHolder(groupId: dep.group, artifactId: dep.name)
+                dep.excludeRules.each { ExcludeRule exclusionRule ->
+                    CoordinateHolder exclusion = new CoordinateHolder(groupId: 
exclusionRule.group, artifactId: exclusionRule.module)
+                    exclusions.get(foundCoordinate).add(exclusion)
+                }
+            }
+        }
+        exclusions
+    }
+
+    private void populateInheritedConstraints(Configuration configuration, 
Map<CoordinateHolder, List<CoordinateHolder>> exclusions, Map<CoordinateHolder, 
ExtractedDependencyConstraint> constraints, PropertyNameCalculator 
propertyNameCalculator) {
+        for (DependencyResult result  : 
configuration.incoming.resolutionResult.allDependencies) {
+            if(!(result instanceof ResolvedDependencyResult)) {
+                throw new GradleException("Dependencies should be resolved 
prior to running this task.")
+            }
+
+            ResolvedDependencyResult dep = (ResolvedDependencyResult) result
+            ModuleComponentSelector moduleComponentSelector = dep.requested as 
ModuleComponentSelector
+
+            // Any non-constraint via api dependency should *always* be a 
platform dependency, so expand each of those
+            CoordinateVersionHolder bomCoordinate = new 
CoordinateVersionHolder(
+                    groupId: moduleComponentSelector.group,
+                    artifactId: moduleComponentSelector.module,
+                    version: moduleComponentSelector.version
+            )
+
+            // fetch the BOM as a pom file so it can be expanded
+            ExtractedDependencyConstraint constraint = 
propertyNameCalculator.calculate(bomCoordinate.groupId, 
bomCoordinate.artifactId, bomCoordinate.version, true)
+            constraint.source = bomCoordinate.artifactId
+            constraints.put(bomCoordinate.toCoordinateHolder(), constraint)
+
+            List<CoordinateHolder> exclusionRules = 
exclusions.get(bomCoordinate.toCoordinateHolder())
+            populatePlatformDependencies(bomCoordinate, exclusionRules, 
constraints)
+        }
+    }
+
+    Properties populatePlatformDependencies(CoordinateVersionHolder 
bomCoordinates, List<CoordinateHolder> exclusionRules, Map<CoordinateHolder, 
ExtractedDependencyConstraint> constraints, boolean error = true, int level = 
0) {
+        Dependency bomDependency = 
project.dependencies.create("${bomCoordinates.coordinates}@pom")
+        Configuration dependencyConfiguration = 
project.configurations.detachedConfiguration(bomDependency)
+        File bomPomFile = dependencyConfiguration.singleFile
+
+        MavenXpp3Reader reader = new MavenXpp3Reader()
+        Model model = reader.read(new FileReader(bomPomFile))
+
+        Properties versionProperties = new Properties()
+        if(model.parent) {
+            // Need to populate the parent bom if it's present first
+            CoordinateVersionHolder parentBom = new CoordinateVersionHolder(
+                    groupId: model.parent.groupId,
+                    artifactId: model.parent.artifactId,
+                    version: model.parent.version
+            )
+            populatePlatformDependencies(parentBom, exclusionRules, 
constraints,false, level + 1)?.entrySet()?.each { Map.Entry<Object, Object> 
entry ->
+                versionProperties.put(entry.key, entry.value)
+            }
+        }
+        model.properties.entrySet().each { Map.Entry<Object, Object> entry ->
+            versionProperties.put(entry.key, entry.value)
+        }
+        versionProperties.put('project.groupId', bomCoordinates.groupId)
+        versionProperties.put('project.version', bomCoordinates.version)
+
+        if (model.dependencyManagement && 
model.dependencyManagement.dependencies) {
+            for 
(io.spring.gradle.dependencymanagement.org.apache.maven.model.Dependency 
depItem : model.dependencyManagement.dependencies) {
+                CoordinateHolder baseCoordinates = new CoordinateHolder(
+                        groupId: depItem.groupId,
+                        artifactId: depItem.artifactId
+                )
+
+                CoordinateHolder resolvedCoordinates = new CoordinateHolder(
+                        groupId: 
resolveMavenProperty(baseCoordinates.coordinatesWithoutVersion, 
depItem.groupId, versionProperties),
+                        artifactId: 
resolveMavenProperty(baseCoordinates.coordinatesWithoutVersion, 
depItem.artifactId, versionProperties)
+                )
+
+                if (!constraints.containsKey(resolvedCoordinates)) {
+                    boolean isExcluded = exclusionRules.any { CoordinateHolder 
excludedCoordinate ->
+                        if (excludedCoordinate.groupId && 
excludedCoordinate.artifactId) {
+                            return resolvedCoordinates == excludedCoordinate
+                        }
+
+                        if (excludedCoordinate.groupId && 
!excludedCoordinate.artifactId) {
+                            return depItem.groupId == 
excludedCoordinate.groupId
+                        }
+
+                        if (!excludedCoordinate.groupId && 
excludedCoordinate.artifactId) {
+                            return depItem.artifactId == 
excludedCoordinate.artifactId
+                        }
+
+                        false
+                    }
+
+                    if (!isExcluded) {
+                        String resolvedVersion = 
resolveMavenProperty(resolvedCoordinates.coordinatesWithoutVersion, 
depItem.version, versionProperties)
+                        String propertyName = depItem.version.contains('$') ? 
depItem.version : null
+                        ExtractedDependencyConstraint constraint = new 
ExtractedDependencyConstraint(
+                                groupId: resolvedCoordinates.groupId, 
artifactId: resolvedCoordinates.artifactId,
+                                version: resolvedVersion, versionProperty: 
propertyName, source: bomCoordinates.artifactId
+                        )
+                        if (depItem.scope == 'import') {
+                            constraints.put(resolvedCoordinates, constraint)
+
+                            CoordinateVersionHolder resolvedBomCoordinates = 
new CoordinateVersionHolder(
+                                    groupId: resolvedCoordinates.groupId,
+                                    artifactId: resolvedCoordinates.artifactId,
+                                    version: resolvedVersion
+                            )
+                            
populatePlatformDependencies(resolvedBomCoordinates, exclusionRules, 
constraints, error, level + 1)
+                        } else {
+                            constraints.put(resolvedCoordinates,constraint)
+                        }
+                    }
+                }
+            }
+        } else {
+            if (error) {
+                // only the boms we directly include need to error since we 
expect a dependency management;
+                // parent boms are sometimes use to share properties so we 
need to not error on these cases
+                throw new GradleException("BOM ${bomCoordinates.coordinates} 
has no dependencyManagement section.")
+            }
+        }
+
+        versionProperties
+    }
+
+    private String resolveMavenProperty(String errorDescription, String 
dynamicVersion, Map properties, int maxIterations = 10) {
+        def dynamicPattern = ~/\$\{([^}]+)\}/
+        String expandedVersion = dynamicVersion
+
+        int iterations = 0
+        while ((expandedVersion =~ dynamicPattern).find() && iterations < 
maxIterations) {
+            expandedVersion = expandedVersion.replaceAll(dynamicPattern) { 
String fullMatch, String propName ->
+                String replacement = properties[propName] as String
+                return replacement ? replacement : fullMatch
+            }
+            iterations++
+        }
+
+        if ((expandedVersion =~ dynamicPattern).find()) {
+            project.logger.warn("Reached max iterations for 
${errorDescription} while resolving properties in: ${dynamicVersion}")
+        }
+
+        expandedVersion
+    }
+}
diff --git 
a/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractedDependencyConstraint.groovy
 
b/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractedDependencyConstraint.groovy
new file mode 100644
index 0000000000..672a211974
--- /dev/null
+++ 
b/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractedDependencyConstraint.groovy
@@ -0,0 +1,25 @@
+package org.apache.grails.gradle.tasks.bom
+
+import groovy.transform.CompileStatic
+import groovy.transform.MapConstructor
+import groovy.transform.ToString
+
+@CompileStatic
+@MapConstructor(includes = ['groupId', 'artifactId', 'version', 
'versionProperty', 'source'], includeSuperProperties = true)
+@ToString(includes = ['groupId', 'artifactId', 'version', 'versionProperty', 
'source'], includeSuperProperties = true)
+class ExtractedDependencyConstraint extends CoordinateVersionHolder {
+    String versionProperty
+    String source
+
+    ExtractedDependencyConstraint(String coordinates) {
+        coordinates.split(':').with { String[] parts ->
+            groupId = parts[0]
+            artifactId = parts[1]
+            version = parts[2]
+        }
+    }
+
+    String getVersionProperty() {
+        versionProperty == '${project.version}' ? '' : versionProperty
+    }
+}
\ No newline at end of file
diff --git 
a/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/PropertyNameCalculator.groovy
 
b/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/PropertyNameCalculator.groovy
new file mode 100644
index 0000000000..56808ff902
--- /dev/null
+++ 
b/grails-gradle/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/PropertyNameCalculator.groovy
@@ -0,0 +1,71 @@
+package org.apache.grails.gradle.tasks.bom
+
+import org.gradle.api.GradleException
+
+/**
+ * Decides on the property name for a dependency
+ */
+class PropertyNameCalculator {
+    final Map<String, String> keysToPlatformCoordinates = [:]
+    final Map<String, ExtractedDependencyConstraint> platformDefinitions = [:]
+
+    final Map<String, String> keysToCoordinates = [:]
+    final Map<String, ExtractedDependencyConstraint> definitions = [:]
+    final Map<String, String> versions = [:]
+
+    PropertyNameCalculator(Map<String, String> platformDefinitions, 
Map<String, String> definitions, Map<String, String> definitionVersions) {
+        this.platformDefinitions.putAll(populate(platformDefinitions, 
keysToPlatformCoordinates))
+        this.definitions.putAll(populate(definitions, keysToCoordinates))
+        this.versions.putAll(definitionVersions)
+    }
+
+    private static Map<String, ExtractedDependencyConstraint> 
populate(Map<String, String> definitions, Map<String, String> keyMappings) {
+        definitions.collectEntries { Map.Entry<String, String> entry ->
+            ExtractedDependencyConstraint constraint = new 
ExtractedDependencyConstraint(entry.value)
+            if (!constraint.version) {
+                throw new GradleException("Version is required for dependency: 
${entry.value}")
+            }
+
+            keyMappings.put(constraint.coordinates, entry.key)
+
+            [constraint.coordinates, constraint]
+        }
+    }
+
+    ExtractedDependencyConstraint calculate(String groupId, String artifactId, 
String version, boolean isPlatform) {
+        Map<String, ExtractedDependencyConstraint> toSearch = isPlatform ? 
platformDefinitions : definitions as Map<String, ExtractedDependencyConstraint>
+        Map<String, String> coordinateMapping = isPlatform ? 
keysToPlatformCoordinates : keysToCoordinates
+
+        ExtractedDependencyConstraint found = 
toSearch.get("$groupId:$artifactId:$version" as String)
+        if (!found) {
+            return null
+        }
+
+        if(found.versionProperty) {
+            return found
+        }
+
+        String propertyName = determinePossibleKey(found, coordinateMapping)
+        if (propertyName) {
+            found.versionProperty = propertyName ? "\${${propertyName}}" : '' 
as String
+            return found
+        }
+
+        throw new GradleException("Could not determine artifact property key 
for ${found.coordinates}")
+    }
+
+    String determinePossibleKey(ExtractedDependencyConstraint found, 
Map<String, String> keyMappings) {
+        String possibleKey = keyMappings[found.coordinates]
+        while (possibleKey) {
+            String propertyName = "${possibleKey}.version" as String
+            if (versions.containsKey(propertyName)) {
+                return propertyName
+            }
+
+            int lastIndex = possibleKey.lastIndexOf('-')
+            possibleKey = lastIndex > 0 ? possibleKey.substring(0, lastIndex) 
: null
+        }
+
+        null
+    }
+}
\ No newline at end of file


Reply via email to