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

com.datadog.gradle.plugin.licenses.internal.DependenciesLicenseProvider.kt Maven / Gradle / Ivy

The newest version!
/*
 * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
 * This product includes software developed at Datadog (https://www.datadoghq.com/).
 * Copyright 2016-Present Datadog, Inc.
 */

package com.datadog.gradle.plugin.licenses.internal

import com.datadog.gradle.plugin.licenses.utils.asSequence
import org.gradle.api.Project
import org.gradle.api.artifacts.component.ComponentIdentifier
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.artifacts.result.ComponentSelectionCause
import org.gradle.api.artifacts.result.ResolvedArtifactResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.maven.MavenModule
import org.gradle.maven.MavenPomArtifact
import org.slf4j.LoggerFactory
import org.w3c.dom.Document
import org.w3c.dom.Node
import javax.xml.parsers.DocumentBuilderFactory

@Suppress("TooManyFunctions")
internal class DependenciesLicenseProvider {

    private val logger = LoggerFactory.getLogger(DependenciesLicenseProvider::class.java)

    fun getThirdPartyDependencies(
        project: Project,
        transitive: Boolean,
        listDependencyOnce: Boolean,
    ): List {
        val dependencies = getConfigurationDependenciesMap(project, transitive)

        val dependencyIds = dependencies.values.flatten()
        val pomFilesList = resolvePomFiles(project, dependencyIds)

        return listThirdPartyLicenses(dependencies, pomFilesList, listDependencyOnce)
    }

    // region Internal

    @Suppress("FunctionMaxLength")
    private fun getConfigurationDependenciesMap(
        project: Project,
        transitive: Boolean,
    ): Map> {
        return project.configurations.filter { it.isCanBeResolved }
            .map { configuration ->
                configuration.name to configuration.incoming.resolutionResult.allDependencies
                    .filterIsInstance()
                    .filter {
                        transitive || (it.isRoot() && it.selected.id !is ProjectComponentIdentifier)
                    }
                    .map { it.selected.id }
            }
            .filter { it.second.isNotEmpty() }
            .toMap()
    }

    private fun resolvePomFiles(
        project: Project,
        dependencyIds: List,
    ): Map {
        return project.dependencies
            .createArtifactResolutionQuery()
            .withArtifacts(MavenModule::class.java, MavenPomArtifact::class.java)
            .forComponents(dependencyIds)
            .execute()
            .resolvedComponents
            .flatMap { result ->
                result.getArtifacts(MavenPomArtifact::class.java)
                    .filterIsInstance()
                    .map { result.id to it.file.absolutePath }
            }.toMap()
    }

    private fun listThirdPartyLicenses(
        dependencies: Map>,
        pomFilesList: Map,
        listDependencyOnce: Boolean,
    ): List {
        val licensesCache = mutableMapOf()
        val sorted = dependencies
            .map { (configuration, dependencies) ->
                listThirdPartyLicensesInConfiguration(
                    configuration,
                    dependencies,
                    pomFilesList,
                    licensesCache,
                )
            }
            .flatten()
            .groupBy { it.origin }
            .map { (origin, dependencies) ->
                ThirdPartyDependency(
                    dependencies.map { it.component }.toSortedSet().firstOrNull()
                        ?: ThirdPartyDependency.Component.BUILD,
                    origin,
                    dependencies.firstOrNull { it.license != License.Empty }?.license ?: License.Empty,
                    "",
                    dependencies.filter { !it.artifacts.isNullOrBlank() }
                        .distinctBy { it.artifacts }
                        .joinToString(",") { it.artifacts.orEmpty() },
                )
            }
            .sortedBy { it.origin }
            .sortedBy { it.component.ordinal }

        return if (listDependencyOnce) {
            val knownOrigins = mutableSetOf()
            val result = mutableListOf()
            sorted.forEach {
                if (it.origin !in knownOrigins) {
                    result.add(it)
                    knownOrigins.add(it.origin)
                } else {
                    logger.info("Ignoring ${it.component.csvName}/${it.origin}, already added.")
                }
            }
            result
        } else {
            sorted
        }
    }

    @Suppress("FunctionMaxLength")
    private fun listThirdPartyLicensesInConfiguration(
        configuration: String,
        dependencies: List,
        pomFilesList: Map,
        licensesCache: MutableMap,
    ): List {
        return dependencies
            .filter { it !is ProjectComponentIdentifier }
            .mapNotNull {
                val pomFilePath = pomFilesList[it]
                if (pomFilePath.isNullOrBlank()) {
                    logger.warn("Missing pom.xml file for dependency $it (${it.displayName} / ${it.javaClass.name})")
                    null
                } else {
                    readInfoFromPomFile(configuration, pomFilePath, licensesCache)
                }
            }
    }

    private fun readInfoFromPomFile(
        configuration: String,
        path: String,
        licensesCache: MutableMap,
    ): ThirdPartyDependency? {
        val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(path)

        val groupId = readGroupIdFromPomDocument(document)
        val versionString = readVersionFromDocument(document)
        val licenseString = readLicenseFromPomDocument(document)
        val artifactString = readArtifactFromPomDocument(document)

        val dependency = if (groupId != null) {
            val license =
                getLicense(groupId, artifactString, versionString, licenseString, licensesCache)

            ThirdPartyDependency(
                component = configurationToComponent(configuration),
                origin = groupId,
                license = license,
                copyright = "",
                artifacts = "$artifactString:$versionString",
            )
        } else {
            logger.warn("Missing groupId -> $path")
            null
        }

        if (dependency?.component == ThirdPartyDependency.Component.UNKNOWN) {
            logger.warn("Unknown configuration:'$configuration' for $groupId:$artifactString:$versionString")
        }

        return dependency
    }

    private fun getLicense(
        groupId: String,
        artifact: String?,
        version: String?,
        licenseString: String?,
        licensesCache: MutableMap,
    ): License {
        return if (artifact != null && version != null) {
            val cacheKey = MavenCoordinates(
                groupId,
                artifact,
                version,
            )
            if (licensesCache.containsKey(cacheKey)) {
                licensesCache.getValue(cacheKey)
            } else {
                License.from(licenseString).also {
                    licensesCache[cacheKey] = it
                }
            }
        } else {
            License.from(licenseString)
        }
    }

    private fun configurationToComponent(configuration: String): ThirdPartyDependency.Component {
        return when (configuration) {
            in knownImportConfiguration -> ThirdPartyDependency.Component.IMPORT
            in knownImportTestConfiguration -> ThirdPartyDependency.Component.IMPORT_TEST
            in knownBuildConfiguration -> ThirdPartyDependency.Component.BUILD
            else -> ThirdPartyDependency.Component.UNKNOWN
        }
    }

    private fun readGroupIdFromPomDocument(document: Document): String? {
        val groupIdNode = document.documentElement
            .getFirstDirectChildByTagName(TAG_GROUP_ID)
            ?: document.documentElement
                .getFirstDirectChildByTagName(TAG_PARENT)
                ?.getFirstDirectChildByTagName(TAG_GROUP_ID)

        return groupIdNode?.textContent
    }

    private fun readLicenseFromPomDocument(document: Document): String? {
        val licenses = document.documentElement
            .getFirstDirectChildByTagName(TAG_LICENSES)
            ?.getDirectChildrenByTagName(TAG_LICENSE)
            ?.mapNotNull {
                it.getFirstDirectChildByTagName(TAG_NAME)
            }
            ?.joinToString("/") { it.textContent }
        return licenses
    }

    private fun readArtifactFromPomDocument(document: Document): String? {
        return document.documentElement
            .getFirstDirectChildByTagName(TAG_ARTIFACT_ID)
            ?.textContent
    }

    private fun readVersionFromDocument(document: Document): String? {
        val versionNode = document.documentElement
            .getFirstDirectChildByTagName(TAG_VERSION)
            ?: document.documentElement
                .getFirstDirectChildByTagName(TAG_PARENT)
                ?.getFirstDirectChildByTagName(TAG_VERSION)

        return versionNode?.textContent
    }

    private fun Node.getFirstDirectChildByTagName(tagName: String): Node? {
        return getDirectChildrenByTagName(tagName).firstOrNull()
    }

    private fun Node.getDirectChildrenByTagName(tagName: String): Sequence {
        return childNodes
            .asSequence()
            .filter { it.nodeName == tagName }
    }

    private fun ResolvedDependencyResult.isRoot(): Boolean {
        return from.selectionReason.descriptions.any {
            it.cause == ComponentSelectionCause.ROOT
        }
    }
    // endregion

    private data class MavenCoordinates(
        private val groupId: String,
        private val artifactId: String,
        private val versionString: String,
    )

    companion object {
        private const val TAG_PARENT = "parent"
        private const val TAG_GROUP_ID = "groupId"
        private const val TAG_ARTIFACT_ID = "artifactId"
        private const val TAG_VERSION = "version"
        private const val TAG_LICENSES = "licenses"
        private const val TAG_LICENSE = "license"
        private const val TAG_NAME = "name"

        private val knownImportConfiguration = setOf(
            "archives",
            "compileClasspath",
            "debugCompileClasspath",
            "debugImplementationDependenciesMetadata",
            "debugRuntimeClasspath",
            "implementationDependenciesMetadata",
            "jvmDebugRuntimeClasspath",
            "jvmReleaseCompileClasspath",
            "jvmReleaseRuntimeClasspath",
            "releaseCompileClasspath",
            "releaseImplementationDependenciesMetadata",
            "releaseRuntimeClasspath",
            "runtimeClasspath",
        )
        private val knownImportTestConfiguration = setOf(
            "androidTestImplementationDependenciesMetadata",
            "artDebugUnitTestCompileClasspath",
            "artDebugUnitTestRuntimeClasspath",
            "artReleaseUnitTestCompileClasspath",
            "artReleaseUnitTestRuntimeClasspath",
            "debugAndroidTestCompileClasspath",
            "debugAndroidTestImplementationDependenciesMetadata",
            "debugAndroidTestRuntimeClasspath",
            "debugTestFixturesCompileClasspath",
            "debugTestFixturesRuntimeClasspath",
            "debugUnitTestCompileClasspath",
            "debugUnitTestImplementationDependenciesMetadata",
            "debugUnitTestRuntimeClasspath",
            "jvmDebugAndroidTestCompileClasspath",
            "jvmDebugAndroidTestRuntimeClasspath",
            "jvmDebugUnitTestCompileClasspath",
            "jvmDebugUnitTestRuntimeClasspath",
            "jvmReleaseUnitTestCompileClasspath",
            "jvmReleaseUnitTestRuntimeClasspath",
            "kotlinCompilerPluginClasspathTest",
            "releaseTestFixturesCompileClasspath",
            "releaseTestFixturesCompileClasspath",
            "releaseTestFixturesRuntimeClasspath",
            "releaseUnitTestCompileClasspath",
            "releaseUnitTestImplementationDependenciesMetadata",
            "releaseUnitTestRuntimeClasspath",
            "testCompileClasspath",
            "testFixturesImplementationDependenciesMetadata",
            "testImplementationDependenciesMetadata",
            "testRuntimeClasspath",
            "unmock",
        )
        private val knownBuildConfiguration = setOf(
            "_agp_internal_javaPreCompileDebug_kspClasspath",
            "_agp_internal_javaPreCompileRelease_kspClasspath",
            "_internal_aapt2_binary",
            "_internal-unified-test-platform-android-device-provider-ddmlib",
            "_internal-unified-test-platform-android-device-provider-gradle",
            "_internal-unified-test-platform-android-driver-instrumentation",
            "_internal-unified-test-platform-android-test-plugin",
            "_internal-unified-test-platform-android-test-plugin-host-additional-test-output",
            "_internal-unified-test-platform-android-test-plugin-host-apk-installer",
            "_internal-unified-test-platform-android-test-plugin-host-coverage",
            "_internal-unified-test-platform-android-test-plugin-host-device-info",
            "_internal-unified-test-platform-android-test-plugin-host-emulator-control",
            "_internal-unified-test-platform-android-test-plugin-host-logcat",
            "_internal-unified-test-platform-android-test-plugin-host-retention",
            "_internal-unified-test-platform-android-test-plugin-result-listener-gradle",
            "_internal-unified-test-platform-core",
            "_internal-unified-test-platform-launcher",
            "detekt",
            "dokkaGfmPartialPlugin",
            "dokkaGfmPartialRuntime",
            "dokkaGfmPlugin",
            "dokkaGfmRuntime",
            "dokkaHtmlPartialPlugin",
            "dokkaHtmlPartialRuntime",
            "dokkaHtmlPlugin",
            "dokkaHtmlRuntime",
            "dokkaJavadocPartialPlugin",
            "dokkaJavadocPartialRuntime",
            "dokkaJavadocPlugin",
            "dokkaJavadocRuntime",
            "dokkaJekyllPartialPlugin",
            "dokkaJekyllPartialRuntime",
            "dokkaJekyllPlugin",
            "dokkaJekyllRuntime",
            "kotlin-extension",
            "kotlinBuildToolsApiClasspath",
            "kotlinCompilerClasspath",
            "kotlinCompilerPluginClasspath",
            "kotlinCompilerPluginClasspathDebug",
            "kotlinCompilerPluginClasspathDebugAndroidTest",
            "kotlinCompilerPluginClasspathDebugUnitTest",
            "kotlinCompilerPluginClasspathMain",
            "kotlinCompilerPluginClasspathRelease",
            "kotlinCompilerPluginClasspathReleaseUnitTest",
            "kotlinKlibCommonizerClasspath",
            "koverJvmAgent",
            "koverJvmReporter",
            "kspDebugAndroidTestKotlinProcessorClasspath",
            "kspDebugKotlinProcessorClasspath",
            "kspDebugUnitTestKotlinProcessorClasspath",
            "kspPluginClasspath",
            "kspPluginClasspathNonEmbeddable",
            "kspPluginClasspathNonEmbeddable",
            "kspReleaseKotlinProcessorClasspath",
            "kspReleaseUnitTestKotlinProcessorClasspath",
            "lintClassPath",
        )
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy