All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.name.remal.gradle_plugins.plugins.dependencies.DependencyVersionsExtension.kt Maven / Gradle / Ivy

There is a newer version: 1.9.2
Show newest version
package name.remal.gradle_plugins.plugins.dependencies

import name.remal.asObservable
import name.remal.asSynchronized
import name.remal.concurrentMapOf
import name.remal.escapeRegex
import name.remal.findAll
import name.remal.gradle_plugins.api.BuildTimeConstants.getClassSimpleName
import name.remal.gradle_plugins.dsl.Extension
import name.remal.gradle_plugins.dsl.extensions.createFromNotation
import name.remal.gradle_plugins.dsl.extensions.makeNotTransitive
import name.remal.gradle_plugins.dsl.extensions.matches
import name.remal.gradle_plugins.dsl.utils.DependencyNotation
import name.remal.gradle_plugins.dsl.utils.DependencyNotationMatcher
import name.remal.gradle_plugins.dsl.utils.getGradleLogger
import name.remal.gradle_plugins.dsl.utils.parseDependencyNotation
import name.remal.version.Version
import org.gradle.api.artifacts.ConfigurationContainer
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.artifacts.dsl.RepositoryHandler
import org.gradle.api.artifacts.repositories.MavenArtifactRepository
import org.gradle.internal.resolve.ModuleVersionNotFoundException
import java.net.URI
import java.util.Comparator.reverseOrder
import java.util.concurrent.ConcurrentMap
import java.util.concurrent.TimeUnit.SECONDS
import kotlin.text.RegexOption.IGNORE_CASE

@Extension
class DependencyVersionsExtension(
    private val dependencies: DependencyHandler,
    private val configurations: ConfigurationContainer,
    private val repositories: RepositoryHandler
) {

    companion object {
        private val logger = getGradleLogger(DependencyVersionsExtension::class.java)
        private val allCaches: ConcurrentMap = concurrentMapOf()
        private val predefinedInvalidVersionPatterns = listOf(
            Regex("maven-metadata(?:-[^.]+)?\\.xml")
        )
    }


    private val projectCaches = Caches()

    init {
        repositories.whenObjectAdded { projectCaches.clear() }
        repositories.whenObjectRemoved { projectCaches.clear() }
    }


    private fun  MutableSet.asProjectCacheCleaner(): MutableSet {
        return asObservable().apply {
            registerCollectionChangedHandler { projectCaches.clear() }
        }
    }


    var rejectVersions = mutableSetOf().asProjectCacheCleaner()
        set(value) {
            field = value.asProjectCacheCleaner()
        }

    fun rejectVersions(notationPattern: String) {
        rejectVersions.add(notationPattern)
    }

    fun rejectVersions(vararg notationPatterns: String) {
        rejectVersions.addAll(notationPatterns)
    }


    var allowAllVersionsFor = mutableSetOf().asProjectCacheCleaner()
        set(value) {
            field = value.asProjectCacheCleaner()
        }

    fun allowAllVersionsFor(notationPattern: String) {
        allowAllVersionsFor.add(notationPattern)
    }

    fun allowAllVersionsFor(vararg notationPatterns: String) {
        allowAllVersionsFor.addAll(notationPatterns)
    }


    var invalidVersionTokens = mutableSetOf(
        "snapshot",
        "nightly",
        "rc", "cr",
        "ea",
        "milestone", "m",
        "beta", "b",
        "alpha", "a",
        "dev",
        "pr",
        "redhat"
    ).asProjectCacheCleaner()
        set(value) {
            field = value.asProjectCacheCleaner()
        }

    fun invalidVersionToken(token: String) {
        invalidVersionTokens.add(token)
    }

    fun invalidVersionTokens(vararg tokens: String) {
        invalidVersionTokens.addAll(tokens)
    }


    fun getFirstInvalidToken(version: String): String? {
        return invalidVersionTokens.toSortedSet(reverseOrder()).firstOrNull { Regex(".*[._-]${escapeRegex(it)}[._-]?\\d*(?:\\.\\d*)*(?:[._-].*)?", IGNORE_CASE).matches(version) }
    }


    fun resolveLatestVersion(notation: String): String {
        return caches.resolveLatestVersionCache.computeIfAbsent(parseDependencyNotation(notation).withDefaultLatestVersion()) { dependencyNotation ->
            val dependency = dependencies.createFromNotation(dependencyNotation)
            val rejectMatchers = rejectVersions.mapTo(mutableSetOf(), ::DependencyNotationMatcher)
            val allowMatchers = allowAllVersionsFor.mapTo(mutableSetOf(), ::DependencyNotationMatcher)
            configurations.detachedConfiguration().let { conf ->
                conf.resolutionStrategy { it.cacheDynamicVersionsFor(0, SECONDS); it.cacheChangingModulesFor(0, SECONDS) }
                conf.makeNotTransitive()
                conf.resolutionStrategy {
                    it.componentSelection {
                        it.all { selection ->
                            with(selection) {
                                val version = candidate.version
                                if (predefinedInvalidVersionPatterns.any { it.matches(version) }) {
                                    reject(version)
                                    return@all
                                }

                                rejectMatchers.firstOrNull { it.matches(candidate) }?.let { matcher ->
                                    reject("Rejected version: $matcher")
                                    return@all
                                }

                                if (allowMatchers.any { it.matches(candidate) }) {
                                    return@all
                                }

                                getFirstInvalidToken(version)?.let { token ->
                                    reject("Invalid version token: $token")
                                    return@all
                                }
                            }
                        }
                    }
                }

                conf.dependencies.add(dependency)

                return@computeIfAbsent conf.resolvedConfiguration.firstLevelModuleDependencies.first().moduleVersion.also {
                    logger.info("Dependency {} resolved with {} version", dependencyNotation, it)
                }
            }
        }
    }


    fun resolveAllVersions(notation: String): List {
        return caches.allVersionsCache.computeIfAbsent(parseDependencyNotation(notation).withDefaultLatestVersion()) { dependencyNotation ->
            val dependency = dependencies.createFromNotation(dependencyNotation)
            val rejectMatchers = rejectVersions.mapTo(mutableSetOf(), ::DependencyNotationMatcher)
            val allowMatchers = allowAllVersionsFor.mapTo(mutableSetOf(), ::DependencyNotationMatcher)
            val result = mutableSetOf().asSynchronized()
            configurations.detachedConfiguration().also { conf ->
                conf.resolutionStrategy { it.cacheDynamicVersionsFor(0, SECONDS); it.cacheChangingModulesFor(0, SECONDS) }
                conf.makeNotTransitive()
                conf.dependencies.add(dependency)
                conf.resolutionStrategy {
                    it.componentSelection {
                        it.all { selection ->
                            with(selection) {
                                val version = candidate.version
                                if (predefinedInvalidVersionPatterns.any { it.matches(version) }) {
                                    reject(version)
                                    return@all
                                }

                                rejectMatchers.firstOrNull { it.matches(candidate) }?.let { matcher ->
                                    reject("Rejected version: $matcher")
                                    return@all
                                }

                                if (allowMatchers.none { it.matches(candidate) }) {
                                    getFirstInvalidToken(version)?.let { token ->
                                        reject("Invalid version token: $token")
                                        return@all
                                    }
                                }

                                result.add(version)

                                reject("Accepted: " + DependencyVersionsExtension::resolveAllVersions.name)
                            }
                        }
                    }
                }

                try {
                    conf.resolve()
                } catch (e: Exception) {
                    if (e.isFullyIgnorable) {
                        // do nothing
                    } else if (e.isIgnorable) {
                        logger.warn(e.toString())
                        // do nothing
                    } else {
                        throw e
                    }
                }
            }

            return@computeIfAbsent result.toSortedVersions()
        }
    }


    private val caches: Caches
        get() {
            val uris = hashSetOf()
            repositories.forEach {
                if (it !is MavenArtifactRepository) {
                    return projectCaches
                }
                uris.add(it.url)
            }

            return allCaches.computeIfAbsent(
                AllCachesKey(
                    rejectVersions.toSortedSet(),
                    allowAllVersionsFor.toSortedSet(),
                    invalidVersionTokens.toSortedSet(),
                    uris
                ),
                { Caches() }
            )
        }

    private data class AllCachesKey(
        val rejectVersions: Set,
        val allowAllVersionsFor: Set,
        val invalidVersionTokens: Set,
        val repositoriesUris: Set
    )


    private class Caches {

        val resolveLatestVersionCache: ConcurrentMap = concurrentMapOf()
        val allVersionsCache: ConcurrentMap> = concurrentMapOf()

        fun clear() {
            resolveLatestVersionCache.clear()
            allVersionsCache.clear()
        }

    }


    private fun Collection.toSortedVersions(): List {
        val versions = mapNotNullTo(mutableListOf(), Version::parseOrNull)
        if (size == versions.size) {
            return versions.sortedDescending().map(Version::toString)
        } else {
            return this.toList()
        }
    }

    private val Throwable.isFullyIgnorable: Boolean
        get() = findAll(Throwable::class.java).any {
            it.javaClass.simpleName == getClassSimpleName(ModuleVersionNotFoundException::class.java)
        }

    private val Throwable.isIgnorable: Boolean
        get() = isFullyIgnorable || findAll(Throwable::class.java).any {
            false
        }

}


fun DependencyVersionsExtension.resolveLatestVersion(notation: DependencyNotation) = resolveLatestVersion(notation.toString())
fun DependencyVersionsExtension.resolveAllVersions(notation: DependencyNotation) = resolveAllVersions(notation.toString())

inline fun  DependencyVersionsExtension.withoutFilters(action: DependencyVersionsExtension.() -> R): R {
    val rejectVersionsPrev = rejectVersions.toSet()
    val allowAllVersionsForPrev = allowAllVersionsFor.toSet()
    val invalidVersionTokensPrev = invalidVersionTokens.toSet()
    rejectVersions = mutableSetOf()
    allowAllVersionsFor = mutableSetOf()
    invalidVersionTokens = mutableSetOf()

    try {
        return action(this)

    } finally {
        rejectVersions = rejectVersionsPrev.toMutableSet()
        allowAllVersionsFor = allowAllVersionsForPrev.toMutableSet()
        invalidVersionTokens = invalidVersionTokensPrev.toMutableSet()
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy