1717package nebula.plugin.resolutionrules
1818
1919import com.netflix.nebula.interop.VersionWithSelector
20- import org.gradle.api.Action
2120import org.gradle.api.Project
2221import org.gradle.api.artifacts.*
2322import org.gradle.api.artifacts.component.ComponentSelector
2423import org.gradle.api.artifacts.component.ModuleComponentSelector
24+ import org.gradle.api.internal.artifacts.DefaultModuleIdentifier
2525import org.gradle.api.internal.artifacts.dsl.ModuleVersionSelectorParsers
26+ import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.VersionSelector
2627import org.gradle.api.logging.Logger
2728import org.gradle.api.logging.Logging
2829import java.io.Serializable
@@ -58,7 +59,7 @@ data class RuleSet(
5859) {
5960
6061 fun dependencyRulesPartOne () =
61- listOf (replace, substitute, reject, deny, exclude).flatten()
62+ listOf (replace, deny, exclude).flatten() + listOf ( SubstituteRules (substitute), RejectRules (reject) )
6263
6364 fun dependencyRulesPartTwo (coreAlignmentEnabled : Boolean ) =
6465 if (coreAlignmentEnabled)
@@ -67,10 +68,7 @@ data class RuleSet(
6768 emptyList()
6869
6970 fun resolveRules (coreAlignmentEnabled : Boolean ) =
70- if (coreAlignmentEnabled)
71- emptyList()
72- else
73- listOf (AlignRules (align))
71+ if (coreAlignmentEnabled) emptyList() else listOf (AlignRules (align))
7472
7573 fun generateAlignmentBelongsToName () {
7674 align.forEachIndexed { index, alignRule ->
@@ -109,20 +107,16 @@ data class ReplaceRule(
109107 override val author : String ,
110108 override val date : String
111109) : ModuleRule {
112- private val moduleId = ModuleIdentifier .valueOf(module)
113- private val withModuleId = ModuleIdentifier .valueOf(with)
114-
115110 override fun apply (
116111 project : Project ,
117112 configuration : Configuration ,
118113 resolutionStrategy : ResolutionStrategy ,
119114 extension : NebulaResolutionRulesExtension
120115 ) {
121- project.dependencies.modules.module(moduleId.toString() ) {
116+ project.dependencies.modules.module(module ) {
122117 val details = it as ComponentModuleMetadataDetails
123- val message =
124- " replaced ${moduleId.organization} :${moduleId.name} -> ${withModuleId.organization} :${withModuleId.name} because '$reason ' by rule $ruleSet "
125- details.replacedBy(withModuleId.toString(), message)
118+ val message = " replaced $module -> $with because '$reason ' by rule $ruleSet "
119+ details.replacedBy(with, message)
126120 }
127121 }
128122}
@@ -131,10 +125,10 @@ data class SubstituteRule(
131125 val module : String , val with : String , override var ruleSet : String? ,
132126 override val reason : String , override val author : String , override val date : String
133127) : BasicRule, Serializable {
134- private lateinit var substitutedModule: ComponentSelector
135- private lateinit var withComponentSelector: ModuleComponentSelector
136- private lateinit var withVersionSelector: ModuleVersionSelector
137- private val versionSelector by lazy {
128+ lateinit var substitutedModule: ComponentSelector
129+ lateinit var withComponentSelector: ModuleComponentSelector
130+ lateinit var withVersionSelector: ModuleVersionSelector
131+ val versionSelector by lazy {
138132 val version = (substitutedModule as ModuleComponentSelector ).version
139133 VersionWithSelector (version).asSelector()
140134 }
@@ -145,44 +139,71 @@ data class SubstituteRule(
145139 resolutionStrategy : ResolutionStrategy ,
146140 extension : NebulaResolutionRulesExtension
147141 ) {
148- val substitution = resolutionStrategy.dependencySubstitution
149- if (! this ::substitutedModule.isInitialized) {
150- substitutedModule = substitution.module(module)
151- val withModule = substitution.module(with)
152- if (withModule !is ModuleComponentSelector ) {
153- throw SubstituteRuleMissingVersionException (with, this )
154- }
155- withComponentSelector = substitution.module(with) as ModuleComponentSelector
156- withVersionSelector = ModuleVersionSelectorParsers .parser().parseNotation(withComponentSelector.displayName)
142+ throw UnsupportedOperationException (" Substitution rules cannot be applied directly and must be applied via SubstituteRules" )
143+ }
144+ }
145+
146+ class SubstituteRules (val rules : List <SubstituteRule >) : Rule {
147+ private lateinit var versionedRulesById: Map <ModuleIdentifier , List <SubstituteRule >>
148+ private lateinit var unversionedRules: List <SubstituteRule >
149+
150+ override fun apply (
151+ project : Project ,
152+ configuration : Configuration ,
153+ resolutionStrategy : ResolutionStrategy ,
154+ extension : NebulaResolutionRulesExtension
155+ ) {
156+ if (! this ::versionedRulesById.isInitialized) {
157+ val (versionedRules, unversionedRules) = rules.map { rule ->
158+ val substitution = resolutionStrategy.dependencySubstitution
159+ rule.substitutedModule = substitution.module(rule.module)
160+ val withModule = substitution.module(rule.with)
161+ if (withModule !is ModuleComponentSelector ) {
162+ throw SubstituteRuleMissingVersionException (rule.with, rule)
163+ }
164+ rule.withComponentSelector = withModule
165+ rule.withVersionSelector = ModuleVersionSelectorParsers .parser()
166+ .parseNotation(rule.withComponentSelector.displayName)
167+ rule
168+ }.partition { it.substitutedModule is ModuleComponentSelector }
169+
170+ this .unversionedRules = unversionedRules
171+ versionedRulesById =
172+ versionedRules.groupBy { (it.substitutedModule as ModuleComponentSelector ).moduleIdentifier }
157173 }
158174
159- if (substitutedModule is ModuleComponentSelector ) {
175+ if (versionedRulesById.isNotEmpty() ) {
160176 // We use eachDependency because dependencySubstitutions.all causes configuration task dependencies to resolve at configuration time
161177 resolutionStrategy.eachDependency { details ->
162178 val requested = details.requested
163- val moduleSelector = substitutedModule as ModuleComponentSelector
164- if (requested.group == moduleSelector.group && requested.module.name == moduleSelector.module) {
165- val requestedSelectorVersion = requested.version
166- if (versionSelector.accept(requestedSelectorVersion)
167- && ! requested.toString().contains(" .+" )
168- && ! requested.toString().contains(" latest" )
179+ val requestedString = requested.toString()
180+ val rules = versionedRulesById[requested.module] ? : return @eachDependency
181+ val requestedSelectorVersion = requested.version
182+ rules.forEach { rule ->
183+ if (rule.versionSelector.accept(requestedSelectorVersion)
184+ && ! requestedString.contains(" .+" )
185+ && ! requestedString.contains(" latest" )
169186 ) {
170187 // Note on `useTarget`:
171188 // Forcing modules via ResolutionStrategy.force(Object...) uses this capability.
172189 // from https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/DependencyResolveDetails.html
173- details.useTarget(withVersionSelector) // We can't pass a ModuleComponentSelector here so we take the conversion hit
174- details.because(" substituted $substitutedModule with $withComponentSelector because '$reason ' by rule $ruleSet " )
190+ details.useTarget(rule.withVersionSelector) // We can't pass a ModuleComponentSelector here so we take the conversion hit
191+ details.because(" substituted ${rule.substitutedModule} with ${rule.withComponentSelector} because '${rule.reason} ' by rule ${rule.ruleSet} " )
192+ return @eachDependency
175193 }
176194 }
177195 }
178- } else {
179- var message = " substituted $withComponentSelector because '$reason ' by rule $ruleSet "
196+ }
197+
198+ unversionedRules.forEach { rule ->
199+ val substitutedModule = rule.substitutedModule
200+ val withComponentSelector = rule.withComponentSelector
201+ var message = " substituted $withComponentSelector because '${rule.reason} ' by rule ${rule.ruleSet} "
180202
181203 val selectorNameSections = substitutedModule.displayName.split(" :" )
182204 if (selectorNameSections.size > 2 ) {
183205 val selectorGroupAndArtifact = " ${selectorNameSections[0 ]} :${selectorNameSections[1 ]} "
184- message =
185- " substituted $selectorGroupAndArtifact with $withComponentSelector because '$reason ' by rule $ruleSet "
206+ message = " substituted $selectorGroupAndArtifact with $withComponentSelector because '${rule.reason} ' by rule ${rule.ruleSet} "
186207 }
187208
188209 resolutionStrategy.dependencySubstitution {
@@ -191,7 +212,6 @@ data class SubstituteRule(
191212 .with (withComponentSelector)
192213 }
193214 }
194-
195215 }
196216}
197217
@@ -202,8 +222,31 @@ data class RejectRule(
202222 override val author : String ,
203223 override val date : String
204224) : ModuleRule {
205- private val moduleId = ModuleVersionIdentifier .valueOf(module)
206- private val versionSelector = VersionWithSelector (moduleId.version).asSelector()
225+ val moduleIdentifier: ModuleIdentifier
226+ lateinit var versionSelector: VersionSelector
227+
228+ init {
229+ val parts = module.split(" :" )
230+ moduleIdentifier = DefaultModuleIdentifier .newId(parts[0 ], parts[1 ])
231+ if (parts.size == 3 ) {
232+ versionSelector = VersionWithSelector (parts[2 ]).asSelector()
233+ }
234+ }
235+
236+ override fun apply (
237+ project : Project ,
238+ configuration : Configuration ,
239+ resolutionStrategy : ResolutionStrategy ,
240+ extension : NebulaResolutionRulesExtension
241+ ) {
242+ throw UnsupportedOperationException (" Reject rules cannot be applied directly and must be applied via RejectRules" )
243+ }
244+
245+ fun hasVersionSelector (): Boolean = this ::versionSelector.isInitialized
246+ }
247+
248+ data class RejectRules (val rules : List <RejectRule >) : Rule {
249+ private val ruleByModuleIdentifier = rules.groupBy { it.moduleIdentifier }
207250
208251 override fun apply (
209252 project : Project ,
@@ -213,10 +256,14 @@ data class RejectRule(
213256 ) {
214257 resolutionStrategy.componentSelection.all { selection ->
215258 val candidate = selection.candidate
216- if (candidate.group == moduleId.organization && candidate.module == moduleId.name) {
217- if (! moduleId.hasVersion() || versionSelector.accept(candidate.version)) {
218- val message = " rejected by rule $ruleSet because '$reason '"
259+ val rules = ruleByModuleIdentifier[candidate.moduleIdentifier] ? : return @all
260+ rules.forEach { rule ->
261+ if (! rule.hasVersionSelector() || rule.versionSelector.accept(candidate.version)) {
262+ val message = " rejected by rule ${rule.ruleSet} because '${rule.reason} '"
219263 selection.reject(message)
264+ if (! rule.hasVersionSelector()) {
265+ return @forEach
266+ }
220267 }
221268 }
222269 }
@@ -230,7 +277,16 @@ data class DenyRule(
230277 override val author : String ,
231278 override val date : String
232279) : ModuleRule {
233- private val moduleId = ModuleVersionIdentifier .valueOf(module)
280+ private val moduleId: ModuleIdentifier
281+ private lateinit var version: String
282+
283+ init {
284+ val parts = module.split(" :" )
285+ moduleId = DefaultModuleIdentifier .newId(parts[0 ], parts[1 ])
286+ if (parts.size == 3 ) {
287+ version = parts[2 ]
288+ }
289+ }
234290
235291 override fun apply (
236292 project : Project ,
@@ -239,16 +295,14 @@ data class DenyRule(
239295 extension : NebulaResolutionRulesExtension
240296 ) {
241297 val match = configuration.allDependencies.find {
242- it is ExternalModuleDependency && it.group == moduleId.organization && it.name == moduleId.name
298+ it is ExternalModuleDependency && it.group == moduleId.group && it.name == moduleId.name
243299 }
244- if (match != null && (! moduleId.hasVersion() || match.version == moduleId.version)) {
245- resolutionStrategy.componentSelection.withModule(
246- " ${moduleId.organization} :${moduleId.name} " ,
247- Action <ComponentSelection > { selection ->
248- val message = " denied by rule $ruleSet because '$reason '"
249- selection.reject(message)
250- })
251- throw DependencyDeniedException (moduleId, this )
300+ if (match != null && (! this ::version.isInitialized || match.version == version)) {
301+ resolutionStrategy.componentSelection.withModule(moduleId) { selection ->
302+ val message = " denied by rule $ruleSet because '$reason '"
303+ selection.reject(message)
304+ }
305+ throw DependencyDeniedException (module, this )
252306 }
253307 }
254308}
@@ -261,7 +315,12 @@ data class ExcludeRule(
261315 override val date : String
262316) : ModuleRule {
263317 private val logger: Logger = Logging .getLogger(ExcludeRule ::class .java)
264- private val moduleId = ModuleIdentifier .valueOf(module)
318+ private val moduleId: ModuleIdentifier
319+
320+ init {
321+ val parts = module.split(" :" )
322+ moduleId = DefaultModuleIdentifier .newId(parts[0 ], parts[1 ])
323+ }
265324
266325 @Override
267326 override fun apply (
@@ -271,19 +330,18 @@ data class ExcludeRule(
271330 extension : NebulaResolutionRulesExtension
272331 ) {
273332 val message =
274- " excluded ${ moduleId.organization} : ${moduleId.name} and transitive dependencies for all dependencies of this configuration by rule $ruleSet "
333+ " excluded $moduleId and transitive dependencies for all dependencies of this configuration by rule $ruleSet "
275334 logger.debug(message)
276335 // TODO: would like a core Gradle feature that accepts a reason
277- configuration.exclude(moduleId.organization, moduleId.name)
278-
279- resolutionStrategy.componentSelection.withModule(" ${moduleId.organization} :${moduleId.name} " ) { selection ->
336+ configuration.exclude(moduleId.group, moduleId.name)
337+ resolutionStrategy.componentSelection.withModule(moduleId.toString()) { selection ->
280338 selection.reject(message)
281339 }
282340 }
283341}
284342
285- class DependencyDeniedException (moduleId : ModuleVersionIdentifier , rule : DenyRule ) :
286- Exception (" Dependency $moduleId denied by rule ${rule.ruleSet} " )
343+ class DependencyDeniedException (notation : String , rule : DenyRule ) :
344+ Exception (" Dependency $notation denied by rule ${rule.ruleSet} " )
287345
288346class SubstituteRuleMissingVersionException (moduleId : String , rule : SubstituteRule ) :
289347 Exception (" The dependency to be substituted ($moduleId ) must have a version. Rule ${rule.ruleSet} is invalid" )
0 commit comments