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

com.intershop.gradle.icm.tasks.WriteCartridgeDescriptor.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019 Intershop Communications AG.
 *
 * Licensed 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
 *
 *     http://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 com.intershop.gradle.icm.tasks

import com.intershop.gradle.icm.ICMBasePlugin.Companion.CONFIGURATION_CARTRIDGE
import com.intershop.gradle.icm.ICMBasePlugin.Companion.CONFIGURATION_CARTRIDGE_RUNTIME
import com.intershop.gradle.icm.extension.IntershopExtension.Companion.INTERSHOP_GROUP_NAME
import com.intershop.gradle.icm.utils.CartridgeUtil
import org.gradle.api.DefaultTask
import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.artifacts.ResolvedDependency
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.file.ProjectLayout
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.internal.util.PropertiesUtils
import java.nio.charset.StandardCharsets
import java.util.Properties

import javax.inject.Inject
import kotlin.collections.HashSet

/**
 * WriteCartridgeDescriptor Gradle task 'writeCartridgeDescriptor'
 *
 * This task writes a cartridge descriptor file. This file
 * is used by the server startup and special tests.
 */
open class WriteCartridgeDescriptor
@Inject constructor(
    projectLayout: ProjectLayout,
    objectFactory: ObjectFactory
) : DefaultTask() {

    companion object {
        const val DEFAULT_NAME = "writeCartridgeDescriptor"
        const val CARTRIDGE_DESCRIPTOR = "descriptor/cartridge.descriptor"
    }

    /**
     * Set property for descriptor cartridge name property.
     *
     * @param cartridgeName set provider for cartridge name.
     */
    @get:Input
    val cartridgeName: Property = objectFactory.property(String::class.java)

    /**
     * Set property for display cartridge name property.
     *
     * @param cartridgeName set provider for cartridge name.
     */
    @get:Input
    val displayName: Property = objectFactory.property(String::class.java)

    @get:Input
    val cartridgeVersion: Property = objectFactory.property(String::class.java)

    @get:Input
    val cartridgeStyle: Property = objectFactory.property(String::class.java)

    @get:Input
    val cartridgeDependencies: String by lazy {
        flattenToString(
            { project.configurations.getByName(CONFIGURATION_CARTRIDGE).dependencies },
            { value ->
                value.toString().apply {
                    project.logger.debug("CartridgeDependencies of project {}: {}", project.name, this)
                }
            }
        )
    }

    @get:Input
    val runtimeDependencies: String by lazy {
        flattenToString(
            {
                project.configurations.getByName(RUNTIME_CLASSPATH_CONFIGURATION_NAME).resolvedConfiguration
                    .lenientConfiguration.allModuleDependencies
            },
            { value ->
                value.toString().apply {
                    project.logger.debug("RuntimeDependencies of project {}: {}", project.name, this)
                }
            }
        )
    }

    /**
     * Output file for generated descriptor.
     *
     * @property outputFile
     */
    @get:OutputFile
    val outputFile: RegularFileProperty = objectFactory.fileProperty()

    init {
        group = INTERSHOP_GROUP_NAME
        description = "Writes all necessary information of the cartridge to a file."

        outputFile.convention(projectLayout.buildDirectory.file(CARTRIDGE_DESCRIPTOR))

        cartridgeName.convention(project.name)
        displayName.convention(project.description ?: project.name)
        cartridgeVersion.convention(project.version.toString())

        cartridgeStyle.convention(CartridgeUtil.getCartridgeStyle(project).name)

    }

    /**
     * Task method for the creation of a descriptor file.
     */
    @TaskAction
    fun runFileCreation() {
        if (!outputFile.asFile.get().exists()) {
            outputFile.asFile.get().parentFile.mkdirs()
        }

        val props = linkedMapOf()
        val comment = "Intershop descriptor file"

        val cartridgeDependencies = getCartridgeDependencies()
        val libs = getLibs(cartridgeDependencies)

        props["descriptor.version"] = "1.0"

        props["cartridge.dependsOn"] = cartridgeDependencies.map { it.cartridgeId }.toSortedSet()
                .joinToString(separator = ";")
        props["cartridge.dependsOnLibs"] = libs.toSortedSet().joinToString(separator = ";")

        props["cartridge.name"] = cartridgeName.get()
        props["cartridge.displayName"] = displayName.get()
        props["cartridge.description"] = displayName.get()
        props["cartridge.version"] = cartridgeVersion.get()

        if(cartridgeStyle.get().isNotBlank()) {
            props["cartridge.style"] = cartridgeStyle.get()
        }

        val propsObject = Properties()
        propsObject.putAll(props)
        try {
            PropertiesUtils.store(
                propsObject,
                outputFile.asFile.get(),
                comment,
                StandardCharsets.ISO_8859_1,
                "\n"
            )
        } finally {
            project.logger.debug("Wrote cartridge descriptor.")
        }
    }

    /**
     * Reimplementation of [ResolvedDependency.getAllModuleArtifacts] but with circle detection (already processed
     * dependencies will be skipped).
     * Original method [ResolvedDependency.getAllModuleArtifacts] will get stuck in a stack overflow when processing
     * `org.apache.solr:solr-solrj` which is dependent from `org.apache.solr:solr-solrj-zookeeper` and vice versa.
     */
    private fun getAllModuleArtifacts(
            dependency: ResolvedDependency,
            processedDependencies: MutableSet,
    ): Set {
        project.logger.debug("Determining module artifacts of {} transitively", dependency.name)
        // detect circular dependencies like
        if (processedDependencies.contains(dependency)) {
            project.logger.debug("Dependency {} already has been processed", dependency.name)
            return setOf() // no extra artifacts
        }
        processedDependencies.add(dependency) // mark as processed
        var artifacts = dependency.moduleArtifacts // own artifacts
        project.logger.debug("Found artifacts {}", artifacts)
        dependency.children.forEach { child ->
            artifacts = artifacts + getAllModuleArtifacts(child, processedDependencies) // append child artifacts
        }
        return artifacts
    }

    private fun getLibs(cartridgeDependencies: Set): Set {
        val processedDependencies = mutableSetOf()
        // put the ids of all cartridge dependencies into 1 single set for later lookup
        val derivedLibraryDependencyIds = cartridgeDependencies.flatMap {
            cartDep ->
            cartDep.childDependencies.flatMap { childDep -> // 1. get children
                getAllModuleArtifacts(childDep, processedDependencies) // 2. get artifacts of children (transitive!)
            }
        }.asSequence().filter { it.extension.equals("jar") } // 3. jars only
                .map { it.id.componentIdentifier } // 4. get componentIdentifier
                .filter { it is ModuleComponentIdentifier } // 5. ModuleComponentIdentifiers only
                .map {
                    with(it as ModuleComponentIdentifier) { // cast to ModuleComponentIdentifier
                        "${group}:${module}:${version}" // 4. render actual id
                    }
                }.toSet()

        val dependencies = HashSet()
        val resolvedConfig = project.configurations.getByName(RUNTIME_CLASSPATH_CONFIGURATION_NAME)
                .resolvedConfiguration
        // ensure build fails if there are resolve errors
        if (resolvedConfig.hasError()){
            resolvedConfig.rethrowFailure()
        }

        var transitiveCount = 0
        var directCount = 0
        resolvedConfig.lenientConfiguration.allModuleDependencies.forEach { dependency ->
            dependency.moduleArtifacts.forEach { artifact ->
                when (val identifier = artifact.id.componentIdentifier) {
                    is ModuleComponentIdentifier -> {
                        // only add non-cartridge-'jar's
                        if (artifact.extension.equals("jar") && !CartridgeUtil.isCartridge(project, identifier)) {
                            val id = "${identifier.group}:${identifier.module}:${identifier.version}"
                            transitiveCount++
                            // only return libs that haven't come along with other cartridges
                            if (!derivedLibraryDependencyIds.contains(id)) {
                                dependencies.add(id)
                                directCount++
                            }
                        }
                    }
                }
            }
        }
        project.logger.debug("Cartridge {} directly depends on {} libraries and transitively on {}",
                project.name, directCount, transitiveCount)
        return dependencies
    }

    private fun getCartridgeDependencies(): Set {
        val dependencies = HashSet()
        val resolvedConfig = project.configurations.getByName(CONFIGURATION_CARTRIDGE_RUNTIME)
                .resolvedConfiguration
        // ensure build fails if there are resolve errors
        if (resolvedConfig.hasError()){
            resolvedConfig.rethrowFailure()
        }
        resolvedConfig.lenientConfiguration.firstLevelModuleDependencies.forEach { dependency ->
            dependency.moduleArtifacts.forEach { artifact ->
                when (val identifier = artifact.id.componentIdentifier) {
                    is ProjectComponentIdentifier ->
                        dependencies.add(CartridgeDependency(identifier.projectName, dependency.children))
                    is ModuleComponentIdentifier ->
                        if (CartridgeUtil.isCartridge(project, identifier)) {
                            dependencies.add(CartridgeDependency("${identifier.module}:${identifier.version}",
                                    dependency.children))
                        }
                }
            }
        }
        return dependencies
    }

    private fun  flattenToString(
        collectionProvider: () -> Collection,
        stringifier: (value: E) -> String = { value -> value.toString() }
    ): String =
        collectionProvider.invoke().map { value -> stringifier.invoke(value) }.sorted().toString()

    private class CartridgeDependency(val cartridgeId : String, val childDependencies : Set) {
        override fun toString(): String {
            return "CartridgeDependency(cartridgeId='$cartridgeId', childDependencies=$childDependencies)"
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy