matrei commented on code in PR #15467: URL: https://github.com/apache/grails-core/pull/15467#discussion_r3339299236
########## grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/bom/BomManagedVersions.groovy: ########## @@ -0,0 +1,503 @@ +/* + * 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.bom + +import java.util.function.Function + +import groovy.transform.CompileStatic +import org.gradle.api.Project +import org.gradle.api.artifacts.ConfigurationContainer +import org.gradle.api.artifacts.DependencyConstraint +import org.gradle.api.artifacts.MutableVersionConstraint +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging +import org.w3c.dom.Document +import org.w3c.dom.Element + +import javax.xml.parsers.DocumentBuilderFactory + +/** + * Lightweight replacement for the Spring Dependency Management plugin's + * version property override feature. + * + * <p>Parses BOM POM files to determine the version every managed artifact + * resolves to, both with the BOM's default {@code <properties>} values and + * with the project's overrides applied (via {@code ext['property.name']} in + * {@code build.gradle} or via {@code gradle.properties}). Any artifact whose + * effective version differs from the BOM default becomes a version override.</p> + * + * <p>Overrides are applied as <strong>strict</strong> dependency constraints + * (see {@link #applyTo(DependencyHandler, String)}). A strict constraint wins + * over the {@code require} constraints contributed by Gradle's native + * {@code platform()} mechanism, so an override is honored even when it + * <em>downgrades</em> a managed version - a plain + * {@code ResolutionStrategy.eachDependency()} / {@code useVersion()} hook + * would lose to the platform's higher version during conflict resolution.</p> + * + * <p>Because the effective version is computed by re-resolving imported + * ({@code <scope>import</scope>}) BOMs with the project's property overrides + * applied, overriding a property that selects an imported BOM's version + * (for example {@code spring-boot.version}) re-imports that BOM and pulls in + * its updated managed-dependency set.</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> + * + * <p>This is the underlying utility used by the + * {@code org.apache.grails.gradle.bom-property-overrides} plugin + * (registered in {@code grails-gradle-plugins}). It is BOM-agnostic and + * can be used directly with any BOM that follows the Maven + * {@code <properties>} convention for managed versions.</p> + * + * @since 8.0 + */ +@CompileStatic +class BomManagedVersions { + + private static final Logger LOG = Logging.getLogger(BomManagedVersions) + private static final int MAX_PROPERTY_INTERPOLATION_DEPTH = 10 + + /** A property resolver that never overrides anything (BOM defaults only). */ + private static final Function<String, String> NO_OVERRIDES = { String name -> null } as Function<String, String> + + private final Map<String, String> versionOverrides = new LinkedHashMap<>() + + /** + * Resolves a single BOM via captured Gradle services rather than a + * {@link Project} reference. Preferred for config-cache discipline: + * callers capture services once (typically inside a single + * {@code afterEvaluate} block) and never leak a {@link Project} + * reference into the override map that lives on past configuration time. + * + * @param configurations the project's configuration container, captured at apply/afterEvaluate time + * @param dependencies the project's dependency handler, captured at apply/afterEvaluate time + * @param propertyLookup function returning the project's property value as a String, or {@code null} if unset + * @param bomCoordinates the BOM coordinates in {@code group:artifact:version} format + * @return a BomManagedVersions instance containing any version overrides to apply + */ + static BomManagedVersions resolve(ConfigurationContainer configurations, + DependencyHandler dependencies, + Function<String, String> propertyLookup, + String bomCoordinates) { + return resolve(configurations, dependencies, propertyLookup, [bomCoordinates]) + } + + /** + * Resolves multiple BOMs via captured Gradle services. The result is a + * plain data carrier (a {@code Map<String, String>} of version overrides + * inside {@link BomManagedVersions}) that holds no {@link Project} + * reference, so it can be safely captured by per-configuration + * constraint declarations and survive configuration-cache serialization. + * + * <p>The override set is computed as the difference between two resolutions + * of the BOM tree: one using the BOM's default property values, and one + * using the project's property overrides. Any managed artifact whose + * effective version differs from its default version is recorded as an + * override.</p> + * + * @param configurations the project's configuration container, captured at apply/afterEvaluate time + * @param dependencies the project's dependency handler, captured at apply/afterEvaluate time + * @param propertyLookup function returning the project's property value as a String, or {@code null} if unset + * @param bomCoordinatesList list of BOM coordinates in {@code group:artifact:version} format + * @return a BomManagedVersions instance containing any version overrides to apply + */ + static BomManagedVersions resolve(ConfigurationContainer configurations, + DependencyHandler dependencies, + Function<String, String> propertyLookup, + Collection<String> bomCoordinatesList) { + def instance = new BomManagedVersions() + + Map<String, String> defaultVersions = computeManagedVersions( + configurations, dependencies, bomCoordinatesList, NO_OVERRIDES) + Map<String, String> effectiveVersions = computeManagedVersions( + configurations, dependencies, bomCoordinatesList, propertyLookup) + + for (def entry : effectiveVersions.entrySet()) { + def artifactKey = entry.key + def effectiveVersion = entry.value + def defaultVersion = defaultVersions.get(artifactKey) + + if (effectiveVersion != null && effectiveVersion != defaultVersion) { + instance.versionOverrides.put(artifactKey, effectiveVersion) + LOG.info( + 'BOM version override: {} = {} (BOM default: {})', + artifactKey, effectiveVersion, defaultVersion ?: 'unknown' + ) + } + } + + if (!instance.versionOverrides.isEmpty()) { + LOG.lifecycle('BOM property overrides: {} version override(s) will be applied', instance.versionOverrides.size()) + } + + return instance + } + + /** + * Convenience overload that captures services from the given {@link Project}. + * Production callers should prefer the services-based overload above so the + * resolve path never sees a {@link Project} reference. This overload is + * primarily useful for tests and ad-hoc usage. + * + * @param project the Gradle project (services are extracted at call time) + * @param bomCoordinates the BOM coordinates in {@code group:artifact:version} format + */ + static BomManagedVersions resolve(Project project, String bomCoordinates) { + return resolve(project, [bomCoordinates]) Review Comment: Unnecessary return statement for simple method? (here and other methods). ########## grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/bom/BomManagedVersions.groovy: ########## @@ -0,0 +1,503 @@ +/* + * 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.bom + +import java.util.function.Function + +import groovy.transform.CompileStatic +import org.gradle.api.Project +import org.gradle.api.artifacts.ConfigurationContainer +import org.gradle.api.artifacts.DependencyConstraint +import org.gradle.api.artifacts.MutableVersionConstraint +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging +import org.w3c.dom.Document +import org.w3c.dom.Element + +import javax.xml.parsers.DocumentBuilderFactory + +/** + * Lightweight replacement for the Spring Dependency Management plugin's + * version property override feature. + * + * <p>Parses BOM POM files to determine the version every managed artifact + * resolves to, both with the BOM's default {@code <properties>} values and + * with the project's overrides applied (via {@code ext['property.name']} in + * {@code build.gradle} or via {@code gradle.properties}). Any artifact whose + * effective version differs from the BOM default becomes a version override.</p> + * + * <p>Overrides are applied as <strong>strict</strong> dependency constraints + * (see {@link #applyTo(DependencyHandler, String)}). A strict constraint wins + * over the {@code require} constraints contributed by Gradle's native + * {@code platform()} mechanism, so an override is honored even when it + * <em>downgrades</em> a managed version - a plain + * {@code ResolutionStrategy.eachDependency()} / {@code useVersion()} hook + * would lose to the platform's higher version during conflict resolution.</p> + * + * <p>Because the effective version is computed by re-resolving imported + * ({@code <scope>import</scope>}) BOMs with the project's property overrides + * applied, overriding a property that selects an imported BOM's version + * (for example {@code spring-boot.version}) re-imports that BOM and pulls in + * its updated managed-dependency set.</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> + * + * <p>This is the underlying utility used by the + * {@code org.apache.grails.gradle.bom-property-overrides} plugin + * (registered in {@code grails-gradle-plugins}). It is BOM-agnostic and + * can be used directly with any BOM that follows the Maven + * {@code <properties>} convention for managed versions.</p> + * + * @since 8.0 + */ +@CompileStatic +class BomManagedVersions { + + private static final Logger LOG = Logging.getLogger(BomManagedVersions) + private static final int MAX_PROPERTY_INTERPOLATION_DEPTH = 10 + + /** A property resolver that never overrides anything (BOM defaults only). */ + private static final Function<String, String> NO_OVERRIDES = { String name -> null } as Function<String, String> + + private final Map<String, String> versionOverrides = new LinkedHashMap<>() + + /** + * Resolves a single BOM via captured Gradle services rather than a + * {@link Project} reference. Preferred for config-cache discipline: + * callers capture services once (typically inside a single + * {@code afterEvaluate} block) and never leak a {@link Project} + * reference into the override map that lives on past configuration time. + * + * @param configurations the project's configuration container, captured at apply/afterEvaluate time + * @param dependencies the project's dependency handler, captured at apply/afterEvaluate time + * @param propertyLookup function returning the project's property value as a String, or {@code null} if unset + * @param bomCoordinates the BOM coordinates in {@code group:artifact:version} format + * @return a BomManagedVersions instance containing any version overrides to apply + */ + static BomManagedVersions resolve(ConfigurationContainer configurations, + DependencyHandler dependencies, + Function<String, String> propertyLookup, + String bomCoordinates) { + return resolve(configurations, dependencies, propertyLookup, [bomCoordinates]) + } + + /** + * Resolves multiple BOMs via captured Gradle services. The result is a + * plain data carrier (a {@code Map<String, String>} of version overrides + * inside {@link BomManagedVersions}) that holds no {@link Project} + * reference, so it can be safely captured by per-configuration + * constraint declarations and survive configuration-cache serialization. + * + * <p>The override set is computed as the difference between two resolutions + * of the BOM tree: one using the BOM's default property values, and one + * using the project's property overrides. Any managed artifact whose + * effective version differs from its default version is recorded as an + * override.</p> + * + * @param configurations the project's configuration container, captured at apply/afterEvaluate time + * @param dependencies the project's dependency handler, captured at apply/afterEvaluate time + * @param propertyLookup function returning the project's property value as a String, or {@code null} if unset + * @param bomCoordinatesList list of BOM coordinates in {@code group:artifact:version} format + * @return a BomManagedVersions instance containing any version overrides to apply + */ + static BomManagedVersions resolve(ConfigurationContainer configurations, + DependencyHandler dependencies, + Function<String, String> propertyLookup, + Collection<String> bomCoordinatesList) { + def instance = new BomManagedVersions() + + Map<String, String> defaultVersions = computeManagedVersions( Review Comment: Use `def` where the type can be inferred? Here and next statement. ########## grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/bom/BomManagedVersions.groovy: ########## @@ -0,0 +1,503 @@ +/* + * 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.bom + +import java.util.function.Function + +import groovy.transform.CompileStatic +import org.gradle.api.Project +import org.gradle.api.artifacts.ConfigurationContainer +import org.gradle.api.artifacts.DependencyConstraint +import org.gradle.api.artifacts.MutableVersionConstraint +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging +import org.w3c.dom.Document +import org.w3c.dom.Element + +import javax.xml.parsers.DocumentBuilderFactory + +/** + * Lightweight replacement for the Spring Dependency Management plugin's + * version property override feature. + * + * <p>Parses BOM POM files to determine the version every managed artifact + * resolves to, both with the BOM's default {@code <properties>} values and + * with the project's overrides applied (via {@code ext['property.name']} in + * {@code build.gradle} or via {@code gradle.properties}). Any artifact whose + * effective version differs from the BOM default becomes a version override.</p> + * + * <p>Overrides are applied as <strong>strict</strong> dependency constraints + * (see {@link #applyTo(DependencyHandler, String)}). A strict constraint wins + * over the {@code require} constraints contributed by Gradle's native + * {@code platform()} mechanism, so an override is honored even when it + * <em>downgrades</em> a managed version - a plain + * {@code ResolutionStrategy.eachDependency()} / {@code useVersion()} hook + * would lose to the platform's higher version during conflict resolution.</p> + * + * <p>Because the effective version is computed by re-resolving imported + * ({@code <scope>import</scope>}) BOMs with the project's property overrides + * applied, overriding a property that selects an imported BOM's version + * (for example {@code spring-boot.version}) re-imports that BOM and pulls in + * its updated managed-dependency set.</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> + * + * <p>This is the underlying utility used by the + * {@code org.apache.grails.gradle.bom-property-overrides} plugin + * (registered in {@code grails-gradle-plugins}). It is BOM-agnostic and + * can be used directly with any BOM that follows the Maven + * {@code <properties>} convention for managed versions.</p> + * + * @since 8.0 + */ +@CompileStatic +class BomManagedVersions { + + private static final Logger LOG = Logging.getLogger(BomManagedVersions) + private static final int MAX_PROPERTY_INTERPOLATION_DEPTH = 10 + + /** A property resolver that never overrides anything (BOM defaults only). */ + private static final Function<String, String> NO_OVERRIDES = { String name -> null } as Function<String, String> + + private final Map<String, String> versionOverrides = new LinkedHashMap<>() + + /** + * Resolves a single BOM via captured Gradle services rather than a + * {@link Project} reference. Preferred for config-cache discipline: + * callers capture services once (typically inside a single + * {@code afterEvaluate} block) and never leak a {@link Project} + * reference into the override map that lives on past configuration time. + * + * @param configurations the project's configuration container, captured at apply/afterEvaluate time + * @param dependencies the project's dependency handler, captured at apply/afterEvaluate time + * @param propertyLookup function returning the project's property value as a String, or {@code null} if unset + * @param bomCoordinates the BOM coordinates in {@code group:artifact:version} format + * @return a BomManagedVersions instance containing any version overrides to apply + */ + static BomManagedVersions resolve(ConfigurationContainer configurations, + DependencyHandler dependencies, + Function<String, String> propertyLookup, + String bomCoordinates) { + return resolve(configurations, dependencies, propertyLookup, [bomCoordinates]) + } + + /** + * Resolves multiple BOMs via captured Gradle services. The result is a + * plain data carrier (a {@code Map<String, String>} of version overrides + * inside {@link BomManagedVersions}) that holds no {@link Project} + * reference, so it can be safely captured by per-configuration + * constraint declarations and survive configuration-cache serialization. + * + * <p>The override set is computed as the difference between two resolutions + * of the BOM tree: one using the BOM's default property values, and one + * using the project's property overrides. Any managed artifact whose + * effective version differs from its default version is recorded as an + * override.</p> + * + * @param configurations the project's configuration container, captured at apply/afterEvaluate time + * @param dependencies the project's dependency handler, captured at apply/afterEvaluate time + * @param propertyLookup function returning the project's property value as a String, or {@code null} if unset + * @param bomCoordinatesList list of BOM coordinates in {@code group:artifact:version} format + * @return a BomManagedVersions instance containing any version overrides to apply + */ + static BomManagedVersions resolve(ConfigurationContainer configurations, + DependencyHandler dependencies, + Function<String, String> propertyLookup, + Collection<String> bomCoordinatesList) { + def instance = new BomManagedVersions() + + Map<String, String> defaultVersions = computeManagedVersions( + configurations, dependencies, bomCoordinatesList, NO_OVERRIDES) + Map<String, String> effectiveVersions = computeManagedVersions( + configurations, dependencies, bomCoordinatesList, propertyLookup) + + for (def entry : effectiveVersions.entrySet()) { + def artifactKey = entry.key + def effectiveVersion = entry.value + def defaultVersion = defaultVersions.get(artifactKey) + + if (effectiveVersion != null && effectiveVersion != defaultVersion) { + instance.versionOverrides.put(artifactKey, effectiveVersion) + LOG.info( + 'BOM version override: {} = {} (BOM default: {})', + artifactKey, effectiveVersion, defaultVersion ?: 'unknown' + ) + } + } + + if (!instance.versionOverrides.isEmpty()) { + LOG.lifecycle('BOM property overrides: {} version override(s) will be applied', instance.versionOverrides.size()) Review Comment: Break line? ########## grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPlugin.groovy: ########## @@ -0,0 +1,209 @@ +/* + * 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.bom + +import java.util.function.Function + +import groovy.transform.CompileStatic +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ConfigurationContainer +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.ModuleDependency +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.attributes.Category + +/** + * Gradle plugin that enables Maven-style property-based version overrides + * for {@code platform()} BOMs. + * + * <p>This is the BOM-agnostic replacement for the Spring Dependency + * Management plugin's property-override feature. The plugin is shipped as + * part of {@code grails-gradle-plugins} but can be applied to any project + * (Grails or otherwise) that consumes a BOM published with version + * property references in its {@code <dependencyManagement>} block:</p> + * + * <pre> + * plugins { + * id 'org.apache.grails.gradle.bom-property-overrides' + * } + * + * dependencies { + * implementation platform('com.example:my-bom:1.0.0') + * } + * + * // gradle.properties or build.gradle + * ext['slf4j.version'] = '2.0.13' + * </pre> + * + * <h2>How it works</h2> + * <ol> + * <li>Auto-detects all {@code platform()} / {@code enforcedPlatform()} + * dependencies declared on the project's configurations (configurable + * via {@link BomPropertyOverridesExtension#autoDetect}).</li> + * <li>Resolves each BOM POM in a detached configuration, parses the + * {@code <properties>} block and the + * {@code <dependencyManagement>} entries, and recursively follows + * {@code <scope>import</scope>} BOMs.</li> + * <li>Computes each managed artifact's version twice - once with the BOM's + * default properties and once with the project's property overrides + * (from {@code gradle.properties} or {@code ext['property.name']}) + * applied, including to imported-BOM selector versions. Any artifact + * whose effective version differs from its default version is recorded + * as an override.</li> + * <li>Applies each override as a <strong>strict</strong> dependency + * constraint on the project's declarable configurations, so the override + * wins over the {@code require} constraints contributed by + * {@code platform()} even when it downgrades a managed version.</li> + * </ol> + * + * <p>The plugin does <strong>not</strong> declare any platforms itself. + * Consumers (or other plugins like {@code grails-app}) remain responsible + * for declaring the {@code platform()} dependencies; this plugin only + * adds the property-override layer on top.</p> + * + * @since 8.0 + * @see BomManagedVersions + * @see BomPropertyOverridesExtension + */ +@CompileStatic +class BomPropertyOverridesPlugin implements Plugin<Project> { + + /** + * The plugin id, exposed as a constant for programmatic application + * (e.g. {@code project.plugins.apply(BomPropertyOverridesPlugin.PLUGIN_ID)}). + */ + static final String PLUGIN_ID = 'org.apache.grails.gradle.bom-property-overrides' + + @Override + void apply(Project project) { + def extension = project.extensions.create( + BomPropertyOverridesExtension.EXTENSION_NAME, + BomPropertyOverridesExtension, + project.objects + ) + + // We wait until afterEvaluate to scan the project's declared platforms + // and resolve their POMs, because the user typically declares + // platform() dependencies and configures the bomPropertyOverrides + // extension in the same build.gradle that applies this plugin. + // + // The afterEvaluate callback itself is a configuration-time callback, + // not serialised into the configuration cache. We capture Gradle + // services (ConfigurationContainer, DependencyHandler) and a property + // lookup function at the boundary of this callback and hand them to + // the resolver, so the resulting BomManagedVersions instance carries + // no Project reference. The per-configuration eachDependency closures + // installed by applyOverrides() capture only the resulting + // Map<String, String> of overrides, which is fully serialisable and + // safe for the configuration cache. + // + // Verified with `./gradlew --configuration-cache`: zero CC warnings + // originate from this plugin or from BomManagedVersions. + project.afterEvaluate { + applyOverrides( + project.configurations, + project.dependencies, + { String name -> project.hasProperty(name) ? project.property(name)?.toString() : null } as Function<String, String>, + extension + ) + } + } + + /** + * Resolves the configured BOMs and applies any version overrides found + * to all project configurations. Takes captured Gradle services rather + * than a {@link Project} so that the resolve path holds no + * configuration-cache-hostile state. Visible for testing. + */ + static void applyOverrides(ConfigurationContainer configurations, + DependencyHandler dependencies, + Function<String, String> propertyLookup, + BomPropertyOverridesExtension extension) { + def bomCoordinates = new LinkedHashSet<String>() + + if (extension.autoDetect.get()) { + bomCoordinates.addAll(detectDeclaredBoms(configurations)) + } + + for (String explicit : extension.boms.get()) { + if (explicit) { + bomCoordinates.add(explicit) + } + } + + if (bomCoordinates.isEmpty()) { + return + } + + def managedVersions = BomManagedVersions.resolve(configurations, dependencies, propertyLookup, bomCoordinates) + if (!managedVersions.hasOverrides()) { + return + } + + // Apply overrides as strict constraints on every declarable configuration, + // mirroring where the platform(grails-bom) constraints are contributed. + // Resolvable/consumable configurations inherit the constraints through the + // declarable configurations they extend. + configurations.configureEach { Configuration conf -> + if (conf.canBeDeclared) { + managedVersions.applyTo(dependencies, conf.name) + } + } Review Comment: Possible simplification (without loosing IDE type hints): ```suggestion configurations.configureEach { if (it.canBeDeclared) { managedVersions.applyTo(dependencies, it.name) } } ``` ########## grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/bom/BomManagedVersions.groovy: ########## @@ -0,0 +1,503 @@ +/* + * 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.bom + +import java.util.function.Function + +import groovy.transform.CompileStatic +import org.gradle.api.Project +import org.gradle.api.artifacts.ConfigurationContainer +import org.gradle.api.artifacts.DependencyConstraint +import org.gradle.api.artifacts.MutableVersionConstraint +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging +import org.w3c.dom.Document +import org.w3c.dom.Element + +import javax.xml.parsers.DocumentBuilderFactory + +/** + * Lightweight replacement for the Spring Dependency Management plugin's + * version property override feature. + * + * <p>Parses BOM POM files to determine the version every managed artifact + * resolves to, both with the BOM's default {@code <properties>} values and + * with the project's overrides applied (via {@code ext['property.name']} in + * {@code build.gradle} or via {@code gradle.properties}). Any artifact whose + * effective version differs from the BOM default becomes a version override.</p> + * + * <p>Overrides are applied as <strong>strict</strong> dependency constraints + * (see {@link #applyTo(DependencyHandler, String)}). A strict constraint wins + * over the {@code require} constraints contributed by Gradle's native + * {@code platform()} mechanism, so an override is honored even when it + * <em>downgrades</em> a managed version - a plain + * {@code ResolutionStrategy.eachDependency()} / {@code useVersion()} hook + * would lose to the platform's higher version during conflict resolution.</p> + * + * <p>Because the effective version is computed by re-resolving imported + * ({@code <scope>import</scope>}) BOMs with the project's property overrides + * applied, overriding a property that selects an imported BOM's version + * (for example {@code spring-boot.version}) re-imports that BOM and pulls in + * its updated managed-dependency set.</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> + * + * <p>This is the underlying utility used by the + * {@code org.apache.grails.gradle.bom-property-overrides} plugin + * (registered in {@code grails-gradle-plugins}). It is BOM-agnostic and + * can be used directly with any BOM that follows the Maven + * {@code <properties>} convention for managed versions.</p> + * + * @since 8.0 + */ +@CompileStatic +class BomManagedVersions { + + private static final Logger LOG = Logging.getLogger(BomManagedVersions) + private static final int MAX_PROPERTY_INTERPOLATION_DEPTH = 10 + + /** A property resolver that never overrides anything (BOM defaults only). */ + private static final Function<String, String> NO_OVERRIDES = { String name -> null } as Function<String, String> + + private final Map<String, String> versionOverrides = new LinkedHashMap<>() + + /** + * Resolves a single BOM via captured Gradle services rather than a + * {@link Project} reference. Preferred for config-cache discipline: + * callers capture services once (typically inside a single + * {@code afterEvaluate} block) and never leak a {@link Project} + * reference into the override map that lives on past configuration time. + * + * @param configurations the project's configuration container, captured at apply/afterEvaluate time + * @param dependencies the project's dependency handler, captured at apply/afterEvaluate time + * @param propertyLookup function returning the project's property value as a String, or {@code null} if unset + * @param bomCoordinates the BOM coordinates in {@code group:artifact:version} format + * @return a BomManagedVersions instance containing any version overrides to apply + */ + static BomManagedVersions resolve(ConfigurationContainer configurations, + DependencyHandler dependencies, + Function<String, String> propertyLookup, + String bomCoordinates) { + return resolve(configurations, dependencies, propertyLookup, [bomCoordinates]) + } + + /** + * Resolves multiple BOMs via captured Gradle services. The result is a + * plain data carrier (a {@code Map<String, String>} of version overrides + * inside {@link BomManagedVersions}) that holds no {@link Project} + * reference, so it can be safely captured by per-configuration + * constraint declarations and survive configuration-cache serialization. + * + * <p>The override set is computed as the difference between two resolutions + * of the BOM tree: one using the BOM's default property values, and one + * using the project's property overrides. Any managed artifact whose + * effective version differs from its default version is recorded as an + * override.</p> + * + * @param configurations the project's configuration container, captured at apply/afterEvaluate time + * @param dependencies the project's dependency handler, captured at apply/afterEvaluate time + * @param propertyLookup function returning the project's property value as a String, or {@code null} if unset + * @param bomCoordinatesList list of BOM coordinates in {@code group:artifact:version} format + * @return a BomManagedVersions instance containing any version overrides to apply + */ + static BomManagedVersions resolve(ConfigurationContainer configurations, + DependencyHandler dependencies, + Function<String, String> propertyLookup, + Collection<String> bomCoordinatesList) { + def instance = new BomManagedVersions() + + Map<String, String> defaultVersions = computeManagedVersions( + configurations, dependencies, bomCoordinatesList, NO_OVERRIDES) + Map<String, String> effectiveVersions = computeManagedVersions( + configurations, dependencies, bomCoordinatesList, propertyLookup) + + for (def entry : effectiveVersions.entrySet()) { + def artifactKey = entry.key + def effectiveVersion = entry.value + def defaultVersion = defaultVersions.get(artifactKey) + + if (effectiveVersion != null && effectiveVersion != defaultVersion) { + instance.versionOverrides.put(artifactKey, effectiveVersion) + LOG.info( + 'BOM version override: {} = {} (BOM default: {})', + artifactKey, effectiveVersion, defaultVersion ?: 'unknown' + ) + } + } + + if (!instance.versionOverrides.isEmpty()) { + LOG.lifecycle('BOM property overrides: {} version override(s) will be applied', instance.versionOverrides.size()) + } + + return instance + } + + /** + * Convenience overload that captures services from the given {@link Project}. + * Production callers should prefer the services-based overload above so the + * resolve path never sees a {@link Project} reference. This overload is + * primarily useful for tests and ad-hoc usage. + * + * @param project the Gradle project (services are extracted at call time) + * @param bomCoordinates the BOM coordinates in {@code group:artifact:version} format + */ + static BomManagedVersions resolve(Project project, String bomCoordinates) { + return resolve(project, [bomCoordinates]) + } + + /** + * Convenience overload that captures services from the given {@link Project}. + * Production callers should prefer the services-based overload above so the + * resolve path never sees a {@link Project} reference. This overload is + * primarily useful for tests and ad-hoc usage. + * + * @param project the Gradle project (services are extracted at call time) + * @param bomCoordinatesList list of BOM coordinates in {@code group:artifact:version} format + */ + static BomManagedVersions resolve(Project project, Collection<String> bomCoordinatesList) { + return resolve( + project.configurations, + project.dependencies, + { String name -> project.hasProperty(name) ? project.property(name)?.toString() : null } as Function<String, String>, + bomCoordinatesList + ) + } + + /** + * Applies the detected version overrides to the given configuration as + * strict dependency constraints. + * + * <p>Strict constraints are used deliberately: a plain {@code platform()} + * contributes {@code require} constraints, and a soft override (e.g. + * {@code useVersion()}) would lose to a higher {@code require} version + * during conflict resolution. A strict constraint overrides {@code require}, + * so the project's chosen version wins in both directions (upgrade and + * downgrade).</p> + * + * @param dependencies the project's dependency handler + * @param configurationName the name of the configuration to add constraints to + */ + void applyTo(DependencyHandler dependencies, String configurationName) { + if (versionOverrides.isEmpty()) { + return + } + + versionOverrides.each { String coordinate, String version -> + dependencies.constraints.add(configurationName, coordinate) { DependencyConstraint constraint -> + constraint.version { MutableVersionConstraint v -> v.strictly(version) } + constraint.because('BOM version override via project property') + } Review Comment: Possible simplification (without loosing IDE hints): ```suggestion dependencies.constraints.add(configurationName, coordinate) { it.version { it.strictly(version) } it.because('BOM version override via project property') } ``` ########## build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsDependencyValidatorPlugin.groovy: ########## @@ -159,19 +161,49 @@ class GrailsDependencyValidatorPlugin implements Plugin<Project> { /** * Scans the project's configurations to find which BOM project is in use. + * + * <p>When multiple known BOMs are declared on the same project (for example, + * the {@code grails-app} plugin auto-injects {@code platform(grails-bom)} on + * every declarable configuration while a Micronaut project additionally + * declares {@code enforcedPlatform(grails-micronaut-bom)}), this method + * prefers an {@code enforcedPlatform} declaration over a regular + * {@code platform}. The enforced BOM is the one whose constraints actually + * win at resolution time, so it is the correct reference for the + * "expected" versions reported by the validator.</p> */ static String detectBomPath(Project project) { + String regularPlatformBomPath = null + for (Configuration config : project.configurations) { for (Dependency dep : config.dependencies) { - if (BOM_PROJECT_NAMES.contains(dep.name)) { - Project bomProject = project.rootProject.findProject(":${dep.name}" as String) - if (bomProject != null) { - return bomProject.path - } + if (!BOM_PROJECT_NAMES.contains(dep.name)) { + continue + } + Project bomProject = project.rootProject.findProject(":${dep.name}" as String) Review Comment: `def`? ########## build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsDependencyValidatorPlugin.groovy: ########## @@ -159,19 +161,49 @@ class GrailsDependencyValidatorPlugin implements Plugin<Project> { /** * Scans the project's configurations to find which BOM project is in use. + * + * <p>When multiple known BOMs are declared on the same project (for example, + * the {@code grails-app} plugin auto-injects {@code platform(grails-bom)} on + * every declarable configuration while a Micronaut project additionally + * declares {@code enforcedPlatform(grails-micronaut-bom)}), this method + * prefers an {@code enforcedPlatform} declaration over a regular + * {@code platform}. The enforced BOM is the one whose constraints actually + * win at resolution time, so it is the correct reference for the + * "expected" versions reported by the validator.</p> */ static String detectBomPath(Project project) { + String regularPlatformBomPath = null + for (Configuration config : project.configurations) { for (Dependency dep : config.dependencies) { - if (BOM_PROJECT_NAMES.contains(dep.name)) { - Project bomProject = project.rootProject.findProject(":${dep.name}" as String) - if (bomProject != null) { - return bomProject.path - } + if (!BOM_PROJECT_NAMES.contains(dep.name)) { + continue + } + Project bomProject = project.rootProject.findProject(":${dep.name}" as String) + if (bomProject == null) { + continue + } + if (isEnforcedPlatformDependency(dep)) { + return bomProject.path + } + if (regularPlatformBomPath == null) { + regularPlatformBomPath = bomProject.path } } } - null + + regularPlatformBomPath + } + + private static boolean isEnforcedPlatformDependency(Dependency dep) { + if (!(dep instanceof ModuleDependency)) { + return false + } + Object categoryAttr = ((ModuleDependency) dep).attributes.getAttribute(Category.CATEGORY_ATTRIBUTE) Review Comment: `def`? ########## grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPlugin.groovy: ########## @@ -0,0 +1,209 @@ +/* + * 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.bom + +import java.util.function.Function + +import groovy.transform.CompileStatic +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ConfigurationContainer +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.ModuleDependency +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.attributes.Category + +/** + * Gradle plugin that enables Maven-style property-based version overrides + * for {@code platform()} BOMs. + * + * <p>This is the BOM-agnostic replacement for the Spring Dependency + * Management plugin's property-override feature. The plugin is shipped as + * part of {@code grails-gradle-plugins} but can be applied to any project + * (Grails or otherwise) that consumes a BOM published with version + * property references in its {@code <dependencyManagement>} block:</p> + * + * <pre> + * plugins { + * id 'org.apache.grails.gradle.bom-property-overrides' + * } + * + * dependencies { + * implementation platform('com.example:my-bom:1.0.0') + * } + * + * // gradle.properties or build.gradle + * ext['slf4j.version'] = '2.0.13' + * </pre> + * + * <h2>How it works</h2> + * <ol> + * <li>Auto-detects all {@code platform()} / {@code enforcedPlatform()} + * dependencies declared on the project's configurations (configurable + * via {@link BomPropertyOverridesExtension#autoDetect}).</li> + * <li>Resolves each BOM POM in a detached configuration, parses the + * {@code <properties>} block and the + * {@code <dependencyManagement>} entries, and recursively follows + * {@code <scope>import</scope>} BOMs.</li> + * <li>Computes each managed artifact's version twice - once with the BOM's + * default properties and once with the project's property overrides + * (from {@code gradle.properties} or {@code ext['property.name']}) + * applied, including to imported-BOM selector versions. Any artifact + * whose effective version differs from its default version is recorded + * as an override.</li> + * <li>Applies each override as a <strong>strict</strong> dependency + * constraint on the project's declarable configurations, so the override + * wins over the {@code require} constraints contributed by + * {@code platform()} even when it downgrades a managed version.</li> + * </ol> + * + * <p>The plugin does <strong>not</strong> declare any platforms itself. + * Consumers (or other plugins like {@code grails-app}) remain responsible + * for declaring the {@code platform()} dependencies; this plugin only + * adds the property-override layer on top.</p> + * + * @since 8.0 + * @see BomManagedVersions + * @see BomPropertyOverridesExtension + */ +@CompileStatic +class BomPropertyOverridesPlugin implements Plugin<Project> { + + /** + * The plugin id, exposed as a constant for programmatic application + * (e.g. {@code project.plugins.apply(BomPropertyOverridesPlugin.PLUGIN_ID)}). + */ + static final String PLUGIN_ID = 'org.apache.grails.gradle.bom-property-overrides' + + @Override + void apply(Project project) { + def extension = project.extensions.create( + BomPropertyOverridesExtension.EXTENSION_NAME, + BomPropertyOverridesExtension, + project.objects + ) + + // We wait until afterEvaluate to scan the project's declared platforms + // and resolve their POMs, because the user typically declares + // platform() dependencies and configures the bomPropertyOverrides + // extension in the same build.gradle that applies this plugin. + // + // The afterEvaluate callback itself is a configuration-time callback, + // not serialised into the configuration cache. We capture Gradle + // services (ConfigurationContainer, DependencyHandler) and a property + // lookup function at the boundary of this callback and hand them to + // the resolver, so the resulting BomManagedVersions instance carries + // no Project reference. The per-configuration eachDependency closures + // installed by applyOverrides() capture only the resulting + // Map<String, String> of overrides, which is fully serialisable and + // safe for the configuration cache. + // + // Verified with `./gradlew --configuration-cache`: zero CC warnings + // originate from this plugin or from BomManagedVersions. + project.afterEvaluate { + applyOverrides( + project.configurations, + project.dependencies, + { String name -> project.hasProperty(name) ? project.property(name)?.toString() : null } as Function<String, String>, + extension + ) + } + } + + /** + * Resolves the configured BOMs and applies any version overrides found + * to all project configurations. Takes captured Gradle services rather + * than a {@link Project} so that the resolve path holds no + * configuration-cache-hostile state. Visible for testing. + */ + static void applyOverrides(ConfigurationContainer configurations, + DependencyHandler dependencies, + Function<String, String> propertyLookup, + BomPropertyOverridesExtension extension) { + def bomCoordinates = new LinkedHashSet<String>() + + if (extension.autoDetect.get()) { + bomCoordinates.addAll(detectDeclaredBoms(configurations)) + } + + for (String explicit : extension.boms.get()) { + if (explicit) { + bomCoordinates.add(explicit) + } + } + + if (bomCoordinates.isEmpty()) { + return + } + + def managedVersions = BomManagedVersions.resolve(configurations, dependencies, propertyLookup, bomCoordinates) + if (!managedVersions.hasOverrides()) { + return + } + + // Apply overrides as strict constraints on every declarable configuration, + // mirroring where the platform(grails-bom) constraints are contributed. + // Resolvable/consumable configurations inherit the constraints through the + // declarable configurations they extend. + configurations.configureEach { Configuration conf -> + if (conf.canBeDeclared) { + managedVersions.applyTo(dependencies, conf.name) + } + } + } + + /** + * Scans every configuration for declared {@code platform()} or + * {@code enforcedPlatform()} dependencies and returns their coordinates. + * Takes a {@link ConfigurationContainer} rather than a {@link Project} + * so the call path stays free of Project references. Visible for testing. + */ + static Set<String> detectDeclaredBoms(ConfigurationContainer configurations) { + def coordinates = new LinkedHashSet<String>() + + configurations.each { Configuration conf -> + for (Dependency dep : conf.dependencies) { Review Comment: Possible simplification (without loosing IDE hints): ```suggestion configurations.each { for (Dependency dep : it.dependencies) { ``` ########## build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy: ########## @@ -259,91 +261,229 @@ abstract class ExtractDependenciesTask extends DefaultTask { Properties populatePlatformDependencies(CoordinateVersionHolder bomCoordinates, List<CoordinateHolder> exclusionRules, Map<CoordinateHolder, ExtractedDependencyConstraint> constraints, boolean error = true, int level = 0) { Dependency bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom") Configuration dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency) + dependencyConfiguration.transitive = false File bomPomFile = dependencyConfiguration.singleFile - MavenXpp3Reader reader = new MavenXpp3Reader() - Model model = reader.read(new FileReader(bomPomFile)) - + Document doc = parsePom(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 - ) + + // Parent POM populated first so its properties can be overridden by the child + CoordinateVersionHolder parentBom = readParentCoordinates(doc) + if (parentBom) { 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) + + readProperties(doc).each { String name, String value -> + versionProperties.put(name, 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, versionPropertyReference: 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 { + List<ManagedDependency> managedDependencies = readManagedDependencies(doc) + if (managedDependencies.isEmpty()) { 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.") } + return versionProperties + } + + for (ManagedDependency depItem : managedDependencies) { + 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)) { + continue + } + + 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) { + continue + } + + 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, versionPropertyReference: propertyName, source: bomCoordinates.artifactId + ) + constraints.put(resolvedCoordinates, constraint) + + if (depItem.scope == 'import') { + CoordinateVersionHolder resolvedBomCoordinates = new CoordinateVersionHolder( + groupId: resolvedCoordinates.groupId, + artifactId: resolvedCoordinates.artifactId, + version: resolvedVersion + ) + populatePlatformDependencies(resolvedBomCoordinates, exclusionRules, constraints, error, level + 1) + } } versionProperties } + /** + * Parses a BOM POM file using the JDK's built-in {@link DocumentBuilderFactory}. + * XML parsing is hardened against XXE / XInclude attacks. + */ + private static Document parsePom(File pomFile) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance() + factory.setNamespaceAware(false) + factory.setValidating(false) + factory.setXIncludeAware(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) Review Comment: Possible simplification: ```suggestion DocumentBuilderFactory.newInstance().tap { namespaceAware = false validating = false XIncludeAware = false setFeature('http://apache.org/xml/features/nonvalidating/load-external-dtd', false) setFeature('http://xml.org/sax/features/external-general-entities', false) setFeature('http://xml.org/sax/features/external-parameter-entities', false) }.newDocumentBuilder().parse(pomFile) ``` ########## build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy: ########## @@ -259,91 +261,229 @@ abstract class ExtractDependenciesTask extends DefaultTask { Properties populatePlatformDependencies(CoordinateVersionHolder bomCoordinates, List<CoordinateHolder> exclusionRules, Map<CoordinateHolder, ExtractedDependencyConstraint> constraints, boolean error = true, int level = 0) { Dependency bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom") Configuration dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency) + dependencyConfiguration.transitive = false File bomPomFile = dependencyConfiguration.singleFile - MavenXpp3Reader reader = new MavenXpp3Reader() - Model model = reader.read(new FileReader(bomPomFile)) - + Document doc = parsePom(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 - ) + + // Parent POM populated first so its properties can be overridden by the child + CoordinateVersionHolder parentBom = readParentCoordinates(doc) + if (parentBom) { 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) + + readProperties(doc).each { String name, String value -> + versionProperties.put(name, 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, versionPropertyReference: 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 { + List<ManagedDependency> managedDependencies = readManagedDependencies(doc) + if (managedDependencies.isEmpty()) { 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.") } + return versionProperties + } + + for (ManagedDependency depItem : managedDependencies) { + 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)) { + continue + } + + 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) { + continue + } + + String resolvedVersion = resolveMavenProperty(resolvedCoordinates.coordinatesWithoutVersion, depItem.version, versionProperties) + String propertyName = depItem.version?.contains('$') ? depItem.version : null + ExtractedDependencyConstraint constraint = new ExtractedDependencyConstraint( Review Comment: 3 x `def`? ########## build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy: ########## @@ -259,91 +261,229 @@ abstract class ExtractDependenciesTask extends DefaultTask { Properties populatePlatformDependencies(CoordinateVersionHolder bomCoordinates, List<CoordinateHolder> exclusionRules, Map<CoordinateHolder, ExtractedDependencyConstraint> constraints, boolean error = true, int level = 0) { Dependency bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom") Configuration dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency) + dependencyConfiguration.transitive = false File bomPomFile = dependencyConfiguration.singleFile - MavenXpp3Reader reader = new MavenXpp3Reader() - Model model = reader.read(new FileReader(bomPomFile)) - + Document doc = parsePom(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 - ) + + // Parent POM populated first so its properties can be overridden by the child + CoordinateVersionHolder parentBom = readParentCoordinates(doc) + if (parentBom) { 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) + + readProperties(doc).each { String name, String value -> + versionProperties.put(name, 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, versionPropertyReference: 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 { + List<ManagedDependency> managedDependencies = readManagedDependencies(doc) + if (managedDependencies.isEmpty()) { 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.") } + return versionProperties + } + + for (ManagedDependency depItem : managedDependencies) { + 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)) { + continue + } + + 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) { + continue + } + + 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, versionPropertyReference: propertyName, source: bomCoordinates.artifactId + ) + constraints.put(resolvedCoordinates, constraint) + + if (depItem.scope == 'import') { + CoordinateVersionHolder resolvedBomCoordinates = new CoordinateVersionHolder( Review Comment: `def`? ########## build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy: ########## @@ -259,91 +261,229 @@ abstract class ExtractDependenciesTask extends DefaultTask { Properties populatePlatformDependencies(CoordinateVersionHolder bomCoordinates, List<CoordinateHolder> exclusionRules, Map<CoordinateHolder, ExtractedDependencyConstraint> constraints, boolean error = true, int level = 0) { Dependency bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom") Configuration dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency) + dependencyConfiguration.transitive = false File bomPomFile = dependencyConfiguration.singleFile - MavenXpp3Reader reader = new MavenXpp3Reader() - Model model = reader.read(new FileReader(bomPomFile)) - + Document doc = parsePom(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 - ) + + // Parent POM populated first so its properties can be overridden by the child + CoordinateVersionHolder parentBom = readParentCoordinates(doc) + if (parentBom) { 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) + + readProperties(doc).each { String name, String value -> + versionProperties.put(name, 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, versionPropertyReference: 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 { + List<ManagedDependency> managedDependencies = readManagedDependencies(doc) Review Comment: `def`? ########## build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy: ########## @@ -259,91 +261,229 @@ abstract class ExtractDependenciesTask extends DefaultTask { Properties populatePlatformDependencies(CoordinateVersionHolder bomCoordinates, List<CoordinateHolder> exclusionRules, Map<CoordinateHolder, ExtractedDependencyConstraint> constraints, boolean error = true, int level = 0) { Dependency bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom") Configuration dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency) + dependencyConfiguration.transitive = false File bomPomFile = dependencyConfiguration.singleFile - MavenXpp3Reader reader = new MavenXpp3Reader() - Model model = reader.read(new FileReader(bomPomFile)) - + Document doc = parsePom(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 - ) + + // Parent POM populated first so its properties can be overridden by the child + CoordinateVersionHolder parentBom = readParentCoordinates(doc) + if (parentBom) { 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) + + readProperties(doc).each { String name, String value -> + versionProperties.put(name, 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, versionPropertyReference: 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 { + List<ManagedDependency> managedDependencies = readManagedDependencies(doc) + if (managedDependencies.isEmpty()) { 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.") } + return versionProperties + } + + for (ManagedDependency depItem : managedDependencies) { + CoordinateHolder baseCoordinates = new CoordinateHolder( + groupId: depItem.groupId, + artifactId: depItem.artifactId + ) + + CoordinateHolder resolvedCoordinates = new CoordinateHolder( Review Comment: `def`? ########## build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy: ########## @@ -259,91 +261,229 @@ abstract class ExtractDependenciesTask extends DefaultTask { Properties populatePlatformDependencies(CoordinateVersionHolder bomCoordinates, List<CoordinateHolder> exclusionRules, Map<CoordinateHolder, ExtractedDependencyConstraint> constraints, boolean error = true, int level = 0) { Dependency bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom") Configuration dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency) + dependencyConfiguration.transitive = false File bomPomFile = dependencyConfiguration.singleFile - MavenXpp3Reader reader = new MavenXpp3Reader() - Model model = reader.read(new FileReader(bomPomFile)) - + Document doc = parsePom(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 - ) + + // Parent POM populated first so its properties can be overridden by the child + CoordinateVersionHolder parentBom = readParentCoordinates(doc) + if (parentBom) { 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) + + readProperties(doc).each { String name, String value -> + versionProperties.put(name, 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, versionPropertyReference: 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 { + List<ManagedDependency> managedDependencies = readManagedDependencies(doc) + if (managedDependencies.isEmpty()) { 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.") } + return versionProperties + } + + for (ManagedDependency depItem : managedDependencies) { + CoordinateHolder baseCoordinates = new CoordinateHolder( Review Comment: `def`? ########## build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy: ########## @@ -259,91 +261,229 @@ abstract class ExtractDependenciesTask extends DefaultTask { Properties populatePlatformDependencies(CoordinateVersionHolder bomCoordinates, List<CoordinateHolder> exclusionRules, Map<CoordinateHolder, ExtractedDependencyConstraint> constraints, boolean error = true, int level = 0) { Dependency bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom") Configuration dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency) + dependencyConfiguration.transitive = false File bomPomFile = dependencyConfiguration.singleFile - MavenXpp3Reader reader = new MavenXpp3Reader() - Model model = reader.read(new FileReader(bomPomFile)) - + Document doc = parsePom(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 - ) + + // Parent POM populated first so its properties can be overridden by the child + CoordinateVersionHolder parentBom = readParentCoordinates(doc) Review Comment: `def`? ########## build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy: ########## @@ -259,91 +261,229 @@ abstract class ExtractDependenciesTask extends DefaultTask { Properties populatePlatformDependencies(CoordinateVersionHolder bomCoordinates, List<CoordinateHolder> exclusionRules, Map<CoordinateHolder, ExtractedDependencyConstraint> constraints, boolean error = true, int level = 0) { Dependency bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom") Configuration dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency) + dependencyConfiguration.transitive = false File bomPomFile = dependencyConfiguration.singleFile - MavenXpp3Reader reader = new MavenXpp3Reader() - Model model = reader.read(new FileReader(bomPomFile)) - + Document doc = parsePom(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 - ) + + // Parent POM populated first so its properties can be overridden by the child + CoordinateVersionHolder parentBom = readParentCoordinates(doc) + if (parentBom) { 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) + + readProperties(doc).each { String name, String value -> + versionProperties.put(name, 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, versionPropertyReference: 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 { + List<ManagedDependency> managedDependencies = readManagedDependencies(doc) + if (managedDependencies.isEmpty()) { 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.") } + return versionProperties + } + + for (ManagedDependency depItem : managedDependencies) { Review Comment: ```suggestion for (def depItem : managedDependencies) { ``` ########## build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy: ########## @@ -259,91 +261,229 @@ abstract class ExtractDependenciesTask extends DefaultTask { Properties populatePlatformDependencies(CoordinateVersionHolder bomCoordinates, List<CoordinateHolder> exclusionRules, Map<CoordinateHolder, ExtractedDependencyConstraint> constraints, boolean error = true, int level = 0) { Dependency bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom") Configuration dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency) + dependencyConfiguration.transitive = false File bomPomFile = dependencyConfiguration.singleFile - MavenXpp3Reader reader = new MavenXpp3Reader() - Model model = reader.read(new FileReader(bomPomFile)) - + Document doc = parsePom(bomPomFile) Properties versionProperties = new Properties() Review Comment: 2 x `def`? ########## build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy: ########## @@ -259,91 +261,229 @@ abstract class ExtractDependenciesTask extends DefaultTask { Properties populatePlatformDependencies(CoordinateVersionHolder bomCoordinates, List<CoordinateHolder> exclusionRules, Map<CoordinateHolder, ExtractedDependencyConstraint> constraints, boolean error = true, int level = 0) { Dependency bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom") Review Comment: `def`? ########## build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy: ########## @@ -259,91 +261,229 @@ abstract class ExtractDependenciesTask extends DefaultTask { Properties populatePlatformDependencies(CoordinateVersionHolder bomCoordinates, List<CoordinateHolder> exclusionRules, Map<CoordinateHolder, ExtractedDependencyConstraint> constraints, boolean error = true, int level = 0) { Dependency bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom") Configuration dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency) + dependencyConfiguration.transitive = false Review Comment: ```suggestion def dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency).tap { transitive = false } ``` ########## grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy: ########## @@ -367,36 +366,111 @@ ${importStatements} protected void applyDefaultPlugins(Project project) { applySpringBootPlugin(project) + applyGrailsBom(project) + } + /** + * Applies the Grails BOM as a Gradle platform and enables property-based + * version overrides via the standalone + * {@code org.apache.grails.gradle.bom-property-overrides} plugin. + * + * <p>This replaces the Spring Dependency Management plugin with two + * orthogonal pieces:</p> + * <ol> + * <li><strong>BOM import</strong>: {@code grails-bom} is added as a + * Gradle {@code platform()} dependency on every declarable + * configuration, mirroring the global behaviour Spring DM provided + * via {@code configurations.all() + resolutionStrategy.eachDependency()}.</li> + * <li><strong>Property overrides</strong>: the BOM-agnostic + * {@link BomPropertyOverridesPlugin} reads the BOM's + * {@code <properties>} block and applies any project-level + * overrides via Gradle's + * {@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 BomPropertyOverridesPlugin + * @since 8.0 + */ + protected void applyGrailsBom(Project project) { + // Ensure the developmentOnly configuration exists. Spring Boot's plugin + // normally creates this, but using maybeCreate guarantees it is available + // even if plugin ordering changes or Spring Boot is not applied. We do + // this outside afterEvaluate so that other plugins applied during the + // same configuration phase can rely on the configuration existing. + project.configurations.maybeCreate('developmentOnly') + + // The opt-out flag `grails { autoApplyBom = false }` is set in the user's + // build.gradle, which runs AFTER plugin apply. We therefore wait until + // afterEvaluate to read the flag and apply the BOM accordingly. By that + // point all declarable configurations exist (java-base creates them + // during apply), so iterating them eagerly via .each is sufficient - + // any plugin that adds a configuration later is responsible for + // declaring its own BOM coordination if it needs it. project.afterEvaluate { - GrailsExtension ge = project.extensions.getByType(GrailsExtension) - if (ge.springDependencyManagement) { - Plugin dependencyManagementPlugin = project.plugins.findPlugin(DependencyManagementPlugin) - if (dependencyManagementPlugin == null) { - project.plugins.apply(DependencyManagementPlugin) - } - - DependencyManagementExtension dme = project.extensions.findByType(DependencyManagementExtension) + GrailsExtension grailsExtension = project.extensions.findByType(GrailsExtension) Review Comment: `def`? ########## grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy: ########## @@ -367,36 +366,111 @@ ${importStatements} protected void applyDefaultPlugins(Project project) { applySpringBootPlugin(project) + applyGrailsBom(project) + } + /** + * Applies the Grails BOM as a Gradle platform and enables property-based + * version overrides via the standalone + * {@code org.apache.grails.gradle.bom-property-overrides} plugin. + * + * <p>This replaces the Spring Dependency Management plugin with two + * orthogonal pieces:</p> + * <ol> + * <li><strong>BOM import</strong>: {@code grails-bom} is added as a + * Gradle {@code platform()} dependency on every declarable + * configuration, mirroring the global behaviour Spring DM provided + * via {@code configurations.all() + resolutionStrategy.eachDependency()}.</li> + * <li><strong>Property overrides</strong>: the BOM-agnostic + * {@link BomPropertyOverridesPlugin} reads the BOM's + * {@code <properties>} block and applies any project-level + * overrides via Gradle's + * {@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 BomPropertyOverridesPlugin + * @since 8.0 + */ + protected void applyGrailsBom(Project project) { + // Ensure the developmentOnly configuration exists. Spring Boot's plugin + // normally creates this, but using maybeCreate guarantees it is available + // even if plugin ordering changes or Spring Boot is not applied. We do + // this outside afterEvaluate so that other plugins applied during the + // same configuration phase can rely on the configuration existing. + project.configurations.maybeCreate('developmentOnly') + + // The opt-out flag `grails { autoApplyBom = false }` is set in the user's + // build.gradle, which runs AFTER plugin apply. We therefore wait until + // afterEvaluate to read the flag and apply the BOM accordingly. By that + // point all declarable configurations exist (java-base creates them + // during apply), so iterating them eagerly via .each is sufficient - + // any plugin that adds a configuration later is responsible for + // declaring its own BOM coordination if it needs it. project.afterEvaluate { - GrailsExtension ge = project.extensions.getByType(GrailsExtension) - if (ge.springDependencyManagement) { - Plugin dependencyManagementPlugin = project.plugins.findPlugin(DependencyManagementPlugin) - if (dependencyManagementPlugin == null) { - project.plugins.apply(DependencyManagementPlugin) - } - - DependencyManagementExtension dme = project.extensions.findByType(DependencyManagementExtension) + GrailsExtension grailsExtension = project.extensions.findByType(GrailsExtension) + boolean autoApply = grailsExtension == null || grailsExtension.autoApplyBom.getOrElse(true) + if (!autoApply) { + project.logger.info( + 'grails.autoApplyBom is false; skipping automatic application of platform(grails-bom) and bom-property-overrides plugin for project {}', + project.path + ) + return + } - applyBomImport(dme, project) + String grailsVersion = (project.findProperty('grailsVersion') ?: BuildSettings.grailsVersion) as String + String bomCoordinates = "org.apache.grails:grails-bom:${grailsVersion}" as String Review Comment: 2 x `def`? ########## grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy: ########## @@ -367,36 +366,111 @@ ${importStatements} protected void applyDefaultPlugins(Project project) { applySpringBootPlugin(project) + applyGrailsBom(project) + } + /** + * Applies the Grails BOM as a Gradle platform and enables property-based + * version overrides via the standalone + * {@code org.apache.grails.gradle.bom-property-overrides} plugin. + * + * <p>This replaces the Spring Dependency Management plugin with two + * orthogonal pieces:</p> + * <ol> + * <li><strong>BOM import</strong>: {@code grails-bom} is added as a + * Gradle {@code platform()} dependency on every declarable + * configuration, mirroring the global behaviour Spring DM provided + * via {@code configurations.all() + resolutionStrategy.eachDependency()}.</li> + * <li><strong>Property overrides</strong>: the BOM-agnostic + * {@link BomPropertyOverridesPlugin} reads the BOM's + * {@code <properties>} block and applies any project-level + * overrides via Gradle's + * {@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 BomPropertyOverridesPlugin + * @since 8.0 + */ + protected void applyGrailsBom(Project project) { + // Ensure the developmentOnly configuration exists. Spring Boot's plugin + // normally creates this, but using maybeCreate guarantees it is available + // even if plugin ordering changes or Spring Boot is not applied. We do + // this outside afterEvaluate so that other plugins applied during the + // same configuration phase can rely on the configuration existing. + project.configurations.maybeCreate('developmentOnly') + + // The opt-out flag `grails { autoApplyBom = false }` is set in the user's + // build.gradle, which runs AFTER plugin apply. We therefore wait until + // afterEvaluate to read the flag and apply the BOM accordingly. By that + // point all declarable configurations exist (java-base creates them + // during apply), so iterating them eagerly via .each is sufficient - + // any plugin that adds a configuration later is responsible for + // declaring its own BOM coordination if it needs it. project.afterEvaluate { - GrailsExtension ge = project.extensions.getByType(GrailsExtension) - if (ge.springDependencyManagement) { - Plugin dependencyManagementPlugin = project.plugins.findPlugin(DependencyManagementPlugin) - if (dependencyManagementPlugin == null) { - project.plugins.apply(DependencyManagementPlugin) - } - - DependencyManagementExtension dme = project.extensions.findByType(DependencyManagementExtension) + GrailsExtension grailsExtension = project.extensions.findByType(GrailsExtension) + boolean autoApply = grailsExtension == null || grailsExtension.autoApplyBom.getOrElse(true) + if (!autoApply) { + project.logger.info( + 'grails.autoApplyBom is false; skipping automatic application of platform(grails-bom) and bom-property-overrides plugin for project {}', + project.path + ) + return + } - applyBomImport(dme, project) + String grailsVersion = (project.findProperty('grailsVersion') ?: BuildSettings.grailsVersion) as String + String bomCoordinates = "org.apache.grails:grails-bom:${grailsVersion}" as String + + // Apply the BOM platform to all declarable project configurations, matching + // the behavior of the Spring Dependency Management plugin which applied version + // constraints globally via configurations.all() + resolutionStrategy.eachDependency(). + // Non-declarable configurations (e.g. apiElements, runtimeElements) inherit + // constraints through their parent configurations. Tool/annotation-processor + // configurations are excluded because they hold independent classpaths that + // already use their own platforms (e.g. Micronaut's annotation processors + // import io.micronaut.platform:micronaut-platform). Adding grails-bom as a + // second non-enforced platform on those configurations causes version conflict + // resolution to upgrade transitives and break the tools/processors - unlike + // resolutionStrategy hooks, platform() constraints participate in version + // conflict resolution. + project.configurations.each { Configuration conf -> + if (conf.canBeDeclared && !isExcludedFromBomPlatform(conf.name)) { + project.dependencies.add(conf.name, project.dependencies.platform(bomCoordinates)) + } } Review Comment: ```suggestion project.configurations.each { if (it.canBeDeclared && !isExcludedFromBomPlatform(it.name)) { project.dependencies.add(it.name, project.dependencies.platform(bomCoordinates)) } } ``` -- 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]
